Merge branch 'develop' into travis/pinned_messages

This commit is contained in:
Travis Ralston 2017-10-14 16:10:32 -06:00
commit c34b55c6c7
185 changed files with 4991 additions and 3543 deletions

View file

@ -1,10 +1,7 @@
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update. # autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
src/async-components/views/dialogs/EncryptedEventDialog.js
src/autocomplete/AutocompleteProvider.js src/autocomplete/AutocompleteProvider.js
src/autocomplete/Autocompleter.js src/autocomplete/Autocompleter.js
src/autocomplete/Components.js
src/autocomplete/DuckDuckGoProvider.js
src/autocomplete/EmojiProvider.js src/autocomplete/EmojiProvider.js
src/autocomplete/UserProvider.js src/autocomplete/UserProvider.js
src/CallHandler.js src/CallHandler.js
@ -12,11 +9,9 @@ src/component-index.js
src/components/structures/ContextualMenu.js src/components/structures/ContextualMenu.js
src/components/structures/CreateRoom.js src/components/structures/CreateRoom.js
src/components/structures/FilePanel.js src/components/structures/FilePanel.js
src/components/structures/InteractiveAuth.js
src/components/structures/LoggedInView.js src/components/structures/LoggedInView.js
src/components/structures/login/ForgotPassword.js src/components/structures/login/ForgotPassword.js
src/components/structures/login/Login.js src/components/structures/login/Login.js
src/components/structures/login/PostRegistration.js
src/components/structures/login/Registration.js src/components/structures/login/Registration.js
src/components/structures/MessagePanel.js src/components/structures/MessagePanel.js
src/components/structures/NotificationPanel.js src/components/structures/NotificationPanel.js
@ -27,51 +22,32 @@ src/components/structures/TimelinePanel.js
src/components/structures/UploadBar.js src/components/structures/UploadBar.js
src/components/views/avatars/BaseAvatar.js src/components/views/avatars/BaseAvatar.js
src/components/views/avatars/MemberAvatar.js src/components/views/avatars/MemberAvatar.js
src/components/views/avatars/RoomAvatar.js
src/components/views/create_room/CreateRoomButton.js
src/components/views/create_room/Presets.js
src/components/views/create_room/RoomAlias.js src/components/views/create_room/RoomAlias.js
src/components/views/dialogs/ChatCreateOrReuseDialog.js src/components/views/dialogs/ChatCreateOrReuseDialog.js
src/components/views/dialogs/DeactivateAccountDialog.js src/components/views/dialogs/DeactivateAccountDialog.js
src/components/views/dialogs/InteractiveAuthDialog.js
src/components/views/dialogs/UnknownDeviceDialog.js src/components/views/dialogs/UnknownDeviceDialog.js
src/components/views/elements/AccessibleButton.js
src/components/views/elements/ActionButton.js
src/components/views/elements/AddressSelector.js src/components/views/elements/AddressSelector.js
src/components/views/elements/AddressTile.js
src/components/views/elements/CreateRoomButton.js src/components/views/elements/CreateRoomButton.js
src/components/views/elements/DeviceVerifyButtons.js src/components/views/elements/DeviceVerifyButtons.js
src/components/views/elements/DirectorySearchBox.js src/components/views/elements/DirectorySearchBox.js
src/components/views/elements/Dropdown.js
src/components/views/elements/EditableText.js src/components/views/elements/EditableText.js
src/components/views/elements/EditableTextContainer.js
src/components/views/elements/HomeButton.js src/components/views/elements/HomeButton.js
src/components/views/elements/LanguageDropdown.js
src/components/views/elements/MemberEventListSummary.js src/components/views/elements/MemberEventListSummary.js
src/components/views/elements/PowerSelector.js src/components/views/elements/PowerSelector.js
src/components/views/elements/ProgressBar.js
src/components/views/elements/RoomDirectoryButton.js src/components/views/elements/RoomDirectoryButton.js
src/components/views/elements/SettingsButton.js src/components/views/elements/SettingsButton.js
src/components/views/elements/StartChatButton.js src/components/views/elements/StartChatButton.js
src/components/views/elements/TintableSvg.js src/components/views/elements/TintableSvg.js
src/components/views/elements/UserSelector.js src/components/views/elements/UserSelector.js
src/components/views/login/CaptchaForm.js
src/components/views/login/CasLogin.js
src/components/views/login/CountryDropdown.js src/components/views/login/CountryDropdown.js
src/components/views/login/CustomServerDialog.js
src/components/views/login/InteractiveAuthEntryComponents.js src/components/views/login/InteractiveAuthEntryComponents.js
src/components/views/login/LoginHeader.js
src/components/views/login/PasswordLogin.js src/components/views/login/PasswordLogin.js
src/components/views/login/RegistrationForm.js src/components/views/login/RegistrationForm.js
src/components/views/login/ServerConfig.js src/components/views/login/ServerConfig.js
src/components/views/messages/MAudioBody.js
src/components/views/messages/MessageEvent.js
src/components/views/messages/MFileBody.js src/components/views/messages/MFileBody.js
src/components/views/messages/MImageBody.js src/components/views/messages/MImageBody.js
src/components/views/messages/MVideoBody.js
src/components/views/messages/RoomAvatarEvent.js src/components/views/messages/RoomAvatarEvent.js
src/components/views/messages/TextualBody.js src/components/views/messages/TextualBody.js
src/components/views/messages/TextualEvent.js
src/components/views/room_settings/AliasSettings.js src/components/views/room_settings/AliasSettings.js
src/components/views/room_settings/ColorSettings.js src/components/views/room_settings/ColorSettings.js
src/components/views/room_settings/UrlPreviewSettings.js src/components/views/room_settings/UrlPreviewSettings.js
@ -86,14 +62,11 @@ src/components/views/rooms/MemberList.js
src/components/views/rooms/MemberTile.js src/components/views/rooms/MemberTile.js
src/components/views/rooms/MessageComposer.js src/components/views/rooms/MessageComposer.js
src/components/views/rooms/MessageComposerInput.js src/components/views/rooms/MessageComposerInput.js
src/components/views/rooms/PresenceLabel.js
src/components/views/rooms/ReadReceiptMarker.js src/components/views/rooms/ReadReceiptMarker.js
src/components/views/rooms/RoomList.js src/components/views/rooms/RoomList.js
src/components/views/rooms/RoomNameEditor.js
src/components/views/rooms/RoomPreviewBar.js src/components/views/rooms/RoomPreviewBar.js
src/components/views/rooms/RoomSettings.js src/components/views/rooms/RoomSettings.js
src/components/views/rooms/RoomTile.js src/components/views/rooms/RoomTile.js
src/components/views/rooms/RoomTopicEditor.js
src/components/views/rooms/SearchableEntityList.js src/components/views/rooms/SearchableEntityList.js
src/components/views/rooms/SearchResultTile.js src/components/views/rooms/SearchResultTile.js
src/components/views/rooms/TopUnreadMessagesBar.js src/components/views/rooms/TopUnreadMessagesBar.js
@ -103,8 +76,6 @@ src/components/views/settings/ChangeAvatar.js
src/components/views/settings/ChangeDisplayName.js src/components/views/settings/ChangeDisplayName.js
src/components/views/settings/ChangePassword.js src/components/views/settings/ChangePassword.js
src/components/views/settings/DevicesPanel.js src/components/views/settings/DevicesPanel.js
src/components/views/settings/DevicesPanelEntry.js
src/components/views/settings/EnableNotificationsButton.js
src/ContentMessages.js src/ContentMessages.js
src/HtmlUtils.js src/HtmlUtils.js
src/ImageUtils.js src/ImageUtils.js
@ -122,7 +93,6 @@ src/RichText.js
src/Roles.js src/Roles.js
src/Rooms.js src/Rooms.js
src/ScalarAuthClient.js src/ScalarAuthClient.js
src/ScalarMessaging.js
src/Tinter.js src/Tinter.js
src/UiEffects.js src/UiEffects.js
src/Unread.js src/Unread.js
@ -135,17 +105,13 @@ src/Velociraptor.js
src/VelocityBounce.js src/VelocityBounce.js
src/WhoIsTyping.js src/WhoIsTyping.js
src/wrappers/withMatrixClient.js src/wrappers/withMatrixClient.js
test/all-tests.js
test/components/structures/login/Registration-test.js test/components/structures/login/Registration-test.js
test/components/structures/MessagePanel-test.js test/components/structures/MessagePanel-test.js
test/components/structures/ScrollPanel-test.js test/components/structures/ScrollPanel-test.js
test/components/structures/TimelinePanel-test.js test/components/structures/TimelinePanel-test.js
test/components/stub-component.js
test/components/views/dialogs/InteractiveAuthDialog-test.js test/components/views/dialogs/InteractiveAuthDialog-test.js
test/components/views/elements/MemberEventListSummary-test.js test/components/views/elements/MemberEventListSummary-test.js
test/components/views/login/RegistrationForm-test.js test/components/views/login/RegistrationForm-test.js
test/components/views/rooms/MessageComposerInput-test.js test/components/views/rooms/MessageComposerInput-test.js
test/mock-clock.js test/mock-clock.js
test/skinned-sdk.js
test/stores/RoomViewStore-test.js test/stores/RoomViewStore-test.js
test/test-utils.js

View file

@ -63,23 +63,22 @@ import dis from './dispatcher';
global.mxCalls = { global.mxCalls = {
//room_id: MatrixCall //room_id: MatrixCall
}; };
var calls = global.mxCalls; const calls = global.mxCalls;
var ConferenceHandler = null; let ConferenceHandler = null;
var audioPromises = {}; const audioPromises = {};
function play(audioId) { function play(audioId) {
// TODO: Attach an invisible element for this instead // TODO: Attach an invisible element for this instead
// which listens? // which listens?
var audio = document.getElementById(audioId); const audio = document.getElementById(audioId);
if (audio) { if (audio) {
if (audioPromises[audioId]) { if (audioPromises[audioId]) {
audioPromises[audioId] = audioPromises[audioId].then(()=>{ audioPromises[audioId] = audioPromises[audioId].then(()=>{
audio.load(); audio.load();
return audio.play(); return audio.play();
}); });
} } else {
else {
audioPromises[audioId] = audio.play(); audioPromises[audioId] = audio.play();
} }
} }
@ -88,12 +87,11 @@ function play(audioId) {
function pause(audioId) { function pause(audioId) {
// TODO: Attach an invisible element for this instead // TODO: Attach an invisible element for this instead
// which listens? // which listens?
var audio = document.getElementById(audioId); const audio = document.getElementById(audioId);
if (audio) { if (audio) {
if (audioPromises[audioId]) { if (audioPromises[audioId]) {
audioPromises[audioId] = audioPromises[audioId].then(()=>audio.pause()); 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(); // pause doesn't actually return a promise, but might as well do this for symmetry with play();
audioPromises[audioId] = audio.pause(); audioPromises[audioId] = audio.pause();
} }
@ -125,38 +123,32 @@ function _setCallListeners(call) {
if (newState === "ringing") { if (newState === "ringing") {
_setCallState(call, call.roomId, "ringing"); _setCallState(call, call.roomId, "ringing");
pause("ringbackAudio"); pause("ringbackAudio");
} } else if (newState === "invite_sent") {
else if (newState === "invite_sent") {
_setCallState(call, call.roomId, "ringback"); _setCallState(call, call.roomId, "ringback");
play("ringbackAudio"); play("ringbackAudio");
} } else if (newState === "ended" && oldState === "connected") {
else if (newState === "ended" && oldState === "connected") {
_setCallState(undefined, call.roomId, "ended"); _setCallState(undefined, call.roomId, "ended");
pause("ringbackAudio"); pause("ringbackAudio");
play("callendAudio"); play("callendAudio");
} } else if (newState === "ended" && oldState === "invite_sent" &&
else if (newState === "ended" && oldState === "invite_sent" &&
(call.hangupParty === "remote" || (call.hangupParty === "remote" ||
(call.hangupParty === "local" && call.hangupReason === "invite_timeout") (call.hangupParty === "local" && call.hangupReason === "invite_timeout")
)) { )) {
_setCallState(call, call.roomId, "busy"); _setCallState(call, call.roomId, "busy");
pause("ringbackAudio"); pause("ringbackAudio");
play("busyAudio"); play("busyAudio");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, { Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
title: _t('Call Timeout'), title: _t('Call Timeout'),
description: _t('The remote side failed to pick up') + '.', 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"); _setCallState(call, call.roomId, "stop_ringback");
pause("ringbackAudio"); pause("ringbackAudio");
} } else if (oldState === "ringing") {
else if (oldState === "ringing") {
_setCallState(call, call.roomId, "stop_ringing"); _setCallState(call, call.roomId, "stop_ringing");
pause("ringbackAudio"); pause("ringbackAudio");
} } else if (newState === "connected") {
else if (newState === "connected") {
_setCallState(call, call.roomId, "connected"); _setCallState(call, call.roomId, "connected");
pause("ringbackAudio"); pause("ringbackAudio");
} }
@ -165,14 +157,13 @@ function _setCallListeners(call) {
function _setCallState(call, roomId, status) { function _setCallState(call, roomId, status) {
console.log( 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; calls[roomId] = call;
if (status === "ringing") { if (status === "ringing") {
play("ringAudio"); play("ringAudio");
} } else if (call && call.call_state === "ringing") {
else if (call && call.call_state === "ringing") {
pause("ringAudio"); pause("ringAudio");
} }
@ -192,14 +183,12 @@ function _onAction(payload) {
_setCallState(newCall, newCall.roomId, "ringback"); _setCallState(newCall, newCall.roomId, "ringback");
if (payload.type === 'voice') { if (payload.type === 'voice') {
newCall.placeVoiceCall(); newCall.placeVoiceCall();
} } else if (payload.type === 'video') {
else if (payload.type === 'video') {
newCall.placeVideoCall( newCall.placeVideoCall(
payload.remote_element, payload.remote_element,
payload.local_element payload.local_element,
); );
} } else if (payload.type === 'screensharing') {
else if (payload.type === 'screensharing') {
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString(); const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
if (screenCapErrorString) { if (screenCapErrorString) {
_setCallState(undefined, newCall.roomId, "ended"); _setCallState(undefined, newCall.roomId, "ended");
@ -213,10 +202,9 @@ function _onAction(payload) {
} }
newCall.placeScreenSharingCall( newCall.placeScreenSharingCall(
payload.remote_element, payload.remote_element,
payload.local_element payload.local_element,
); );
} } else {
else {
console.error("Unknown conf call type: %s", payload.type); console.error("Unknown conf call type: %s", payload.type);
} }
} }
@ -255,21 +243,19 @@ function _onAction(payload) {
description: _t('You cannot place a call with yourself.'), description: _t('You cannot place a call with yourself.'),
}); });
return; return;
} } else if (members.length === 2) {
else if (members.length === 2) {
console.log("Place %s call in %s", payload.type, payload.room_id); console.log("Place %s call in %s", payload.type, payload.room_id);
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, { const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false), forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false),
}); });
placeCall(call); placeCall(call);
} } else { // > 2
else { // > 2
dis.dispatch({ dis.dispatch({
action: "place_conference_call", action: "place_conference_call",
room_id: payload.room_id, room_id: payload.room_id,
type: payload.type, type: payload.type,
remote_element: payload.remote_element, remote_element: payload.remote_element,
local_element: payload.local_element local_element: payload.local_element,
}); });
} }
break; break;
@ -280,15 +266,13 @@ function _onAction(payload) {
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, { Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
description: _t('Conference calls are not supported in this client'), 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"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, { Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
title: _t('VoIP is unsupported'), title: _t('VoIP is unsupported'),
description: _t('You cannot place VoIP calls in this browser.'), 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 // Conference calls are implemented by sending the media to central
// server which combines the audio from all the participants together // server which combines the audio from all the participants together
// into a single stream. This is incompatible with end-to-end encryption // into a single stream. This is incompatible with end-to-end encryption
@ -299,16 +283,15 @@ function _onAction(payload) {
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, { Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
description: _t('Conference calls are not supported in encrypted rooms'), description: _t('Conference calls are not supported in encrypted rooms'),
}); });
} } else {
else { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, { Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
title: _t('Warning!'), title: _t('Warning!'),
description: _t('Conference calling is in development and may not be reliable.'), description: _t('Conference calling is in development and may not be reliable.'),
onFinished: confirm=>{ onFinished: (confirm)=>{
if (confirm) { if (confirm) {
ConferenceHandler.createNewMatrixCall( ConferenceHandler.createNewMatrixCall(
MatrixClientPeg.get(), payload.room_id MatrixClientPeg.get(), payload.room_id,
).done(function(call) { ).done(function(call) {
placeCall(call); placeCall(call);
}, function(err) { }, function(err) {
@ -357,7 +340,7 @@ function _onAction(payload) {
_setCallState(calls[payload.room_id], payload.room_id, "connected"); _setCallState(calls[payload.room_id], payload.room_id, "connected");
dis.dispatch({ dis.dispatch({
action: "view_room", action: "view_room",
room_id: payload.room_id room_id: payload.room_id,
}); });
break; break;
} }
@ -368,9 +351,9 @@ if (!global.mxCallHandler) {
dis.register(_onAction); dis.register(_onAction);
} }
var callHandler = { const callHandler = {
getCallForRoom: function(roomId) { getCallForRoom: function(roomId) {
var call = module.exports.getCall(roomId); let call = module.exports.getCall(roomId);
if (call) return call; if (call) return call;
if (ConferenceHandler) { if (ConferenceHandler) {
@ -386,8 +369,8 @@ var callHandler = {
}, },
getAnyActiveCall: function() { getAnyActiveCall: function() {
var roomsWithCalls = Object.keys(calls); const roomsWithCalls = Object.keys(calls);
for (var i = 0; i < roomsWithCalls.length; i++) { for (let i = 0; i < roomsWithCalls.length; i++) {
if (calls[roomsWithCalls[i]] && if (calls[roomsWithCalls[i]] &&
calls[roomsWithCalls[i]].call_state !== "ended") { calls[roomsWithCalls[i]].call_state !== "ended") {
return calls[roomsWithCalls[i]]; return calls[roomsWithCalls[i]];
@ -402,7 +385,7 @@ var callHandler = {
getConferenceHandler: function() { getConferenceHandler: function() {
return ConferenceHandler; return ConferenceHandler;
} },
}; };
// Only things in here which actually need to be global are the // Only things in here which actually need to be global are the
// calls list (done separately) and making sure we only register // calls list (done separately) and making sure we only register

View file

@ -17,14 +17,14 @@ limitations under the License.
'use strict'; 'use strict';
import Promise from 'bluebird'; import Promise from 'bluebird';
var extend = require('./extend'); const extend = require('./extend');
var dis = require('./dispatcher'); const dis = require('./dispatcher');
var MatrixClientPeg = require('./MatrixClientPeg'); const MatrixClientPeg = require('./MatrixClientPeg');
var sdk = require('./index'); const sdk = require('./index');
import { _t } from './languageHandler'; 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 // Polyfill for Canvas.toBlob API using Canvas.toDataURL
require("blueimp-canvas-to-blob"); require("blueimp-canvas-to-blob");
@ -54,8 +54,8 @@ const MAX_HEIGHT = 600;
function createThumbnail(element, inputWidth, inputHeight, mimeType) { function createThumbnail(element, inputWidth, inputHeight, mimeType) {
const deferred = Promise.defer(); const deferred = Promise.defer();
var targetWidth = inputWidth; let targetWidth = inputWidth;
var targetHeight = inputHeight; let targetHeight = inputHeight;
if (targetHeight > MAX_HEIGHT) { if (targetHeight > MAX_HEIGHT) {
targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
targetHeight = MAX_HEIGHT; targetHeight = MAX_HEIGHT;
@ -81,7 +81,7 @@ function createThumbnail(element, inputWidth, inputHeight, mimeType) {
w: inputWidth, w: inputWidth,
h: inputHeight, h: inputHeight,
}, },
thumbnail: thumbnail thumbnail: thumbnail,
}); });
}, mimeType); }, mimeType);
@ -129,12 +129,12 @@ function loadImageElement(imageFile) {
* @return {Promise} A promise that resolves with the attachment info. * @return {Promise} A promise that resolves with the attachment info.
*/ */
function infoForImageFile(matrixClient, roomId, imageFile) { function infoForImageFile(matrixClient, roomId, imageFile) {
var thumbnailType = "image/png"; let thumbnailType = "image/png";
if (imageFile.type == "image/jpeg") { if (imageFile.type == "image/jpeg") {
thumbnailType = "image/jpeg"; thumbnailType = "image/jpeg";
} }
var imageInfo; let imageInfo;
return loadImageElement(imageFile).then(function(img) { return loadImageElement(imageFile).then(function(img) {
return createThumbnail(img, img.width, img.height, thumbnailType); return createThumbnail(img, img.width, img.height, thumbnailType);
}).then(function(result) { }).then(function(result) {
@ -191,7 +191,7 @@ function loadVideoElement(videoFile) {
function infoForVideoFile(matrixClient, roomId, videoFile) { function infoForVideoFile(matrixClient, roomId, videoFile) {
const thumbnailType = "image/jpeg"; const thumbnailType = "image/jpeg";
var videoInfo; let videoInfo;
return loadVideoElement(videoFile).then(function(video) { return loadVideoElement(videoFile).then(function(video) {
return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType); return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
}).then(function(result) { }).then(function(result) {
@ -286,7 +286,7 @@ class ContentMessages {
body: file.name || 'Attachment', body: file.name || 'Attachment',
info: { info: {
size: file.size, size: file.size,
} },
}; };
// if we have a mime type for the file, add it to the message metadata // if we have a mime type for the file, add it to the message metadata
@ -297,10 +297,10 @@ class ContentMessages {
const def = Promise.defer(); const def = Promise.defer();
if (file.type.indexOf('image/') == 0) { if (file.type.indexOf('image/') == 0) {
content.msgtype = 'm.image'; content.msgtype = 'm.image';
infoForImageFile(matrixClient, roomId, file).then(imageInfo=>{ infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{
extend(content.info, imageInfo); extend(content.info, imageInfo);
def.resolve(); def.resolve();
}, error=>{ }, (error)=>{
console.error(error); console.error(error);
content.msgtype = 'm.file'; content.msgtype = 'm.file';
def.resolve(); def.resolve();
@ -310,10 +310,10 @@ class ContentMessages {
def.resolve(); def.resolve();
} else if (file.type.indexOf('video/') == 0) { } else if (file.type.indexOf('video/') == 0) {
content.msgtype = 'm.video'; content.msgtype = 'm.video';
infoForVideoFile(matrixClient, roomId, file).then(videoInfo=>{ infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
extend(content.info, videoInfo); extend(content.info, videoInfo);
def.resolve(); def.resolve();
}, error=>{ }, (error)=>{
content.msgtype = 'm.file'; content.msgtype = 'm.file';
def.resolve(); def.resolve();
}); });
@ -331,7 +331,7 @@ class ContentMessages {
this.inprogress.push(upload); this.inprogress.push(upload);
dis.dispatch({action: 'upload_started'}); dis.dispatch({action: 'upload_started'});
var error; let error;
function onProgress(ev) { function onProgress(ev) {
upload.total = ev.total; upload.total = ev.total;
@ -355,11 +355,11 @@ class ContentMessages {
}, function(err) { }, function(err) {
error = err; error = err;
if (!upload.canceled) { 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) { if (err.http_status == 413) {
desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName}); 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, { Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
title: _t('Upload Failed'), title: _t('Upload Failed'),
description: desc, description: desc,
@ -367,8 +367,8 @@ class ContentMessages {
} }
}).finally(() => { }).finally(() => {
const inprogressKeys = Object.keys(this.inprogress); const inprogressKeys = Object.keys(this.inprogress);
for (var i = 0; i < this.inprogress.length; ++i) { for (let i = 0; i < this.inprogress.length; ++i) {
var k = inprogressKeys[i]; const k = inprogressKeys[i];
if (this.inprogress[k].promise === upload.promise) { if (this.inprogress[k].promise === upload.promise) {
this.inprogress.splice(k, 1); this.inprogress.splice(k, 1);
break; break;
@ -376,8 +376,7 @@ class ContentMessages {
} }
if (error) { if (error) {
dis.dispatch({action: 'upload_failed', upload: upload}); dis.dispatch({action: 'upload_failed', upload: upload});
} } else {
else {
dis.dispatch({action: 'upload_finished', upload: upload}); dis.dispatch({action: 'upload_finished', upload: upload});
} }
}); });
@ -389,9 +388,9 @@ class ContentMessages {
cancelUpload(promise) { cancelUpload(promise) {
const inprogressKeys = Object.keys(this.inprogress); const inprogressKeys = Object.keys(this.inprogress);
var upload; let upload;
for (var i = 0; i < this.inprogress.length; ++i) { for (let i = 0; i < this.inprogress.length; ++i) {
var k = inprogressKeys[i]; const k = inprogressKeys[i];
if (this.inprogress[k].promise === promise) { if (this.inprogress[k].promise === promise) {
upload = this.inprogress[k]; upload = this.inprogress[k];
break; break;

View file

@ -19,6 +19,7 @@ import sdk from './';
import MultiInviter from './utils/MultiInviter'; import MultiInviter from './utils/MultiInviter';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import GroupStoreCache from './stores/GroupStoreCache';
export function showGroupInviteDialog(groupId) { export function showGroupInviteDialog(groupId) {
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
@ -75,6 +76,13 @@ function _onGroupInviteFinished(groupId, addrs) {
title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}), title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}),
description: errorList.join(", "), description: errorList.join(", "),
}); });
} else {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Group invitations sent', '', QuestionDialog, {
title: _t("Invites sent"),
description: _t("Your group invitations have been sent."),
hasCancelButton: false,
});
} }
}).catch((err) => { }).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -86,10 +94,11 @@ function _onGroupInviteFinished(groupId, addrs) {
} }
function _onGroupAddRoomFinished(groupId, addrs) { function _onGroupAddRoomFinished(groupId, addrs) {
const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
const errorList = []; const errorList = [];
return Promise.all(addrs.map((addr) => { return Promise.all(addrs.map((addr) => {
return MatrixClientPeg.get() return groupStore
.addRoomToGroup(groupId, addr.address) .addRoomToGroup(addr.address)
.catch(() => { errorList.push(addr.address); }) .catch(() => { errorList.push(addr.address); })
.reflect(); .reflect();
})).then(() => { })).then(() => {

View file

@ -17,10 +17,10 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
var sanitizeHtml = require('sanitize-html'); const sanitizeHtml = require('sanitize-html');
var highlight = require('highlight.js'); const highlight = require('highlight.js');
var linkifyMatrix = require('./linkify-matrix'); const linkifyMatrix = require('./linkify-matrix');
import escape from 'lodash/escape'; import escape from 'lodash/escape';
import emojione from 'emojione'; import emojione from 'emojione';
import classNames from 'classnames'; import classNames from 'classnames';
@ -66,8 +66,7 @@ function unicodeToImage(str) {
if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) { if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
// if the unicodeChar doesnt exist just return the entire match // if the unicodeChar doesnt exist just return the entire match
return unicodeChar; return unicodeChar;
} } else {
else {
// get the unicode codepoint from the actual char // get the unicode codepoint from the actual char
unicode = emojione.jsEscapeMap[unicodeChar]; unicode = emojione.jsEscapeMap[unicodeChar];
@ -183,21 +182,19 @@ const sanitizeHtmlParams = {
if (attribs.href) { if (attribs.href) {
attribs.target = '_blank'; // by default attribs.target = '_blank'; // by default
var m; let m;
// FIXME: horrible duplication with linkify-matrix // FIXME: horrible duplication with linkify-matrix
m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN); m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
if (m) { if (m) {
attribs.href = m[1]; attribs.href = m[1];
delete attribs.target; delete attribs.target;
} } else {
else {
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
if (m) { if (m) {
var entity = m[1]; const entity = m[1];
if (entity[0] === '@') { if (entity[0] === '@') {
attribs.href = '#/user/' + entity; attribs.href = '#/user/' + entity;
} } else if (entity[0] === '#' || entity[0] === '!') {
else if (entity[0] === '#' || entity[0] === '!') {
attribs.href = '#/room/' + entity; attribs.href = '#/room/' + entity;
} }
delete attribs.target; delete attribs.target;
@ -205,7 +202,7 @@ const sanitizeHtmlParams = {
} }
} }
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
return { tagName: tagName, attribs : attribs }; return { tagName: tagName, attribs: attribs };
}, },
'img': function(tagName, attribs) { 'img': function(tagName, attribs) {
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
@ -224,7 +221,7 @@ const sanitizeHtmlParams = {
'code': function(tagName, attribs) { 'code': function(tagName, attribs) {
if (typeof attribs.class !== 'undefined') { if (typeof attribs.class !== 'undefined') {
// Filter out all classes other than ones starting with language- for syntax highlighting. // 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-'); return cl.startsWith('language-');
}); });
attribs.class = classes.join(' '); attribs.class = classes.join(' ');
@ -287,11 +284,11 @@ class BaseHighlighter {
* TextHighlighter). * TextHighlighter).
*/ */
applyHighlights(safeSnippet, safeHighlights) { applyHighlights(safeSnippet, safeHighlights) {
var lastOffset = 0; let lastOffset = 0;
var offset; let offset;
var nodes = []; let nodes = [];
var safeHighlight = safeHighlights[0]; const safeHighlight = safeHighlights[0];
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) { while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
// handle preamble // handle preamble
if (offset > lastOffset) { if (offset > lastOffset) {
@ -301,7 +298,7 @@ class BaseHighlighter {
// do highlight. use the original string rather than safeHighlight // do highlight. use the original string rather than safeHighlight
// to preserve the original casing. // to preserve the original casing.
var endOffset = offset + safeHighlight.length; const endOffset = offset + safeHighlight.length;
nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true)); nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true));
lastOffset = endOffset; lastOffset = endOffset;
@ -319,8 +316,7 @@ class BaseHighlighter {
if (safeHighlights[1]) { if (safeHighlights[1]) {
// recurse into this range to check for the next set of highlight matches // recurse into this range to check for the next set of highlight matches
return this.applyHighlights(safeSnippet, safeHighlights.slice(1)); return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
} } else {
else {
// no more highlights to be found, just return the unhighlighted string // no more highlights to be found, just return the unhighlighted string
return [this._processSnippet(safeSnippet, false)]; return [this._processSnippet(safeSnippet, false)];
} }
@ -341,7 +337,7 @@ class HtmlHighlighter extends BaseHighlighter {
return snippet; return snippet;
} }
var span = "<span class=\""+this.highlightClass+"\">" let span = "<span class=\""+this.highlightClass+"\">"
+ snippet + "</span>"; + snippet + "</span>";
if (this.highlightLink) { if (this.highlightLink) {
@ -366,15 +362,15 @@ class TextHighlighter extends BaseHighlighter {
* returns a React node * returns a React node
*/ */
_processSnippet(snippet, highlight) { _processSnippet(snippet, highlight) {
var key = this._key++; const key = this._key++;
var node = let node =
<span key={key} className={highlight ? this.highlightClass : null }> <span key={key} className={highlight ? this.highlightClass : null}>
{ snippet } { snippet }
</span>; </span>;
if (highlight && this.highlightLink) { if (highlight && this.highlightLink) {
node = <a key={key} href={this.highlightLink}>{node}</a>; node = <a key={key} href={this.highlightLink}>{ node }</a>;
} }
return node; return node;
@ -389,24 +385,23 @@ class TextHighlighter extends BaseHighlighter {
* highlights: optional list of words to highlight, ordered by longest word first * highlights: optional list of words to highlight, ordered by longest word first
* *
* opts.highlightLink: optional href to add to highlighted words * 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) { export function bodyToHtml(content, highlights, opts={}) {
opts = 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; let bodyHasEmoji = false;
var safeBody; let safeBody;
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying // 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 // 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 // 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 // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
try { try {
if (highlights && highlights.length > 0) { if (highlights && highlights.length > 0) {
var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink); const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
var safeHighlights = highlights.map(function(highlight) { const safeHighlights = highlights.map(function(highlight) {
return sanitizeHtml(highlight, sanitizeHtmlParams); return sanitizeHtml(highlight, sanitizeHtmlParams);
}); });
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure. // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
@ -417,16 +412,15 @@ export function bodyToHtml(content, highlights, opts) {
safeBody = sanitizeHtml(body, sanitizeHtmlParams); safeBody = sanitizeHtml(body, sanitizeHtmlParams);
bodyHasEmoji = containsEmoji(body); bodyHasEmoji = containsEmoji(body);
if (bodyHasEmoji) safeBody = unicodeToImage(safeBody); if (bodyHasEmoji) safeBody = unicodeToImage(safeBody);
} } finally {
finally {
delete sanitizeHtmlParams.textFilter; delete sanitizeHtmlParams.textFilter;
} }
let emojiBody = false; let emojiBody = false;
if (bodyHasEmoji) { if (!opts.disableBigEmoji && bodyHasEmoji) {
EMOJI_REGEX.lastIndex = 0; EMOJI_REGEX.lastIndex = 0;
let contentBodyTrimmed = content.body !== undefined ? content.body.trim() : ''; const contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
let match = EMOJI_REGEX.exec(contentBodyTrimmed); const match = EMOJI_REGEX.exec(contentBodyTrimmed);
emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length; emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length;
} }

View file

@ -42,13 +42,12 @@ module.exports = {
// no scaling needs to be applied // no scaling needs to be applied
return fullHeight; return fullHeight;
} }
var widthMulti = thumbWidth / fullWidth; const widthMulti = thumbWidth / fullWidth;
var heightMulti = thumbHeight / fullHeight; const heightMulti = thumbHeight / fullHeight;
if (widthMulti < heightMulti) { if (widthMulti < heightMulti) {
// width is the dominant dimension so scaling will be fixed on that // width is the dominant dimension so scaling will be fixed on that
return Math.floor(widthMulti * fullHeight); return Math.floor(widthMulti * fullHeight);
} } else {
else {
// height is the dominant dimension so scaling will be fixed on that // height is the dominant dimension so scaling will be fixed on that
return Math.floor(heightMulti * fullHeight); return Math.floor(heightMulti * fullHeight);
} }

View file

@ -59,8 +59,8 @@ export default class Login {
} }
getFlows() { getFlows() {
var self = this; const self = this;
var client = this._createTemporaryClient(); const client = this._createTemporaryClient();
return client.loginFlows().then(function(result) { return client.loginFlows().then(function(result) {
self._flows = result.flows; self._flows = result.flows;
self._currentFlowIndex = 0; self._currentFlowIndex = 0;
@ -77,12 +77,12 @@ export default class Login {
getCurrentFlowStep() { getCurrentFlowStep() {
// technically the flow can have multiple steps, but no one does this // technically the flow can have multiple steps, but no one does this
// for login so we can ignore it. // for login so we can ignore it.
var flowStep = this._flows[this._currentFlowIndex]; const flowStep = this._flows[this._currentFlowIndex];
return flowStep ? flowStep.type : null; return flowStep ? flowStep.type : null;
} }
loginAsGuest() { loginAsGuest() {
var client = this._createTemporaryClient(); const client = this._createTemporaryClient();
return client.registerGuest({ return client.registerGuest({
body: { body: {
initial_device_display_name: this._defaultDeviceDisplayName, initial_device_display_name: this._defaultDeviceDisplayName,
@ -94,7 +94,7 @@ export default class Login {
accessToken: creds.access_token, accessToken: creds.access_token,
homeserverUrl: this._hsUrl, homeserverUrl: this._hsUrl,
identityServerUrl: this._isUrl, identityServerUrl: this._isUrl,
guest: true guest: true,
}; };
}, (error) => { }, (error) => {
throw error; throw error;
@ -149,12 +149,12 @@ export default class Login {
identityServerUrl: self._isUrl, identityServerUrl: self._isUrl,
userId: data.user_id, userId: data.user_id,
deviceId: data.device_id, deviceId: data.device_id,
accessToken: data.access_token accessToken: data.access_token,
}); });
}, function(error) { }, function(error) {
if (error.httpStatus === 403) { if (error.httpStatus === 403) {
if (self._fallbackHsUrl) { if (self._fallbackHsUrl) {
var fbClient = Matrix.createClient({ const fbClient = Matrix.createClient({
baseUrl: self._fallbackHsUrl, baseUrl: self._fallbackHsUrl,
idBaseUrl: this._isUrl, idBaseUrl: this._isUrl,
}); });
@ -165,7 +165,7 @@ export default class Login {
identityServerUrl: self._isUrl, identityServerUrl: self._isUrl,
userId: data.user_id, userId: data.user_id,
deviceId: data.device_id, deviceId: data.device_id,
accessToken: data.access_token accessToken: data.access_token,
}); });
}, function(fallback_error) { }, function(fallback_error) {
// throw the original error // throw the original error

View file

@ -48,7 +48,7 @@ function html_if_tag_allowed(node) {
* or false if it is only a single line. * or false if it is only a single line.
*/ */
function is_multi_line(node) { function is_multi_line(node) {
var par = node; let par = node;
while (par.parent) { while (par.parent) {
par = par.parent; par = par.parent;
} }
@ -143,7 +143,7 @@ export default class Markdown {
if (isMultiLine) this.cr(); if (isMultiLine) this.cr();
html_if_tag_allowed.call(this, node); html_if_tag_allowed.call(this, node);
if (isMultiLine) this.cr(); if (isMultiLine) this.cr();
} };
return renderer.render(this.parsed); return renderer.render(this.parsed);
} }
@ -178,7 +178,7 @@ export default class Markdown {
renderer.html_block = function(node) { renderer.html_block = function(node) {
this.lit(node.literal); this.lit(node.literal);
if (is_multi_line(node) && node.next) this.lit('\n\n'); if (is_multi_line(node) && node.next) this.lit('\n\n');
} };
return renderer.render(this.parsed); return renderer.render(this.parsed);
} }

View file

@ -95,7 +95,7 @@ class MatrixClientPeg {
opts.pendingEventOrdering = "detached"; opts.pendingEventOrdering = "detached";
try { try {
let promise = this.matrixClient.store.startup(); const promise = this.matrixClient.store.startup();
console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`); console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`);
await promise; await promise;
} catch(err) { } catch(err) {
@ -136,7 +136,7 @@ class MatrixClientPeg {
} }
_createClient(creds: MatrixClientCreds) { _createClient(creds: MatrixClientCreds) {
var opts = { const opts = {
baseUrl: creds.homeserverUrl, baseUrl: creds.homeserverUrl,
idBaseUrl: creds.identityServerUrl, idBaseUrl: creds.identityServerUrl,
accessToken: creds.accessToken, accessToken: creds.accessToken,
@ -153,8 +153,8 @@ class MatrixClientPeg {
this.matrixClient.setGuest(Boolean(creds.guest)); this.matrixClient.setGuest(Boolean(creds.guest));
var notifTimelineSet = new EventTimelineSet(null, { const notifTimelineSet = new EventTimelineSet(null, {
timelineSupport: true timelineSupport: true,
}); });
// XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync. // XXX: what is our initial pagination token?! it somehow needs to be synchronised with /sync.
notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS); notifTimelineSet.getLiveTimeline().setPaginationToken("", EventTimeline.BACKWARDS);

View file

@ -17,8 +17,8 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
var ReactDOM = require('react-dom'); const ReactDOM = require('react-dom');
import Analytics from './Analytics'; import Analytics from './Analytics';
import sdk from './index'; import sdk from './index';
@ -137,15 +137,15 @@ class ModalManager {
* @param {String} className CSS class to apply to the modal wrapper * @param {String} className CSS class to apply to the modal wrapper
*/ */
createDialogAsync(loader, props, className) { createDialogAsync(loader, props, className) {
var self = this; const self = this;
const modal = {}; const modal = {};
// never call this from onFinished() otherwise it will loop // never call this from onFinished() otherwise it will loop
// //
// nb explicit function() rather than arrow function, to get `arguments` // 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); if (props && props.onFinished) props.onFinished.apply(null, arguments);
var i = self._modals.indexOf(modal); const i = self._modals.indexOf(modal);
if (i >= 0) { if (i >= 0) {
self._modals.splice(i, 1); 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! // property set here so you can't close the dialog from a button click!
modal.elem = ( modal.elem = (
<AsyncWrapper key={modalCount} loader={loader} {...props} <AsyncWrapper key={modalCount} loader={loader} {...props}
onFinished={closeDialog}/> onFinished={closeDialog} />
); );
modal.onFinished = props ? props.onFinished : null; modal.onFinished = props ? props.onFinished : null;
modal.className = className; modal.className = className;
@ -191,13 +191,13 @@ class ModalManager {
return; return;
} }
var modal = this._modals[0]; const modal = this._modals[0];
var dialog = ( const dialog = (
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '') }> <div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
<div className="mx_Dialog"> <div className="mx_Dialog">
{modal.elem} { modal.elem }
</div> </div>
<div className="mx_Dialog_background" onClick={ this.closeAll }></div> <div className="mx_Dialog_background" onClick={this.closeAll}></div>
</div> </div>
); );

View file

@ -81,7 +81,7 @@ const Notifier = {
} }
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember( const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
ev.sender, 40, 40, 'crop' ev.sender, 40, 40, 'crop',
) : null; ) : null;
const notif = plaf.displayNotification(title, msg, avatarUrl, room); const notif = plaf.displayNotification(title, msg, avatarUrl, room);
@ -303,7 +303,7 @@ const Notifier = {
this._playAudioNotification(ev, room); this._playAudioNotification(ev, room);
} }
} }
} },
}; };
if (!global.mxNotifier) { if (!global.mxNotifier) {

View file

@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var MatrixClientPeg = require("./MatrixClientPeg"); const MatrixClientPeg = require("./MatrixClientPeg");
var dis = require("./dispatcher"); const dis = require("./dispatcher");
// Time in ms after that a user is considered as unavailable/away // Time in ms after that a user is considered as unavailable/away
var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
var PRESENCE_STATES = ["online", "offline", "unavailable"]; const PRESENCE_STATES = ["online", "offline", "unavailable"];
class Presence { class Presence {
@ -71,14 +71,14 @@ class Presence {
if (!this.running) { if (!this.running) {
return; return;
} }
var old_state = this.state; const old_state = this.state;
this.state = newState; this.state = newState;
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
return; // don't try to set presence when a guest; it won't work. return; // don't try to set presence when a guest; it won't work.
} }
var self = this; const self = this;
MatrixClientPeg.get().setPresence(this.state).done(function() { MatrixClientPeg.get().setPresence(this.state).done(function() {
console.log("Presence: %s", newState); console.log("Presence: %s", newState);
}, function(err) { }, function(err) {
@ -104,7 +104,7 @@ class Presence {
* @private * @private
*/ */
_resetTimer() { _resetTimer() {
var self = this; const self = this;
this.setState("online"); this.setState("online");
// Re-arm the timer // Re-arm the timer
clearTimeout(this.timer); clearTimeout(this.timer);

View file

@ -44,9 +44,9 @@ export const contentStateToHTML = (contentState: ContentState) => {
return stateToHTML(contentState, { return stateToHTML(contentState, {
inlineStyles: { inlineStyles: {
UNDERLINE: { UNDERLINE: {
element: 'u' element: 'u',
} },
} },
}); });
}; };
@ -59,7 +59,7 @@ function unicodeToEmojiUri(str) {
let replaceWith, unicode, alt; let replaceWith, unicode, alt;
if ((!emojione.unicodeAlt) || (emojione.sprites)) { 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 // 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) { str = str.replace(emojione.regUnicode, function(unicodeChar) {
@ -67,8 +67,14 @@ function unicodeToEmojiUri(str) {
// if the unicodeChar doesnt exist just return the entire match // if the unicodeChar doesnt exist just return the entire match
return unicodeChar; return unicodeChar;
} else { } 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 // get the unicode codepoint from the actual char
unicode = emojione.jsEscapeMap[unicodeChar]; unicode = emojione.jsEscapeMap[unicodeChar];
return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam; 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 // Workaround for https://github.com/facebook/draft-js/issues/414
let emojiDecorator = { const emojiDecorator = {
strategy: (contentState, contentBlock, callback) => { strategy: (contentState, contentBlock, callback) => {
findWithRegex(EMOJI_REGEX, contentBlock, callback); findWithRegex(EMOJI_REGEX, contentBlock, callback);
}, },
component: (props) => { component: (props) => {
let uri = unicodeToEmojiUri(props.children[0].props.text); const uri = unicodeToEmojiUri(props.children[0].props.text);
let shortname = emojione.toShort(props.children[0].props.text); const shortname = emojione.toShort(props.children[0].props.text);
let style = { const style = {
display: 'inline-block', display: 'inline-block',
width: '1em', width: '1em',
maxHeight: '1em', maxHeight: '1em',
@ -106,7 +112,7 @@ let emojiDecorator = {
backgroundPosition: 'center center', backgroundPosition: 'center center',
overflow: 'hidden', 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 { export function getScopedMDDecorators(scope: any): CompositeDecorator {
let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map( const markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
(style) => ({ (style) => ({
strategy: (contentState, contentBlock, callback) => { strategy: (contentState, contentBlock, callback) => {
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback); return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
}, },
component: (props) => ( component: (props) => (
<span className={"mx_MarkdownElement mx_Markdown_" + style}> <span className={"mx_MarkdownElement mx_Markdown_" + style}>
{props.children} { props.children }
</span> </span>
) ),
})); }));
markdownDecorators.push({ markdownDecorators.push({
@ -136,9 +142,9 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
}, },
component: (props) => ( component: (props) => (
<a href="#" className="mx_MarkdownElement mx_Markdown_LINK"> <a href="#" className="mx_MarkdownElement mx_Markdown_LINK">
{props.children} { props.children }
</a> </a>
) ),
}); });
// markdownDecorators.push(emojiDecorator); // markdownDecorators.push(emojiDecorator);
// TODO Consider renabling "syntax highlighting" when we can do it properly // 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; for (let currentKey = startKey;
currentKey && currentKey !== endKey; currentKey && currentKey !== endKey;
currentKey = contentState.getKeyAfter(currentKey)) { currentKey = contentState.getKeyAfter(currentKey)) {
let blockText = getText(currentKey); const blockText = getText(currentKey);
text += blockText.substring(startOffset, blockText.length); text += blockText.substring(startOffset, blockText.length);
// from now on, we'll take whole blocks // from now on, we'll take whole blocks
@ -182,7 +188,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
export function selectionStateToTextOffsets(selectionState: SelectionState, export function selectionStateToTextOffsets(selectionState: SelectionState,
contentBlocks: Array<ContentBlock>): {start: number, end: number} { contentBlocks: Array<ContentBlock>): {start: number, end: number} {
let offset = 0, start = 0, end = 0; let offset = 0, start = 0, end = 0;
for (let block of contentBlocks) { for (const block of contentBlocks) {
if (selectionState.getStartKey() === block.getKey()) { if (selectionState.getStartKey() === block.getKey()) {
start = offset + selectionState.getStartOffset(); start = offset + selectionState.getStartOffset();
} }
@ -259,7 +265,7 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor
.set('focusOffset', end); .set('focusOffset', end);
const emojiText = plainText.substring(start, end); const emojiText = plainText.substring(start, end);
newContentState = newContentState.createEntity( newContentState = newContentState.createEntity(
'emoji', 'IMMUTABLE', { emojiUnicode: emojiText } 'emoji', 'IMMUTABLE', { emojiUnicode: emojiText },
); );
const entityKey = newContentState.getLastCreatedEntityKey(); const entityKey = newContentState.getLastCreatedEntityKey();
newContentState = Modifier.replaceText( newContentState = Modifier.replaceText(

View file

@ -62,8 +62,7 @@ export function isConfCallRoom(room, me, conferenceHandler) {
export function looksLikeDirectMessageRoom(room, me) { export function looksLikeDirectMessageRoom(room, me) {
if (me.membership == "join" || me.membership === "ban" || 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 // Used to split rooms via tags
const tagNames = Object.keys(room.tags); const tagNames = Object.keys(room.tags);
// Used for 1:1 direct chats // Used for 1:1 direct chats

View file

@ -15,10 +15,10 @@ limitations under the License.
*/ */
import Promise from 'bluebird'; import Promise from 'bluebird';
var request = require('browser-request'); const request = require('browser-request');
var SdkConfig = require('./SdkConfig'); const SdkConfig = require('./SdkConfig');
var MatrixClientPeg = require('./MatrixClientPeg'); const MatrixClientPeg = require('./MatrixClientPeg');
class ScalarAuthClient { class ScalarAuthClient {
@ -38,7 +38,7 @@ class ScalarAuthClient {
// Returns a scalar_token string // Returns a scalar_token string
getScalarToken() { getScalarToken() {
var tok = window.localStorage.getItem("mx_scalar_token"); const tok = window.localStorage.getItem("mx_scalar_token");
if (tok) return Promise.resolve(tok); if (tok) return Promise.resolve(tok);
// No saved token, so do the dance to get one. First, we // No saved token, so do the dance to get one. First, we
@ -53,9 +53,9 @@ class ScalarAuthClient {
} }
exchangeForScalarToken(openid_token_object) { 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({ request({
method: 'POST', method: 'POST',
uri: scalar_rest_url+'/register', uri: scalar_rest_url+'/register',
@ -77,7 +77,7 @@ class ScalarAuthClient {
} }
getScalarInterfaceUrlForRoom(roomId, screen, id) { 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 += "?scalar_token=" + encodeURIComponent(this.scalarToken);
url += "&room_id=" + encodeURIComponent(roomId); url += "&room_id=" + encodeURIComponent(roomId);
if (id) { if (id) {

View file

@ -356,12 +356,12 @@ function getWidgets(event, roomId) {
} }
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets"); const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
// Only return widgets which have required fields // Only return widgets which have required fields
let widgetStateEvents = []; const widgetStateEvents = [];
stateEvents.forEach((ev) => { stateEvents.forEach((ev) => {
if (ev.getContent().type && ev.getContent().url) { if (ev.getContent().type && ev.getContent().url) {
widgetStateEvents.push(ev.event); // return the raw event widgetStateEvents.push(ev.event); // return the raw event
} }
}) });
sendResponse(event, widgetStateEvents); sendResponse(event, widgetStateEvents);
} }
@ -376,7 +376,7 @@ function setPlumbingState(event, roomId, status) {
sendError(event, _t('You need to be logged in.')); sendError(event, _t('You need to be logged in.'));
return; return;
} }
client.sendStateEvent(roomId, "m.room.plumbing", { status : status }).done(() => { client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => {
sendResponse(event, { sendResponse(event, {
success: true, success: true,
}); });
@ -415,11 +415,11 @@ function setBotPower(event, roomId, userId, level) {
} }
client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => { client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
let powerEvent = new MatrixEvent( const powerEvent = new MatrixEvent(
{ {
type: "m.room.power_levels", type: "m.room.power_levels",
content: powerLevels, content: powerLevels,
} },
); );
client.setPowerLevel(roomId, userId, level, powerEvent).done(() => { client.setPowerLevel(roomId, userId, level, powerEvent).done(() => {
@ -485,8 +485,7 @@ function canSendEvent(event, roomId) {
let canSend = false; let canSend = false;
if (isState) { if (isState) {
canSend = room.currentState.maySendStateEvent(evType, me); canSend = room.currentState.maySendStateEvent(evType, me);
} } else {
else {
canSend = room.currentState.maySendEvent(evType, me); canSend = room.currentState.maySendEvent(evType, me);
} }
@ -517,8 +516,8 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
sendResponse(event, stateEvent.getContent()); sendResponse(event, stateEvent.getContent());
} }
var currentRoomId = null; let currentRoomId = null;
var currentRoomAlias = null; let currentRoomAlias = null;
// Listen for when a room is viewed // Listen for when a room is viewed
dis.register(onAction); dis.register(onAction);
@ -542,7 +541,7 @@ const onMessage = function(event) {
// //
// All strings start with the empty string, so for sanity return if the length // All strings start with the empty string, so for sanity return if the length
// of the event origin is 0. // of the event origin is 0.
let url = SdkConfig.get().integrations_ui_url; const url = SdkConfig.get().integrations_ui_url;
if (event.origin.length === 0 || !url.startsWith(event.origin) || !event.data.action) { if (event.origin.length === 0 || !url.startsWith(event.origin) || !event.data.action) {
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
} }
@ -647,7 +646,7 @@ module.exports = {
// Make an error so we get a stack trace // Make an error so we get a stack trace
const e = new Error( const e = new Error(
"ScalarMessaging: mismatched startListening / stopListening detected." + "ScalarMessaging: mismatched startListening / stopListening detected." +
" Negative count" " Negative count",
); );
console.error(e); console.error(e);
} }

View file

@ -243,7 +243,7 @@ function textForPowerEvent(event) {
if (to !== from) { if (to !== from) {
diff.push( diff.push(
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId: userId, userId,
fromPowerLevel: Roles.textualPowerLevel(from, userDefault), fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
toPowerLevel: Roles.textualPowerLevel(to, userDefault), toPowerLevel: Roles.textualPowerLevel(to, userDefault),
}), }),
@ -254,7 +254,7 @@ function textForPowerEvent(event) {
return ''; return '';
} }
return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', { return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
senderName: senderName, senderName,
powerLevelDiffText: diff.join(", "), powerLevelDiffText: diff.join(", "),
}); });
} }
@ -296,12 +296,15 @@ function textForWidgetEvent(event) {
const handlers = { const handlers = {
'm.room.message': textForMessageEvent, 'm.room.message': textForMessageEvent,
'm.room.name': textForRoomNameEvent,
'm.room.topic': textForTopicEvent,
'm.room.member': textForMemberEvent,
'm.call.invite': textForCallInviteEvent, 'm.call.invite': textForCallInviteEvent,
'm.call.answer': textForCallAnswerEvent, 'm.call.answer': textForCallAnswerEvent,
'm.call.hangup': textForCallHangupEvent, '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.third_party_invite': textForThreePidInviteEvent,
'm.room.history_visibility': textForHistoryVisibilityEvent, 'm.room.history_visibility': textForHistoryVisibilityEvent,
'm.room.encryption': textForEncryptionEvent, 'm.room.encryption': textForEncryptionEvent,
@ -313,8 +316,8 @@ const handlers = {
module.exports = { module.exports = {
textForEvent: function(ev) { textForEvent: function(ev) {
const hdlr = handlers[ev.getType()]; const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
if (!hdlr) return ''; if (handler) return handler(ev);
return hdlr(ev); return '';
}, },
}; };

View file

@ -18,10 +18,10 @@ limitations under the License.
// module.exports otherwise this will break when included by both // module.exports otherwise this will break when included by both
// react-sdk and apps layered on top. // react-sdk and apps layered on top.
var DEBUG = 0; const DEBUG = 0;
// The colour keys to be replaced as referred to in CSS // The colour keys to be replaced as referred to in CSS
var keyRgb = [ const keyRgb = [
"rgb(118, 207, 166)", // Vector Green "rgb(118, 207, 166)", // Vector Green
"rgb(234, 245, 240)", // Vector Light Green "rgb(234, 245, 240)", // Vector Light Green
"rgb(211, 239, 225)", // BottomLeftMenu overlay (20% Vector Green) "rgb(211, 239, 225)", // BottomLeftMenu overlay (20% Vector Green)
@ -35,7 +35,7 @@ var keyRgb = [
// x = (255 - 234) / (255 - 118) = 0.16 // x = (255 - 234) / (255 - 118) = 0.16
// The colour keys to be replaced as referred to in SVGs // The colour keys to be replaced as referred to in SVGs
var keyHex = [ const keyHex = [
"#76CFA6", // Vector Green "#76CFA6", // Vector Green
"#EAF5F0", // Vector Light Green "#EAF5F0", // Vector Light Green
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green) "#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green)
@ -44,14 +44,14 @@ var keyHex = [
// cache of our replacement colours // cache of our replacement colours
// defaults to our keys. // defaults to our keys.
var colors = [ const colors = [
keyHex[0], keyHex[0],
keyHex[1], keyHex[1],
keyHex[2], keyHex[2],
keyHex[3], keyHex[3],
]; ];
var cssFixups = [ const cssFixups = [
// { // {
// style: a style object that should be fixed up taken from a stylesheet // style: a style object that should be fixed up taken from a stylesheet
// attr: name of the attribute to be clobbered, e.g. 'color' // attr: name of the attribute to be clobbered, e.g. 'color'
@ -60,7 +60,7 @@ var cssFixups = [
]; ];
// CSS attributes to be fixed up // CSS attributes to be fixed up
var cssAttrs = [ const cssAttrs = [
"color", "color",
"backgroundColor", "backgroundColor",
"borderColor", "borderColor",
@ -69,17 +69,17 @@ var cssAttrs = [
"borderLeftColor", "borderLeftColor",
]; ];
var svgAttrs = [ const svgAttrs = [
"fill", "fill",
"stroke", "stroke",
]; ];
var cached = false; let cached = false;
function calcCssFixups() { function calcCssFixups() {
if (DEBUG) console.log("calcSvgFixups start"); if (DEBUG) console.log("calcSvgFixups start");
for (var i = 0; i < document.styleSheets.length; i++) { for (let i = 0; i < document.styleSheets.length; i++) {
var ss = document.styleSheets[i]; const ss = document.styleSheets[i];
if (!ss) continue; // well done safari >:( if (!ss) continue; // well done safari >:(
// Chromium apparently sometimes returns null here; unsure why. // Chromium apparently sometimes returns null here; unsure why.
// see $14534907369972FRXBx:matrix.org in HQ // see $14534907369972FRXBx:matrix.org in HQ
@ -104,12 +104,12 @@ function calcCssFixups() {
if (ss.href && !ss.href.match(/\/bundle.*\.css$/)) continue; if (ss.href && !ss.href.match(/\/bundle.*\.css$/)) continue;
if (!ss.cssRules) continue; if (!ss.cssRules) continue;
for (var j = 0; j < ss.cssRules.length; j++) { for (let j = 0; j < ss.cssRules.length; j++) {
var rule = ss.cssRules[j]; const rule = ss.cssRules[j];
if (!rule.style) continue; if (!rule.style) continue;
for (var k = 0; k < cssAttrs.length; k++) { for (let k = 0; k < cssAttrs.length; k++) {
var attr = cssAttrs[k]; const attr = cssAttrs[k];
for (var l = 0; l < keyRgb.length; l++) { for (let l = 0; l < keyRgb.length; l++) {
if (rule.style[attr] === keyRgb[l]) { if (rule.style[attr] === keyRgb[l]) {
cssFixups.push({ cssFixups.push({
style: rule.style, style: rule.style,
@ -126,8 +126,8 @@ function calcCssFixups() {
function applyCssFixups() { function applyCssFixups() {
if (DEBUG) console.log("applyCssFixups start"); if (DEBUG) console.log("applyCssFixups start");
for (var i = 0; i < cssFixups.length; i++) { for (let i = 0; i < cssFixups.length; i++) {
var cssFixup = cssFixups[i]; const cssFixup = cssFixups[i];
cssFixup.style[cssFixup.attr] = colors[cssFixup.index]; cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
} }
if (DEBUG) console.log("applyCssFixups end"); if (DEBUG) console.log("applyCssFixups end");
@ -140,15 +140,15 @@ function hexToRgb(color) {
color[1] + color[1] + color[1] + color[1] +
color[2] + color[2]; color[2] + color[2];
} }
var val = parseInt(color, 16); const val = parseInt(color, 16);
var r = (val >> 16) & 255; const r = (val >> 16) & 255;
var g = (val >> 8) & 255; const g = (val >> 8) & 255;
var b = val & 255; const b = val & 255;
return [r, g, b]; return [r, g, b];
} }
function rgbToHex(rgb) { function rgbToHex(rgb) {
var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
return '#' + (0x1000000 + val).toString(16).slice(1); return '#' + (0x1000000 + val).toString(16).slice(1);
} }
@ -167,12 +167,11 @@ module.exports = {
* *
* @param {Function} tintable Function to call when the tint changes. * @param {Function} tintable Function to call when the tint changes.
*/ */
registerTintable : function(tintable) { registerTintable: function(tintable) {
tintables.push(tintable); tintables.push(tintable);
}, },
tint: function(primaryColor, secondaryColor, tertiaryColor) { tint: function(primaryColor, secondaryColor, tertiaryColor) {
if (!cached) { if (!cached) {
calcCssFixups(); calcCssFixups();
cached = true; cached = true;
@ -185,7 +184,7 @@ module.exports = {
if (!secondaryColor) { if (!secondaryColor) {
const x = 0.16; // average weighting factor calculated from vector green & light green const x = 0.16; // average weighting factor calculated from vector green & light green
var rgb = hexToRgb(primaryColor); const rgb = hexToRgb(primaryColor);
rgb[0] = x * rgb[0] + (1 - x) * 255; rgb[0] = x * rgb[0] + (1 - x) * 255;
rgb[1] = x * rgb[1] + (1 - x) * 255; rgb[1] = x * rgb[1] + (1 - x) * 255;
rgb[2] = x * rgb[2] + (1 - x) * 255; rgb[2] = x * rgb[2] + (1 - x) * 255;
@ -194,8 +193,8 @@ module.exports = {
if (!tertiaryColor) { if (!tertiaryColor) {
const x = 0.19; const x = 0.19;
var rgb1 = hexToRgb(primaryColor); const rgb1 = hexToRgb(primaryColor);
var rgb2 = hexToRgb(secondaryColor); const rgb2 = hexToRgb(secondaryColor);
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0]; rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1]; rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2]; rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
@ -204,8 +203,7 @@ module.exports = {
if (colors[0] === primaryColor && if (colors[0] === primaryColor &&
colors[1] === secondaryColor && colors[1] === secondaryColor &&
colors[2] === tertiaryColor) colors[2] === tertiaryColor) {
{
return; return;
} }
@ -248,14 +246,13 @@ module.exports = {
// key colour; cache the element and apply. // key colour; cache the element and apply.
if (DEBUG) console.log("calcSvgFixups start for " + svgs); if (DEBUG) console.log("calcSvgFixups start for " + svgs);
var fixups = []; const fixups = [];
for (var i = 0; i < svgs.length; i++) { for (let i = 0; i < svgs.length; i++) {
var svgDoc; var svgDoc;
try { try {
svgDoc = svgs[i].contentDocument; svgDoc = svgs[i].contentDocument;
} } catch(e) {
catch(e) { let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
var msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
if (e.message) { if (e.message) {
msg += e.message; msg += e.message;
} }
@ -265,12 +262,12 @@ module.exports = {
console.error(e); console.error(e);
} }
if (!svgDoc) continue; if (!svgDoc) continue;
var tags = svgDoc.getElementsByTagName("*"); const tags = svgDoc.getElementsByTagName("*");
for (var j = 0; j < tags.length; j++) { for (let j = 0; j < tags.length; j++) {
var tag = tags[j]; const tag = tags[j];
for (var k = 0; k < svgAttrs.length; k++) { for (let k = 0; k < svgAttrs.length; k++) {
var attr = svgAttrs[k]; const attr = svgAttrs[k];
for (var l = 0; l < keyHex.length; l++) { for (let l = 0; l < keyHex.length; l++) {
if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) { if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) {
fixups.push({ fixups.push({
node: tag, node: tag,
@ -289,10 +286,10 @@ module.exports = {
applySvgFixups: function(fixups) { applySvgFixups: function(fixups) {
if (DEBUG) console.log("applySvgFixups start for " + fixups); if (DEBUG) console.log("applySvgFixups start for " + fixups);
for (var i = 0; i < fixups.length; i++) { for (let i = 0; i < fixups.length; i++) {
var svgFixup = fixups[i]; const svgFixup = fixups[i];
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]); svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
} }
if (DEBUG) console.log("applySvgFixups end"); if (DEBUG) console.log("applySvgFixups end");
} },
}; };

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var MatrixClientPeg = require('./MatrixClientPeg'); const MatrixClientPeg = require('./MatrixClientPeg');
import UserSettingsStore from './UserSettingsStore'; import UserSettingsStore from './UserSettingsStore';
import shouldHideEvent from './shouldHideEvent'; import shouldHideEvent from './shouldHideEvent';
var sdk = require('./index'); const sdk = require('./index');
module.exports = { module.exports = {
/** /**
@ -34,17 +34,17 @@ module.exports = {
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { } else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
return false; return false;
} }
var EventTile = sdk.getComponent('rooms.EventTile'); const EventTile = sdk.getComponent('rooms.EventTile');
return EventTile.haveTileForEvent(ev); return EventTile.haveTileForEvent(ev);
}, },
doesRoomHaveUnreadMessages: function(room) { 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. // get the most recent read receipt sent by our account.
// N.B. this is NOT a read marker (RM, aka "read up to marker"), // N.B. this is NOT a read marker (RM, aka "read up to marker"),
// despite the name of the method :(( // 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 // 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! // if *we* sent the last message into the room, we consider it not unread!
@ -54,8 +54,7 @@ module.exports = {
// https://github.com/vector-im/riot-web/issues/3363 // https://github.com/vector-im/riot-web/issues/3363
if (room.timeline.length && if (room.timeline.length &&
room.timeline[room.timeline.length - 1].sender && 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; return false;
} }
@ -67,8 +66,8 @@ module.exports = {
const syncedSettings = UserSettingsStore.getSyncedSettings(); const syncedSettings = UserSettingsStore.getSyncedSettings();
// Loop through messages, starting with the most recent... // Loop through messages, starting with the most recent...
for (var i = room.timeline.length - 1; i >= 0; --i) { for (let i = room.timeline.length - 1; i >= 0; --i) {
var ev = room.timeline[i]; const ev = room.timeline[i];
if (ev.getId() == readUpToId) { if (ev.getId() == readUpToId) {
// If we've read up to this event, there's nothing more recents // If we've read up to this event, there's nothing more recents
@ -86,5 +85,5 @@ module.exports = {
// is unread on the theory that false positives are better than // is unread on the theory that false positives are better than
// false negatives here. // false negatives here.
return true; return true;
} },
}; };

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,33 +18,51 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import Notifier from './Notifier'; import Notifier from './Notifier';
import { _t } from './languageHandler'; import { _t, _td } from './languageHandler';
import SdkConfig from './SdkConfig';
/* /*
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage. * TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
*/ */
const FEATURES = [
{
id: 'feature_groups',
name: _td("Groups"),
},
];
export default { export default {
LABS_FEATURES: [ getLabsFeatures() {
{ const featuresConfig = SdkConfig.get()['features'] || {};
name: "-",
id: 'matrix_apps',
default: true,
// XXX: Always use default, ignore localStorage and remove from labs // The old flag: honourned for backwards compat
override: true, const enableLabs = SdkConfig.get()['enableLabs'];
},
{
name: "-",
id: 'feature_groups',
default: false,
},
],
// horrible but it works. The locality makes this somewhat more palatable. let labsFeatures;
doTranslations: function() { if (enableLabs) {
this.LABS_FEATURES[0].name = _t("Matrix Apps"); labsFeatures = FEATURES;
this.LABS_FEATURES[1].name = _t("Groups"); } else {
labsFeatures = FEATURES.filter((f) => {
const sdkConfigValue = featuresConfig[f.id];
if (sdkConfigValue === 'labs') {
return true;
}
});
}
return labsFeatures.map((f) => {
return f.id;
});
},
translatedNameForFeature(featureId) {
const feature = FEATURES.filter((f) => {
return f.id === featureId;
})[0];
if (feature === undefined) return null;
return _t(feature.name);
}, },
loadProfileInfo: function() { loadProfileInfo: function() {
@ -180,33 +199,33 @@ export default {
localStorage.setItem('mx_local_settings', JSON.stringify(settings)); 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 { isFeatureEnabled: function(featureId: string): boolean {
// Disable labs for guests. const featuresConfig = SdkConfig.get()['features'];
if (MatrixClientPeg.get().isGuest()) return false;
const feature = this.getFeatureById(featureId); // The old flag: honourned for backwards compat
if (!feature) { const enableLabs = SdkConfig.get()['enableLabs'];
console.warn(`Unknown feature "${featureId}"`);
let sdkConfigValue = enableLabs ? 'labs' : 'disable';
if (featuresConfig && featuresConfig[featureId] !== undefined) {
sdkConfigValue = featuresConfig[featureId];
}
if (sdkConfigValue === 'enable') {
return true;
} else if (sdkConfigValue === 'disable') {
return false;
} else if (sdkConfigValue === 'labs') {
if (!MatrixClientPeg.get().isGuest()) {
// Make it explicit that guests get the defaults (although they shouldn't
// have been able to ever toggle the flags anyway)
const userValue = localStorage.getItem(`mx_labs_feature_${featureId}`);
return userValue === 'true';
}
return false;
} else {
console.warn(`Unknown features config for ${featureId}: ${sdkConfigValue}`);
return false; 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) { setFeatureEnabled: function(featureId: string, enabled: boolean) {

View file

@ -1,6 +1,6 @@
var React = require('react'); const React = require('react');
var ReactDom = require('react-dom'); const ReactDom = require('react-dom');
var Velocity = require('velocity-vector'); const Velocity = require('velocity-vector');
/** /**
* The Velociraptor contains components and animates transitions with velocity. * 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 * update `this.children` according to the new list of children given
*/ */
_updateChildren: function(newChildren) { _updateChildren: function(newChildren) {
var self = this; const self = this;
var oldChildren = this.children || {}; const oldChildren = this.children || {};
this.children = {}; this.children = {};
React.Children.toArray(newChildren).forEach(function(c) { React.Children.toArray(newChildren).forEach(function(c) {
if (oldChildren[c.key]) { if (oldChildren[c.key]) {
var old = oldChildren[c.key]; const old = oldChildren[c.key];
var oldNode = ReactDom.findDOMNode(self.nodes[old.key]); const oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
if (oldNode && oldNode.style.left != c.props.style.left) { if (oldNode && oldNode.style.left != c.props.style.left) {
Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() { Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
@ -71,18 +71,18 @@ module.exports = React.createClass({
} else { } else {
// new element. If we have a startStyle, use that as the style and go through // new element. If we have a startStyle, use that as the style and go through
// the enter animations // the enter animations
var newProps = {}; const newProps = {};
var restingStyle = c.props.style; const restingStyle = c.props.style;
var startStyles = self.props.startStyles; const startStyles = self.props.startStyles;
if (startStyles.length > 0) { if (startStyles.length > 0) {
var startStyle = startStyles[0]; const startStyle = startStyles[0];
newProps.style = startStyle; newProps.style = startStyle;
// console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); // console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
} }
newProps.ref = (n => self._collectNode( newProps.ref = ((n) => self._collectNode(
c.key, n, restingStyle c.key, n, restingStyle,
)); ));
self.children[c.key] = React.cloneElement(c, newProps); self.children[c.key] = React.cloneElement(c, newProps);
@ -103,8 +103,8 @@ module.exports = React.createClass({
this.nodes[k] === undefined && this.nodes[k] === undefined &&
this.props.startStyles.length > 0 this.props.startStyles.length > 0
) { ) {
var startStyles = this.props.startStyles; const startStyles = this.props.startStyles;
var transitionOpts = this.props.enterTransitionOpts; const transitionOpts = this.props.enterTransitionOpts;
const domNode = ReactDom.findDOMNode(node); const domNode = ReactDom.findDOMNode(node);
// start from startStyle 1: 0 is the one we gave it // start from startStyle 1: 0 is the one we gave it
// to start with, so now we animate 1 etc. // to start with, so now we animate 1 etc.
@ -154,7 +154,7 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<span> <span>
{Object.values(this.children)} { Object.values(this.children) }
</span> </span>
); );
}, },

View file

@ -1,9 +1,9 @@
var Velocity = require('velocity-vector'); const Velocity = require('velocity-vector');
// courtesy of https://github.com/julianshapiro/velocity/issues/283 // courtesy of https://github.com/julianshapiro/velocity/issues/283
// We only use easeOutBounce (easeInBounce is just sort of nonsensical) // We only use easeOutBounce (easeInBounce is just sort of nonsensical)
function bounce( p ) { function bounce( p ) {
var pow2, let pow2,
bounce = 4; bounce = 4;
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) { while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {

View file

@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var MatrixClientPeg = require("./MatrixClientPeg"); const MatrixClientPeg = require("./MatrixClientPeg");
import { _t } from './languageHandler'; import { _t } from './languageHandler';
module.exports = { module.exports = {
usersTypingApartFromMeAndIgnored: function(room) { usersTypingApartFromMeAndIgnored: function(room) {
return this.usersTyping( return this.usersTyping(
room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()) room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()),
); );
}, },
usersTypingApartFromMe: function(room) { usersTypingApartFromMe: function(room) {
return this.usersTyping( return this.usersTyping(
room, [MatrixClientPeg.get().credentials.userId] room, [MatrixClientPeg.get().credentials.userId],
); );
}, },
@ -35,15 +35,15 @@ module.exports = {
* to exclude, return a list of user objects who are typing. * to exclude, return a list of user objects who are typing.
*/ */
usersTyping: function(room, exclude) { usersTyping: function(room, exclude) {
var whoIsTyping = []; const whoIsTyping = [];
if (exclude === undefined) { if (exclude === undefined) {
exclude = []; exclude = [];
} }
var memberKeys = Object.keys(room.currentState.members); const memberKeys = Object.keys(room.currentState.members);
for (var i = 0; i < memberKeys.length; ++i) { for (let i = 0; i < memberKeys.length; ++i) {
var userId = memberKeys[i]; const userId = memberKeys[i];
if (room.currentState.members[userId].typing) { if (room.currentState.members[userId].typing) {
if (exclude.indexOf(userId) == -1) { if (exclude.indexOf(userId) == -1) {
@ -76,5 +76,5 @@ module.exports = {
const lastPerson = names.pop(); const lastPerson = names.pop();
return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson}); return _t('%(names)s and %(lastPerson)s are typing', {names: names.join(', '), lastPerson: lastPerson});
} }
} },
}; };

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require("react"); const React = require("react");
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
var sdk = require('../../../index'); const sdk = require('../../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg"); const MatrixClientPeg = require("../../../MatrixClientPeg");
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'EncryptedEventDialog', displayName: 'EncryptedEventDialog',
@ -33,7 +33,7 @@ module.exports = React.createClass({
componentWillMount: function() { componentWillMount: function() {
this._unmounted = false; this._unmounted = false;
var client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
// first try to load the device from our store. // first try to load the device from our store.
// //
@ -60,7 +60,7 @@ module.exports = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
this._unmounted = true; this._unmounted = true;
var client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (client) { if (client) {
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
} }
@ -89,12 +89,12 @@ module.exports = React.createClass({
}, },
_renderDeviceInfo: function() { _renderDeviceInfo: function() {
var device = this.state.device; const device = this.state.device;
if (!device) { if (!device) {
return (<i>{ _t('unknown device') }</i>); return (<i>{ _t('unknown device') }</i>);
} }
var verificationStatus = (<b>{ _t('NOT verified') }</b>); let verificationStatus = (<b>{ _t('NOT verified') }</b>);
if (device.isBlocked()) { if (device.isBlocked()) {
verificationStatus = (<b>{ _t('Blacklisted') }</b>); verificationStatus = (<b>{ _t('Blacklisted') }</b>);
} else if (device.isVerified()) { } else if (device.isVerified()) {
@ -118,7 +118,7 @@ module.exports = React.createClass({
</tr> </tr>
<tr> <tr>
<td>{ _t('Ed25519 fingerprint') }</td> <td>{ _t('Ed25519 fingerprint') }</td>
<td><code>{device.getFingerprint()}</code></td> <td><code>{ device.getFingerprint() }</code></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -126,7 +126,7 @@ module.exports = React.createClass({
}, },
_renderEventInfo: function() { _renderEventInfo: function() {
var event = this.props.event; const event = this.props.event;
return ( return (
<table> <table>
@ -165,36 +165,36 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
var buttons = null; let buttons = null;
if (this.state.device) { if (this.state.device) {
buttons = ( buttons = (
<DeviceVerifyButtons device={ this.state.device } <DeviceVerifyButtons device={this.state.device}
userId={ this.props.event.getSender() } userId={this.props.event.getSender()}
/> />
); );
} }
return ( return (
<div className="mx_EncryptedEventDialog" onKeyDown={ this.onKeyDown }> <div className="mx_EncryptedEventDialog" onKeyDown={this.onKeyDown}>
<div className="mx_Dialog_title"> <div className="mx_Dialog_title">
{ _t('End-to-end encryption information') } { _t('End-to-end encryption information') }
</div> </div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<h4>{ _t('Event information') }</h4> <h4>{ _t('Event information') }</h4>
{this._renderEventInfo()} { this._renderEventInfo() }
<h4>{ _t('Sender device information') }</h4> <h4>{ _t('Sender device information') }</h4>
{this._renderDeviceInfo()} { this._renderDeviceInfo() }
</div> </div>
<div className="mx_Dialog_buttons"> <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') } { _t('OK') }
</button> </button>
{buttons} { buttons }
</div> </div>
</div> </div>
); );
} },
}); });

View file

@ -45,7 +45,7 @@ const PROVIDERS = [
EmojiProvider, EmojiProvider,
CommandProvider, CommandProvider,
DuckDuckGoProvider, DuckDuckGoProvider,
].map(completer => completer.getInstance()); ].map((completer) => completer.getInstance());
// Providers will get rejected if they take longer than this. // Providers will get rejected if they take longer than this.
const PROVIDER_COMPLETION_TIMEOUT = 3000; const PROVIDER_COMPLETION_TIMEOUT = 3000;

View file

@ -16,7 +16,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler'; import { _t, _td } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import FuzzyMatcher from './FuzzyMatcher'; import FuzzyMatcher from './FuzzyMatcher';
import {TextualCompletion} from './Components'; import {TextualCompletion} from './Components';
@ -27,82 +27,82 @@ const COMMANDS = [
{ {
command: '/me', command: '/me',
args: '<message>', args: '<message>',
description: 'Displays action', description: _td('Displays action'),
}, },
{ {
command: '/ban', command: '/ban',
args: '<user-id> [reason]', args: '<user-id> [reason]',
description: 'Bans user with given id', description: _td('Bans user with given id'),
}, },
{ {
command: '/unban', command: '/unban',
args: '<user-id>', args: '<user-id>',
description: 'Unbans user with given id', description: _td('Unbans user with given id'),
}, },
{ {
command: '/op', command: '/op',
args: '<user-id> [<power-level>]', args: '<user-id> [<power-level>]',
description: 'Define the power level of a user', description: _td('Define the power level of a user'),
}, },
{ {
command: '/deop', command: '/deop',
args: '<user-id>', args: '<user-id>',
description: 'Deops user with given id', description: _td('Deops user with given id'),
}, },
{ {
command: '/invite', command: '/invite',
args: '<user-id>', args: '<user-id>',
description: 'Invites user with given id to current room', description: _td('Invites user with given id to current room'),
}, },
{ {
command: '/join', command: '/join',
args: '<room-alias>', args: '<room-alias>',
description: 'Joins room with given alias', description: _td('Joins room with given alias'),
}, },
{ {
command: '/part', command: '/part',
args: '[<room-alias>]', args: '[<room-alias>]',
description: 'Leave room', description: _td('Leave room'),
}, },
{ {
command: '/topic', command: '/topic',
args: '<topic>', args: '<topic>',
description: 'Sets the room topic', description: _td('Sets the room topic'),
}, },
{ {
command: '/kick', command: '/kick',
args: '<user-id> [reason]', args: '<user-id> [reason]',
description: 'Kicks user with given id', description: _td('Kicks user with given id'),
}, },
{ {
command: '/nick', command: '/nick',
args: '<display-name>', args: '<display-name>',
description: 'Changes your display nickname', description: _td('Changes your display nickname'),
}, },
{ {
command: '/ddg', command: '/ddg',
args: '<query>', args: '<query>',
description: 'Searches DuckDuckGo for results', description: _td('Searches DuckDuckGo for results'),
}, },
{ {
command: '/tint', command: '/tint',
args: '<color1> [<color2>]', args: '<color1> [<color2>]',
description: 'Changes colour scheme of current room', description: _td('Changes colour scheme of current room'),
}, },
{ {
command: '/verify', command: '/verify',
args: '<user-id> <device-id> <device-signing-key>', 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', command: '/ignore',
args: '<user-id>', args: '<user-id>',
description: 'Ignores a user, hiding their messages from you', description: _td('Ignores a user, hiding their messages from you'),
}, },
{ {
command: '/unignore', command: '/unignore',
args: '<user-id>', args: '<user-id>',
description: 'Stops ignoring a user, showing their messages going forward', description: _td('Stops ignoring a user, showing their messages going forward'),
}, },
// Omitting `/markdown` as it only seems to apply to OldComposer // Omitting `/markdown` as it only seems to apply to OldComposer
]; ];

View file

@ -30,13 +30,13 @@ export class TextualCompletion extends React.Component {
subtitle, subtitle,
description, description,
className, className,
...restProps, ...restProps
} = this.props; } = this.props;
return ( return (
<div className={classNames('mx_Autocomplete_Completion_block', className)} {...restProps}> <div className={classNames('mx_Autocomplete_Completion_block', className)} {...restProps}>
<span className="mx_Autocomplete_Completion_title">{title}</span> <span className="mx_Autocomplete_Completion_title">{ title }</span>
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span> <span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
<span className="mx_Autocomplete_Completion_description">{description}</span> <span className="mx_Autocomplete_Completion_description">{ description }</span>
</div> </div>
); );
} }
@ -56,14 +56,14 @@ export class PillCompletion extends React.Component {
description, description,
initialComponent, initialComponent,
className, className,
...restProps, ...restProps
} = this.props; } = this.props;
return ( return (
<div className={classNames('mx_Autocomplete_Completion_pill', className)} {...restProps}> <div className={classNames('mx_Autocomplete_Completion_pill', className)} {...restProps}>
{initialComponent} { initialComponent }
<span className="mx_Autocomplete_Completion_title">{title}</span> <span className="mx_Autocomplete_Completion_title">{ title }</span>
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span> <span className="mx_Autocomplete_Completion_subtitle">{ subtitle }</span>
<span className="mx_Autocomplete_Completion_description">{description}</span> <span className="mx_Autocomplete_Completion_description">{ description }</span>
</div> </div>
); );
} }

View file

@ -38,7 +38,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
} }
async getCompletions(query: string, selection: {start: number, end: number}) { 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) { if (!query || !command) {
return []; return [];
} }
@ -47,7 +47,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
method: 'GET', method: 'GET',
}); });
const json = await response.json(); const json = await response.json();
let results = json.Results.map(result => { const results = json.Results.map((result) => {
return { return {
completion: result.Text, completion: result.Text,
component: ( component: (
@ -105,7 +105,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
renderCompletions(completions: [React.Component]): ?React.Component { renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_block"> return <div className="mx_Autocomplete_Completion_container_block">
{completions} { completions }
</div>; </div>;
} }
} }

View file

@ -138,7 +138,7 @@ export default class EmojiProvider extends AutocompleteProvider {
return { return {
completion: unicode, completion: unicode,
component: ( component: (
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{unicode}</EmojiText>} /> <PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{ unicode }</EmojiText>} />
), ),
range, range,
}; };
@ -152,14 +152,13 @@ export default class EmojiProvider extends AutocompleteProvider {
} }
static getInstance() { static getInstance() {
if (instance == null) if (instance == null) {instance = new EmojiProvider();}
{instance = new EmojiProvider();}
return instance; return instance;
} }
renderCompletions(completions: [React.Component]): ?React.Component { renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill"> return <div className="mx_Autocomplete_Completion_container_pill">
{completions} { completions }
</div>; </div>;
} }
} }

View file

@ -59,7 +59,7 @@ export default class UserProvider extends AutocompleteProvider {
if (this.users === null) this._makeUsers(); if (this.users === null) this._makeUsers();
let completions = []; let completions = [];
let {command, range} = this.getCurrentCommand(query, selection, force); const {command, range} = this.getCurrentCommand(query, selection, force);
if (command) { if (command) {
completions = this.matcher.match(command[0]).map((user) => { completions = this.matcher.match(command[0]).map((user) => {
const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
@ -71,7 +71,7 @@ export default class UserProvider extends AutocompleteProvider {
href: 'https://matrix.to/#/' + user.userId, href: 'https://matrix.to/#/' + user.userId,
component: ( component: (
<PillCompletion <PillCompletion
initialComponent={<MemberAvatar member={user} width={24} height={24}/>} initialComponent={<MemberAvatar member={user} width={24} height={24} />}
title={displayName} title={displayName}
description={user.userId} /> description={user.userId} />
), ),
@ -132,7 +132,7 @@ export default class UserProvider extends AutocompleteProvider {
renderCompletions(completions: [React.Component]): ?React.Component { renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"> return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
{completions} { completions }
</div>; </div>;
} }

View file

@ -17,9 +17,9 @@ limitations under the License.
'use strict'; 'use strict';
var classNames = require('classnames'); const classNames = require('classnames');
var React = require('react'); const React = require('react');
var ReactDOM = require('react-dom'); const ReactDOM = require('react-dom');
// Shamelessly ripped off Modal.js. There's probably a better way // Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and // of doing reusable widgets like dialog boxes & menus where we go and
@ -36,7 +36,7 @@ module.exports = {
}, },
getOrCreateContainer: function() { getOrCreateContainer: function() {
var container = document.getElementById(this.ContextualMenuContainerId); let container = document.getElementById(this.ContextualMenuContainerId);
if (!container) { if (!container) {
container = document.createElement("div"); container = document.createElement("div");
@ -48,9 +48,9 @@ module.exports = {
}, },
createMenu: function(Element, props) { createMenu: function(Element, props) {
var self = this; const self = this;
var closeMenu = function() { const closeMenu = function() {
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer()); ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) { if (props && props.onFinished) {
@ -58,17 +58,17 @@ module.exports = {
} }
}; };
var position = { const position = {
top: props.top, top: props.top,
}; };
var chevronOffset = {}; const chevronOffset = {};
if (props.chevronOffset) { if (props.chevronOffset) {
chevronOffset.top = props.chevronOffset; chevronOffset.top = props.chevronOffset;
} }
// To override the default chevron colour, if it's been set // To override the default chevron colour, if it's been set
var chevronCSS = ""; let chevronCSS = "";
if (props.menuColour) { if (props.menuColour) {
chevronCSS = ` chevronCSS = `
.mx_ContextualMenu_chevron_left:after { .mx_ContextualMenu_chevron_left:after {
@ -81,7 +81,7 @@ module.exports = {
`; `;
} }
var chevron = null; let chevron = null;
if (props.left) { if (props.left) {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>; chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
position.left = props.left; position.left = props.left;
@ -90,15 +90,15 @@ module.exports = {
position.right = props.right; position.right = props.right;
} }
var className = 'mx_ContextualMenu_wrapper'; const className = 'mx_ContextualMenu_wrapper';
var menuClasses = classNames({ const menuClasses = classNames({
'mx_ContextualMenu': true, 'mx_ContextualMenu': true,
'mx_ContextualMenu_left': props.left, 'mx_ContextualMenu_left': props.left,
'mx_ContextualMenu_right': !props.left, 'mx_ContextualMenu_right': !props.left,
}); });
var menuStyle = {}; const menuStyle = {};
if (props.menuWidth) { if (props.menuWidth) {
menuStyle.width = props.menuWidth; menuStyle.width = props.menuWidth;
} }
@ -113,14 +113,14 @@ module.exports = {
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished // FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click! // property set here so you can't close the menu from a button click!
var menu = ( const menu = (
<div className={className} style={position}> <div className={className} style={position}>
<div className={menuClasses} style={menuStyle}> <div className={menuClasses} style={menuStyle}>
{chevron} { chevron }
<Element {...props} onFinished={closeMenu}/> <Element {...props} onFinished={closeMenu} />
</div> </div>
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div> <div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
<style>{chevronCSS}</style> <style>{ chevronCSS }</style>
</div> </div>
); );

View file

@ -61,7 +61,7 @@ module.exports = React.createClass({
}, },
onCreateRoom: function() { onCreateRoom: function() {
var options = {}; const options = {};
if (this.state.room_name) { if (this.state.room_name) {
options.name = this.state.room_name; options.name = this.state.room_name;
@ -79,14 +79,14 @@ module.exports = React.createClass({
{ {
type: "m.room.join_rules", type: "m.room.join_rules",
content: { content: {
"join_rule": this.state.is_private ? "invite" : "public" "join_rule": this.state.is_private ? "invite" : "public",
} },
}, },
{ {
type: "m.room.history_visibility", type: "m.room.history_visibility",
content: { 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; options.invite = this.state.invited_users;
var alias = this.getAliasLocalpart(); const alias = this.getAliasLocalpart();
if (alias) { if (alias) {
options.room_alias_name = alias; options.room_alias_name = alias;
} }
var cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!cli) { if (!cli) {
// TODO: Error. // TODO: Error.
console.error("Cannot create room: No matrix client."); console.error("Cannot create room: No matrix client.");
return; return;
} }
var deferred = cli.createRoom(options); const deferred = cli.createRoom(options);
if (this.state.encrypt) { if (this.state.encrypt) {
// TODO // TODO
@ -116,7 +116,7 @@ module.exports = React.createClass({
phase: this.phases.CREATING, phase: this.phases.CREATING,
}); });
var self = this; const self = this;
deferred.then(function(resp) { deferred.then(function(resp) {
self.setState({ self.setState({
@ -209,7 +209,7 @@ module.exports = React.createClass({
onAliasChanged: function(alias) { onAliasChanged: function(alias) {
this.setState({ this.setState({
alias: alias alias: alias,
}); });
}, },
@ -220,64 +220,64 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var curr_phase = this.state.phase; const curr_phase = this.state.phase;
if (curr_phase == this.phases.CREATING) { if (curr_phase == this.phases.CREATING) {
var Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
return ( return (
<Loader/> <Loader />
); );
} else { } else {
var error_box = ""; let error_box = "";
if (curr_phase == this.phases.ERROR) { if (curr_phase == this.phases.ERROR) {
error_box = ( error_box = (
<div className="mx_Error"> <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> </div>
); );
} }
var CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton"); const CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
var RoomAlias = sdk.getComponent("create_room.RoomAlias"); const RoomAlias = sdk.getComponent("create_room.RoomAlias");
var Presets = sdk.getComponent("create_room.Presets"); const Presets = sdk.getComponent("create_room.Presets");
var UserSelector = sdk.getComponent("elements.UserSelector"); const UserSelector = sdk.getComponent("elements.UserSelector");
var SimpleRoomHeader = sdk.getComponent("rooms.SimpleRoomHeader"); const SimpleRoomHeader = sdk.getComponent("rooms.SimpleRoomHeader");
var domain = MatrixClientPeg.get().getDomain(); const domain = MatrixClientPeg.get().getDomain();
return ( return (
<div className="mx_CreateRoom"> <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"> <div className="mx_CreateRoom_body">
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')}/> <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 /> <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 /> <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 /> <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 /> <Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset} /> <br />
<div> <div>
<label> <label>
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/> <input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged} />
{_t('Make this room private')} { _t('Make this room private') }
</label> </label>
</div> </div>
<div> <div>
<label> <label>
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/> <input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged} />
{_t('Share message history with new users')} { _t('Share message history with new users') }
</label> </label>
</div> </div>
<div className="mx_CreateRoom_encrypt"> <div className="mx_CreateRoom_encrypt">
<label> <label>
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/> <input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged} />
{_t('Encrypt room')} { _t('Encrypt room') }
</label> </label>
</div> </div>
<div> <div>
<CreateRoomButton onCreateRoom={this.onCreateRoom} /> <br /> <CreateRoomButton onCreateRoom={this.onCreateRoom} /> <br />
</div> </div>
{error_box} { error_box }
</div> </div>
</div> </div>
); );
} }
} },
}); });

View file

@ -24,7 +24,7 @@ import { _t, _tJsx } from '../../languageHandler';
/* /*
* Component which shows the filtered file using a TimelinePanel * Component which shows the filtered file using a TimelinePanel
*/ */
var FilePanel = React.createClass({ const FilePanel = React.createClass({
displayName: 'FilePanel', displayName: 'FilePanel',
propTypes: { propTypes: {
@ -55,33 +55,33 @@ var FilePanel = React.createClass({
}, },
updateTimelineSet: function(roomId) { updateTimelineSet: function(roomId) {
var client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
var room = client.getRoom(roomId); const room = client.getRoom(roomId);
this.noRoom = !room; this.noRoom = !room;
if (room) { if (room) {
var filter = new Matrix.Filter(client.credentials.userId); const filter = new Matrix.Filter(client.credentials.userId);
filter.setDefinition( filter.setDefinition(
{ {
"room": { "room": {
"timeline": { "timeline": {
"contains_url": true "contains_url": true,
}, },
} },
} },
); );
// FIXME: we shouldn't be doing this every time we change room - see comment above. // FIXME: we shouldn't be doing this every time we change room - see comment above.
client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then( client.getOrCreateFilter("FILTER_FILES_" + client.credentials.userId, filter).then(
(filterId)=>{ (filterId)=>{
filter.filterId = filterId; filter.filterId = filterId;
var timelineSet = room.getOrCreateFilteredTimelineSet(filter); const timelineSet = room.getOrCreateFilteredTimelineSet(filter);
this.setState({ timelineSet: timelineSet }); this.setState({ timelineSet: timelineSet });
}, },
(error)=>{ (error)=>{
console.error("Failed to get or create file panel filter", error); console.error("Failed to get or create file panel filter", error);
} },
); );
} else { } else {
console.error("Failed to add filtered timelineSet for FilePanel as no room!"); console.error("Failed to add filtered timelineSet for FilePanel as no room!");
@ -92,18 +92,18 @@ var FilePanel = React.createClass({
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper"> return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
<div className="mx_RoomView_empty"> <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>)} { _tJsx("You must <a>register</a> to use this functionality", /<a>(.*?)<\/a>/, (sub) => <a href="#/register" key="sub">{ sub }</a>) }
</div> </div>
</div>; </div>;
} else if (this.noRoom) { } else if (this.noRoom) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper"> 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>; </div>;
} }
// wrap a TimelinePanel with the jump-to-event bits turned off. // wrap a TimelinePanel with the jump-to-event bits turned off.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
if (this.state.timelineSet) { if (this.state.timelineSet) {
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " + // console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
@ -114,17 +114,16 @@ var FilePanel = React.createClass({
manageReadReceipts={false} manageReadReceipts={false}
manageReadMarkers={false} manageReadMarkers={false}
timelineSet={this.state.timelineSet} timelineSet={this.state.timelineSet}
showUrlPreview = { false } showUrlPreview = {false}
tileShape="file_grid" tileShape="file_grid"
opacity={ this.props.opacity } opacity={this.props.opacity}
empty={_t('There are no visible files in this room')} empty={_t('There are no visible files in this room')}
/> />
); );
} } else {
else {
return ( return (
<div className="mx_FilePanel"> <div className="mx_FilePanel">
<Loader/> <Loader />
</div> </div>
); );
} }

View file

@ -27,7 +27,8 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal'; import Modal from '../../Modal';
import classnames from 'classnames'; import classnames from 'classnames';
import GroupSummaryStore from '../../stores/GroupSummaryStore'; import GroupStoreCache from '../../stores/GroupStoreCache';
import GroupStore from '../../stores/GroupStore';
const RoomSummaryType = PropTypes.shape({ const RoomSummaryType = PropTypes.shape({
room_id: PropTypes.string.isRequired, room_id: PropTypes.string.isRequired,
@ -78,7 +79,7 @@ const CategoryRoomList = React.createClass({
if (!success) return; if (!success) return;
const errorList = []; const errorList = [];
Promise.all(addrs.map((addr) => { Promise.all(addrs.map((addr) => {
return this.context.groupSummaryStore return this.context.groupStore
.addRoomToGroupSummary(addr.address) .addRoomToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); }) .catch(() => { errorList.push(addr.address); })
.reflect(); .reflect();
@ -157,7 +158,7 @@ const FeaturedRoom = React.createClass({
onDeleteClicked: function(e) { onDeleteClicked: function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.context.groupSummaryStore.removeRoomFromGroupSummary( this.context.groupStore.removeRoomFromGroupSummary(
this.props.summaryInfo.room_id, this.props.summaryInfo.room_id,
).catch((err) => { ).catch((err) => {
console.error('Error whilst removing room from group summary', err); console.error('Error whilst removing room from group summary', err);
@ -252,7 +253,7 @@ const RoleUserList = React.createClass({
if (!success) return; if (!success) return;
const errorList = []; const errorList = [];
Promise.all(addrs.map((addr) => { Promise.all(addrs.map((addr) => {
return this.context.groupSummaryStore return this.context.groupStore
.addUserToGroupSummary(addr.address) .addUserToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); }) .catch(() => { errorList.push(addr.address); })
.reflect(); .reflect();
@ -327,7 +328,7 @@ const FeaturedUser = React.createClass({
onDeleteClicked: function(e) { onDeleteClicked: function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.context.groupSummaryStore.removeUserFromGroupSummary( this.context.groupStore.removeUserFromGroupSummary(
this.props.summaryInfo.user_id, this.props.summaryInfo.user_id,
).catch((err) => { ).catch((err) => {
console.error('Error whilst removing user from group summary', err); console.error('Error whilst removing user from group summary', err);
@ -373,14 +374,14 @@ const FeaturedUser = React.createClass({
}, },
}); });
const GroupSummaryContext = { const GroupContext = {
groupSummaryStore: React.PropTypes.instanceOf(GroupSummaryStore).isRequired, groupStore: React.PropTypes.instanceOf(GroupStore).isRequired,
}; };
CategoryRoomList.contextTypes = GroupSummaryContext; CategoryRoomList.contextTypes = GroupContext;
FeaturedRoom.contextTypes = GroupSummaryContext; FeaturedRoom.contextTypes = GroupContext;
RoleUserList.contextTypes = GroupSummaryContext; RoleUserList.contextTypes = GroupContext;
FeaturedUser.contextTypes = GroupSummaryContext; FeaturedUser.contextTypes = GroupContext;
export default React.createClass({ export default React.createClass({
displayName: 'GroupView', displayName: 'GroupView',
@ -390,12 +391,12 @@ export default React.createClass({
}, },
childContextTypes: { childContextTypes: {
groupSummaryStore: React.PropTypes.instanceOf(GroupSummaryStore), groupStore: React.PropTypes.instanceOf(GroupStore),
}, },
getChildContext: function() { getChildContext: function() {
return { return {
groupSummaryStore: this._groupSummaryStore, groupStore: this._groupStore,
}; };
}, },
@ -413,14 +414,14 @@ export default React.createClass({
componentWillMount: function() { componentWillMount: function() {
this._changeAvatarComponent = null; this._changeAvatarComponent = null;
this._initGroupSummaryStore(this.props.groupId); this._initGroupStore(this.props.groupId);
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership); MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership); MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
this._groupSummaryStore.removeAllListeners(); this._groupStore.removeAllListeners();
}, },
componentWillReceiveProps: function(newProps) { componentWillReceiveProps: function(newProps) {
@ -429,7 +430,7 @@ export default React.createClass({
summary: null, summary: null,
error: null, error: null,
}, () => { }, () => {
this._initGroupSummaryStore(newProps.groupId); this._initGroupStore(newProps.groupId);
}); });
} }
}, },
@ -440,17 +441,15 @@ export default React.createClass({
this.setState({membershipBusy: false}); this.setState({membershipBusy: false});
}, },
_initGroupSummaryStore: function(groupId) { _initGroupStore: function(groupId) {
this._groupSummaryStore = new GroupSummaryStore( this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
MatrixClientPeg.get(), this.props.groupId, this._groupStore.on('update', () => {
);
this._groupSummaryStore.on('update', () => {
this.setState({ this.setState({
summary: this._groupSummaryStore.getSummary(), summary: this._groupStore.getSummary(),
error: null, error: null,
}); });
}); });
this._groupSummaryStore.on('error', (err) => { this._groupStore.on('error', (err) => {
this.setState({ this.setState({
summary: null, summary: null,
error: err, error: err,
@ -527,7 +526,7 @@ export default React.createClass({
editing: false, editing: false,
summary: null, summary: null,
}); });
this._initGroupSummaryStore(this.props.groupId); this._initGroupStore(this.props.groupId);
}).catch((e) => { }).catch((e) => {
this.setState({ this.setState({
saving: false, saving: false,
@ -606,7 +605,7 @@ export default React.createClass({
this.setState({ this.setState({
publicityBusy: true, publicityBusy: true,
}); });
this._groupSummaryStore.setGroupPublicity(publicity).then(() => { this._groupStore.setGroupPublicity(publicity).then(() => {
this.setState({ this.setState({
publicityBusy: false, publicityBusy: false,
}); });

View file

@ -107,7 +107,7 @@ export default React.createClass({
const msg = error.message || error.toString(); const msg = error.message || error.toString();
this.setState({ this.setState({
errorText: msg errorText: msg,
}); });
}).done(); }).done();
@ -207,7 +207,7 @@ export default React.createClass({
if (this.state.errorText) { if (this.state.errorText) {
error = ( error = (
<div className="error"> <div className="error">
{this.state.errorText} { this.state.errorText }
</div> </div>
); );
} }
@ -215,8 +215,8 @@ export default React.createClass({
return ( return (
<div> <div>
<div> <div>
{this._renderCurrentStage()} { this._renderCurrentStage() }
{error} { error }
</div> </div>
</div> </div>
); );

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -164,7 +165,7 @@ export default React.createClass({
case KeyCode.UP: case KeyCode.UP:
case KeyCode.DOWN: case KeyCode.DOWN:
if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { 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'; 'view_prev_room' : 'view_next_room';
dis.dispatch({action: action}); dis.dispatch({action: action});
handled = true; handled = true;
@ -206,8 +207,7 @@ export default React.createClass({
_onScrollKeyPressed: function(ev) { _onScrollKeyPressed: function(ev) {
if (this.refs.roomView) { if (this.refs.roomView) {
this.refs.roomView.handleScrollKey(ev); this.refs.roomView.handleScrollKey(ev);
} } else if (this.refs.roomDirectory) {
else if (this.refs.roomDirectory) {
this.refs.roomDirectory.handleScrollKey(ev); this.refs.roomDirectory.handleScrollKey(ev);
} }
}, },
@ -251,11 +251,10 @@ export default React.createClass({
page_element = <UserSettings page_element = <UserSettings
onClose={this.props.onUserSettingsClose} onClose={this.props.onUserSettingsClose}
brand={this.props.config.brand} brand={this.props.config.brand}
enableLabs={this.props.config.enableLabs}
referralBaseUrl={this.props.config.referralBaseUrl} referralBaseUrl={this.props.config.referralBaseUrl}
teamToken={this.props.teamToken} teamToken={this.props.teamToken}
/>; />;
if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>; if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity} />;
break; break;
case PageTypes.MyGroups: case PageTypes.MyGroups:
@ -267,7 +266,7 @@ export default React.createClass({
onRoomCreated={this.props.onRoomCreated} onRoomCreated={this.props.onRoomCreated}
collapsedRhs={this.props.collapseRhs} collapsedRhs={this.props.collapseRhs}
/>; />;
if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>; if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity} />;
break; break;
case PageTypes.RoomDirectory: case PageTypes.RoomDirectory:
@ -320,7 +319,7 @@ export default React.createClass({
topBar = <MatrixToolbar />; topBar = <MatrixToolbar />;
} }
var bodyClasses = 'mx_MatrixChat'; let bodyClasses = 'mx_MatrixChat';
if (topBar) { if (topBar) {
bodyClasses += ' mx_MatrixChat_toolbarShowing'; bodyClasses += ' mx_MatrixChat_toolbarShowing';
} }
@ -330,7 +329,7 @@ export default React.createClass({
return ( return (
<div className='mx_MatrixChat_wrapper'> <div className='mx_MatrixChat_wrapper'>
{topBar} { topBar }
<div className={bodyClasses}> <div className={bodyClasses}>
<LeftPanel <LeftPanel
selectedRoom={this.props.currentRoomId} selectedRoom={this.props.currentRoomId}
@ -338,9 +337,9 @@ export default React.createClass({
opacity={this.props.leftOpacity} opacity={this.props.leftOpacity}
/> />
<main className='mx_MatrixChat_middlePanel'> <main className='mx_MatrixChat_middlePanel'>
{page_element} { page_element }
</main> </main>
{right_panel} { right_panel }
</div> </div>
</div> </div>
); );

View file

@ -773,15 +773,13 @@ module.exports = React.createClass({
dis.dispatch({action: 'view_set_mxid'}); dis.dispatch({action: 'view_set_mxid'});
return; return;
} }
const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
Modal.createTrackedDialog('Create Room', '', TextInputDialog, { Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
title: _t('Create Room'), onFinished: (shouldCreate, name, noFederate) => {
description: _t('Room name (optional)'),
button: _t('Create Room'),
onFinished: (shouldCreate, name) => {
if (shouldCreate) { if (shouldCreate) {
const createOpts = {}; const createOpts = {};
if (name) createOpts.name = name; if (name) createOpts.name = name;
if (noFederate) createOpts.creation_content = {'m.federate': false};
createRoom({createOpts}).done(); createRoom({createOpts}).done();
} }
}, },

View file

@ -154,15 +154,15 @@ module.exports = React.createClass({
// 0: read marker is within the window // 0: read marker is within the window
// +1: read marker is below the window // +1: read marker is below the window
getReadMarkerPosition: function() { getReadMarkerPosition: function() {
var readMarker = this.refs.readMarkerNode; const readMarker = this.refs.readMarkerNode;
var messageWrapper = this.refs.scrollPanel; const messageWrapper = this.refs.scrollPanel;
if (!readMarker || !messageWrapper) { if (!readMarker || !messageWrapper) {
return null; return null;
} }
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect(); const wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
var readMarkerRect = readMarker.getBoundingClientRect(); const readMarkerRect = readMarker.getBoundingClientRect();
// the read-marker pretends to have zero height when it is actually // the read-marker pretends to have zero height when it is actually
// two pixels high; +2 here to account for that. // two pixels high; +2 here to account for that.
@ -262,7 +262,7 @@ module.exports = React.createClass({
this.eventNodes = {}; this.eventNodes = {};
var i; let i;
// first figure out which is the last event in the list which we're // first figure out which is the last event in the list which we're
// actually going to show; this allows us to behave slightly // actually going to show; this allows us to behave slightly
@ -272,9 +272,9 @@ module.exports = React.createClass({
// a local echo, to manage the read-marker. // a local echo, to manage the read-marker.
let lastShownEvent; let lastShownEvent;
var lastShownNonLocalEchoIndex = -1; let lastShownNonLocalEchoIndex = -1;
for (i = this.props.events.length-1; i >= 0; i--) { 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)) { if (!this._shouldShowEvent(mxEv)) {
continue; continue;
} }
@ -292,12 +292,12 @@ module.exports = React.createClass({
break; 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 // 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 the readmarker has moved, cancel any active ghost.
if (this.currentReadMarkerEventId && this.props.readMarkerEventId && if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
@ -309,16 +309,16 @@ module.exports = React.createClass({
const isMembershipChange = (e) => e.getType() === 'm.room.member'; const isMembershipChange = (e) => e.getType() === 'm.room.member';
for (i = 0; i < this.props.events.length; i++) { for (i = 0; i < this.props.events.length; i++) {
let mxEv = this.props.events[i]; const mxEv = this.props.events[i];
let eventId = mxEv.getId(); const eventId = mxEv.getId();
let last = (mxEv === lastShownEvent); const last = (mxEv === lastShownEvent);
const wantTile = this._shouldShowEvent(mxEv); const wantTile = this._shouldShowEvent(mxEv);
// Wrap consecutive member events in a ListSummary, ignore if redacted // Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) && wantTile) { if (isMembershipChange(mxEv) && wantTile) {
let readMarkerInMels = false; let readMarkerInMels = false;
let ts1 = mxEv.getTs(); const ts1 = mxEv.getTs();
// Ensure that the key of the MemberEventListSummary does not change with new // Ensure that the key of the MemberEventListSummary does not change with new
// member events. This will prevent it from being re-created unnecessarily, and // member events. This will prevent it from being re-created unnecessarily, and
// instead will allow new props to be provided. In turn, the shouldComponentUpdate // instead will allow new props to be provided. In turn, the shouldComponentUpdate
@ -330,7 +330,7 @@ module.exports = React.createClass({
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial"); const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) { 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); ret.push(dateSeparator);
} }
@ -339,7 +339,7 @@ module.exports = React.createClass({
readMarkerInMels = true; readMarkerInMels = true;
} }
let summarisedEvents = [mxEv]; const summarisedEvents = [mxEv];
for (;i + 1 < this.props.events.length; i++) { for (;i + 1 < this.props.events.length; i++) {
const collapsedMxEv = this.props.events[i + 1]; const collapsedMxEv = this.props.events[i + 1];
@ -390,7 +390,7 @@ module.exports = React.createClass({
onToggle={this._onWidgetLoad} // Update scroll state onToggle={this._onWidgetLoad} // Update scroll state
startExpanded={highlightInMels} startExpanded={highlightInMels}
> >
{eventTiles} { eventTiles }
</MemberEventListSummary>); </MemberEventListSummary>);
if (readMarkerInMels) { if (readMarkerInMels) {
@ -408,7 +408,7 @@ module.exports = React.createClass({
prevEvent = mxEv; prevEvent = mxEv;
} }
var isVisibleReadMarker = false; let isVisibleReadMarker = false;
if (eventId == this.props.readMarkerEventId) { if (eventId == this.props.readMarkerEventId) {
var visible = this.props.readMarkerVisible; var visible = this.props.readMarkerVisible;
@ -448,10 +448,10 @@ module.exports = React.createClass({
_getTilesForEvent: function(prevEvent, mxEv, last) { _getTilesForEvent: function(prevEvent, mxEv, last) {
const EventTile = sdk.getComponent('rooms.EventTile'); const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator'); const DateSeparator = sdk.getComponent('messages.DateSeparator');
var ret = []; const ret = [];
// is this a continuation of the previous message? // is this a continuation of the previous message?
var continuation = false; let continuation = false;
if (prevEvent !== null if (prevEvent !== null
&& prevEvent.sender && mxEv.sender && prevEvent.sender && mxEv.sender
@ -476,8 +476,8 @@ module.exports = React.createClass({
// local echoes have a fake date, which could even be yesterday. Treat them // local echoes have a fake date, which could even be yesterday. Treat them
// as 'today' for the date separators. // as 'today' for the date separators.
var ts1 = mxEv.getTs(); let ts1 = mxEv.getTs();
var eventDate = mxEv.getDate(); let eventDate = mxEv.getDate();
if (mxEv.status) { if (mxEv.status) {
eventDate = new Date(); eventDate = new Date();
ts1 = eventDate.getTime(); ts1 = eventDate.getTime();
@ -485,19 +485,19 @@ module.exports = React.createClass({
// do we need a date separator since the last event? // do we need a date separator since the last event?
if (this._wantsDateSeparator(prevEvent, eventDate)) { 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); ret.push(dateSeparator);
continuation = false; continuation = false;
} }
var eventId = mxEv.getId(); const eventId = mxEv.getId();
var highlight = (eventId == this.props.highlightedEventId); const highlight = (eventId == this.props.highlightedEventId);
// we can't use local echoes as scroll tokens, because their event IDs change. // we can't use local echoes as scroll tokens, because their event IDs change.
// Local echos have a send "status". // Local echos have a send "status".
var scrollToken = mxEv.status ? undefined : eventId; const scrollToken = mxEv.status ? undefined : eventId;
var readReceipts; let readReceipts;
if (this.props.showReadReceipts) { if (this.props.showReadReceipts) {
readReceipts = this._getReadReceiptsForEvent(mxEv); readReceipts = this._getReadReceiptsForEvent(mxEv);
} }
@ -515,8 +515,8 @@ module.exports = React.createClass({
eventSendStatus={mxEv.status} eventSendStatus={mxEv.status}
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour} isTwelveHour={this.props.isTwelveHour}
last={last} isSelectedEvent={highlight}/> last={last} isSelectedEvent={highlight} />
</li> </li>,
); );
return ret; return ret;
@ -551,7 +551,7 @@ module.exports = React.createClass({
if (!room) { if (!room) {
return null; return null;
} }
let receipts = []; const receipts = [];
room.getReceiptsForEvent(event).forEach((r) => { room.getReceiptsForEvent(event).forEach((r) => {
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) { if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
return; // ignore non-read receipts and receipts from self. return; // ignore non-read receipts and receipts from self.
@ -559,7 +559,7 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isUserIgnored(r.userId)) { if (MatrixClientPeg.get().isUserIgnored(r.userId)) {
return; // ignore ignored users return; // ignore ignored users
} }
let member = room.getMember(r.userId); const member = room.getMember(r.userId);
if (!member) { if (!member) {
return; // ignore unknown user IDs return; // ignore unknown user IDs
} }
@ -575,7 +575,7 @@ module.exports = React.createClass({
}, },
_getReadMarkerTile: function(visible) { _getReadMarkerTile: function(visible) {
var hr; let hr;
if (visible) { if (visible) {
hr = <hr className="mx_RoomView_myReadMarker" hr = <hr className="mx_RoomView_myReadMarker"
style={{opacity: 1, width: '99%'}} style={{opacity: 1, width: '99%'}}
@ -585,7 +585,7 @@ module.exports = React.createClass({
return ( return (
<li key="_readupto" ref="readMarkerNode" <li key="_readupto" ref="readMarkerNode"
className="mx_RoomView_myReadMarker_container"> className="mx_RoomView_myReadMarker_container">
{hr} { hr }
</li> </li>
); );
}, },
@ -604,7 +604,7 @@ module.exports = React.createClass({
}, },
_getReadMarkerGhostTile: function() { _getReadMarkerGhostTile: function() {
var hr = <hr className="mx_RoomView_myReadMarker" const hr = <hr className="mx_RoomView_myReadMarker"
style={{opacity: 1, width: '99%'}} style={{opacity: 1, width: '99%'}}
ref={this._startAnimation} ref={this._startAnimation}
/>; />;
@ -615,7 +615,7 @@ module.exports = React.createClass({
return ( return (
<li key={"_readuptoghost_"+this.currentGhostEventId} <li key={"_readuptoghost_"+this.currentGhostEventId}
className="mx_RoomView_myReadMarker_container"> className="mx_RoomView_myReadMarker_container">
{hr} { hr }
</li> </li>
); );
}, },
@ -627,7 +627,7 @@ module.exports = React.createClass({
// once dynamic content in the events load, make the scrollPanel check the // once dynamic content in the events load, make the scrollPanel check the
// scroll offsets. // scroll offsets.
_onWidgetLoad: function() { _onWidgetLoad: function() {
var scrollPanel = this.refs.scrollPanel; const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) { if (scrollPanel) {
scrollPanel.forceUpdate(); scrollPanel.forceUpdate();
} }
@ -638,9 +638,9 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var ScrollPanel = sdk.getComponent("structures.ScrollPanel"); const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
var Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
var topSpinner, bottomSpinner; let topSpinner, bottomSpinner;
if (this.props.backPaginating) { if (this.props.backPaginating) {
topSpinner = <li key="_topSpinner"><Spinner /></li>; topSpinner = <li key="_topSpinner"><Spinner /></li>;
} }
@ -648,25 +648,25 @@ module.exports = React.createClass({
bottomSpinner = <li key="_bottomSpinner"><Spinner /></li>; bottomSpinner = <li key="_bottomSpinner"><Spinner /></li>;
} }
var style = this.props.hidden ? { display: 'none' } : {}; const style = this.props.hidden ? { display: 'none' } : {};
style.opacity = this.props.opacity; style.opacity = this.props.opacity;
var className = this.props.className + " mx_fadable"; let className = this.props.className + " mx_fadable";
if (this.props.alwaysShowTimestamps) { if (this.props.alwaysShowTimestamps) {
className += " mx_MessagePanel_alwaysShowTimestamps"; className += " mx_MessagePanel_alwaysShowTimestamps";
} }
return ( return (
<ScrollPanel ref="scrollPanel" className={ className } <ScrollPanel ref="scrollPanel" className={className}
onScroll={ this.props.onScroll } onScroll={this.props.onScroll}
onResize={ this.onResize } onResize={this.onResize}
onFillRequest={ this.props.onFillRequest } onFillRequest={this.props.onFillRequest}
onUnfillRequest={ this.props.onUnfillRequest } onUnfillRequest={this.props.onUnfillRequest}
style={ style } style={style}
stickyBottom={ this.props.stickyBottom }> stickyBottom={this.props.stickyBottom}>
{topSpinner} { topSpinner }
{this._getEventTiles()} { this._getEventTiles() }
{bottomSpinner} { bottomSpinner }
</ScrollPanel> </ScrollPanel>
); );
}, },

View file

@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); const React = require('react');
var ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
var Matrix = require("matrix-js-sdk"); const Matrix = require("matrix-js-sdk");
var sdk = require('../../index'); const sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg"); const MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher"); const dis = require("../../dispatcher");
/* /*
* Component which shows the global notification list using a TimelinePanel * Component which shows the global notification list using a TimelinePanel
*/ */
var NotificationPanel = React.createClass({ const NotificationPanel = React.createClass({
displayName: 'NotificationPanel', displayName: 'NotificationPanel',
propTypes: { propTypes: {
@ -33,10 +33,10 @@ var NotificationPanel = React.createClass({
render: function() { render: function() {
// wrap a TimelinePanel with the jump-to-event bits turned off. // wrap a TimelinePanel with the jump-to-event bits turned off.
var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
var Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
var timelineSet = MatrixClientPeg.get().getNotifTimelineSet(); const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) { if (timelineSet) {
return ( return (
<TimelinePanel key={"NotificationPanel_" + this.props.roomId} <TimelinePanel key={"NotificationPanel_" + this.props.roomId}
@ -44,18 +44,17 @@ var NotificationPanel = React.createClass({
manageReadReceipts={false} manageReadReceipts={false}
manageReadMarkers={false} manageReadMarkers={false}
timelineSet={timelineSet} timelineSet={timelineSet}
showUrlPreview = { false } showUrlPreview = {false}
opacity={ this.props.opacity } opacity={this.props.opacity}
tileShape="notif" tileShape="notif"
empty={ _t('You have no visible notifications') } empty={_t('You have no visible notifications')}
/> />
); );
} } else {
else {
console.error("No notifTimelineSet available!"); console.error("No notifTimelineSet available!");
return ( return (
<div className="mx_NotificationPanel"> <div className="mx_NotificationPanel">
<Loader/> <Loader />
</div> </div>
); );
} }

View file

@ -43,6 +43,10 @@ module.exports = React.createClass({
// the end of the live timeline. // the end of the live timeline.
atEndOfLiveTimeline: React.PropTypes.bool, 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 // 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 // the 'Active Call' text in the status bar if there is nothing
// more interesting) // more interesting)
@ -60,6 +64,14 @@ module.exports = React.createClass({
// 'unsent messages' bar // 'unsent messages' bar
onCancelAllClick: React.PropTypes.func, 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 // callback for when the user clicks on the 'scroll to bottom' button
onScrollToBottomClick: React.PropTypes.func, onScrollToBottomClick: React.PropTypes.func,
@ -103,7 +115,7 @@ module.exports = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
// we may have entirely lost our client as we're logging out before clicking login on the guest bar... // 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) { if (client) {
client.removeListener("sync", this.onSyncStateChange); client.removeListener("sync", this.onSyncStateChange);
client.removeListener("RoomMember.typing", this.onRoomMemberTyping); client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
@ -115,7 +127,7 @@ module.exports = React.createClass({
return; return;
} }
this.setState({ this.setState({
syncState: state syncState: state,
}); });
}, },
@ -126,7 +138,7 @@ module.exports = React.createClass({
}, },
// Check whether current size is greater than 0, if yes call props.onVisible // Check whether current size is greater than 0, if yes call props.onVisible
_checkSize: function () { _checkSize: function() {
if (this.props.onVisible && this._getSize()) { if (this.props.onVisible && this._getSize()) {
this.props.onVisible(); this.props.onVisible();
} }
@ -140,7 +152,8 @@ module.exports = React.createClass({
(this.state.usersTyping.length > 0) || (this.state.usersTyping.length > 0) ||
this.props.numUnreadMessages || this.props.numUnreadMessages ||
!this.props.atEndOfLiveTimeline || !this.props.atEndOfLiveTimeline ||
this.props.hasActiveCall this.props.hasActiveCall ||
this.props.sentMessageAndIsAlone
) { ) {
return STATUS_BAR_EXPANDED; return STATUS_BAR_EXPANDED;
} else if (this.props.unsentMessageError) { } else if (this.props.unsentMessageError) {
@ -157,9 +170,9 @@ module.exports = React.createClass({
if (this.props.numUnreadMessages) { if (this.props.numUnreadMessages) {
return ( return (
<div className="mx_RoomStatusBar_scrollDownIndicator" <div className="mx_RoomStatusBar_scrollDownIndicator"
onClick={ this.props.onScrollToBottomClick }> onClick={this.props.onScrollToBottomClick}>
<img src="img/newmessages.svg" width="24" height="24" <img src="img/newmessages.svg" width="24" height="24"
alt=""/> alt="" />
</div> </div>
); );
} }
@ -167,18 +180,18 @@ module.exports = React.createClass({
if (!this.props.atEndOfLiveTimeline) { if (!this.props.atEndOfLiveTimeline) {
return ( return (
<div className="mx_RoomStatusBar_scrollDownIndicator" <div className="mx_RoomStatusBar_scrollDownIndicator"
onClick={ this.props.onScrollToBottomClick }> onClick={this.props.onScrollToBottomClick}>
<img src="img/scrolldown.svg" width="24" height="24" <img src="img/scrolldown.svg" width="24" height="24"
alt={ _t("Scroll to bottom of page") } alt={_t("Scroll to bottom of page")}
title={ _t("Scroll to bottom of page") }/> title={_t("Scroll to bottom of page")} />
</div> </div>
); );
} }
if (this.props.hasActiveCall) { if (this.props.hasActiveCall) {
var TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
return ( return (
<TintableSvg src="img/sound-indicator.svg" width="23" height="20"/> <TintableSvg src="img/sound-indicator.svg" width="23" height="20" />
); );
} }
@ -189,7 +202,7 @@ module.exports = React.createClass({
if (wantPlaceholder) { if (wantPlaceholder) {
return ( return (
<div className="mx_RoomStatusBar_typingIndicatorAvatars"> <div className="mx_RoomStatusBar_typingIndicatorAvatars">
{this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit)} { this._renderTypingIndicatorAvatars(this.props.whoIsTypingLimit) }
</div> </div>
); );
} }
@ -221,8 +234,8 @@ module.exports = React.createClass({
if (othersCount > 0) { if (othersCount > 0) {
avatars.push( avatars.push(
<span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others"> <span className="mx_RoomStatusBar_typingIndicatorRemaining" key="others">
+{othersCount} +{ othersCount }
</span> </span>,
); );
} }
@ -240,12 +253,12 @@ module.exports = React.createClass({
if (this.state.syncState === "ERROR") { if (this.state.syncState === "ERROR") {
return ( return (
<div className="mx_RoomStatusBar_connectionLostBar"> <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"> <div className="mx_RoomStatusBar_connectionLostBar_title">
{_t('Connectivity to the server has been lost.')} { _t('Connectivity to the server has been lost.') }
</div> </div>
<div className="mx_RoomStatusBar_connectionLostBar_desc"> <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>
</div> </div>
); );
@ -254,18 +267,18 @@ module.exports = React.createClass({
if (this.props.unsentMessageError) { if (this.props.unsentMessageError) {
return ( return (
<div className="mx_RoomStatusBar_connectionLostBar"> <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"> <div className="mx_RoomStatusBar_connectionLostBar_title">
{ this.props.unsentMessageError } { this.props.unsentMessageError }
</div> </div>
<div className="mx_RoomStatusBar_connectionLostBar_desc"> <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.", { _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>/], [/<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="resend" onClick={this.props.onResendAllClick}>{ sub }</a>,
(sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={ this.props.onCancelAllClick }>{sub}</a>, (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this.props.onCancelAllClick}>{ sub }</a>,
] ],
)} ) }
</div> </div>
</div> </div>
); );
@ -275,24 +288,24 @@ module.exports = React.createClass({
// set when you've scrolled up // set when you've scrolled up
if (this.props.numUnreadMessages) { if (this.props.numUnreadMessages) {
// MUST use var name "count" for pluralization to kick in // 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 ( return (
<div className="mx_RoomStatusBar_unreadMessagesBar" <div className="mx_RoomStatusBar_unreadMessagesBar"
onClick={ this.props.onScrollToBottomClick }> onClick={this.props.onScrollToBottomClick}>
{unreadMsgs} { unreadMsgs }
</div> </div>
); );
} }
const typingString = WhoIsTyping.whoIsTypingString( const typingString = WhoIsTyping.whoIsTypingString(
this.state.usersTyping, this.state.usersTyping,
this.props.whoIsTypingLimit this.props.whoIsTypingLimit,
); );
if (typingString) { if (typingString) {
return ( return (
<div className="mx_RoomStatusBar_typingBar"> <div className="mx_RoomStatusBar_typingBar">
<EmojiText>{typingString}</EmojiText> <EmojiText>{ typingString }</EmojiText>
</div> </div>
); );
} }
@ -300,7 +313,22 @@ module.exports = React.createClass({
if (this.props.hasActiveCall) { if (this.props.hasActiveCall) {
return ( return (
<div className="mx_RoomStatusBar_callBar"> <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">
{ _tJsx("There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?",
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
[
(sub) => <a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
(sub) => <a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
],
) }
</div> </div>
); );
} }
@ -310,15 +338,15 @@ module.exports = React.createClass({
render: function() { render: function() {
var content = this._getContent(); const content = this._getContent();
var indicator = this._getIndicator(this.state.usersTyping.length > 0); const indicator = this._getIndicator(this.state.usersTyping.length > 0);
return ( return (
<div className="mx_RoomStatusBar"> <div className="mx_RoomStatusBar">
<div className="mx_RoomStatusBar_indicator"> <div className="mx_RoomStatusBar_indicator">
{indicator} { indicator }
</div> </div>
{content} { content }
</div> </div>
); );
}, },

View file

@ -22,25 +22,25 @@ limitations under the License.
import shouldHideEvent from "../../shouldHideEvent"; import shouldHideEvent from "../../shouldHideEvent";
var React = require("react"); const React = require("react");
var ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import Promise from 'bluebird'; import Promise from 'bluebird';
var classNames = require("classnames"); const classNames = require("classnames");
var Matrix = require("matrix-js-sdk"); const Matrix = require("matrix-js-sdk");
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
var UserSettingsStore = require('../../UserSettingsStore'); const UserSettingsStore = require('../../UserSettingsStore');
var MatrixClientPeg = require("../../MatrixClientPeg"); const MatrixClientPeg = require("../../MatrixClientPeg");
var ContentMessages = require("../../ContentMessages"); const ContentMessages = require("../../ContentMessages");
var Modal = require("../../Modal"); const Modal = require("../../Modal");
var sdk = require('../../index'); const sdk = require('../../index');
var CallHandler = require('../../CallHandler'); const CallHandler = require('../../CallHandler');
var Resend = require("../../Resend"); const Resend = require("../../Resend");
var dis = require("../../dispatcher"); const dis = require("../../dispatcher");
var Tinter = require("../../Tinter"); const Tinter = require("../../Tinter");
var rate_limited_func = require('../../ratelimitedfunc'); const rate_limited_func = require('../../ratelimitedfunc');
var ObjectUtils = require('../../ObjectUtils'); const ObjectUtils = require('../../ObjectUtils');
var Rooms = require('../../Rooms'); const Rooms = require('../../Rooms');
import KeyCode from '../../KeyCode'; import KeyCode from '../../KeyCode';
@ -49,7 +49,7 @@ import UserProvider from '../../autocomplete/UserProvider';
import RoomViewStore from '../../stores/RoomViewStore'; import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
let DEBUG = false; const DEBUG = false;
let debuglog = function() {}; let debuglog = function() {};
const BROWSER_SUPPORTS_SANDBOX = 'sandbox' in document.createElement('iframe'); const BROWSER_SUPPORTS_SANDBOX = 'sandbox' in document.createElement('iframe');
@ -117,6 +117,8 @@ module.exports = React.createClass({
guestsCanJoin: false, guestsCanJoin: false,
canPeek: false, canPeek: false,
showApps: false, showApps: false,
isAlone: false,
isPeeking: false,
// error object, as from the matrix client/server API // error object, as from the matrix client/server API
// If we failed to load information about the room, // If we failed to load information about the room,
@ -266,6 +268,7 @@ module.exports = React.createClass({
console.log("Attempting to peek into room %s", roomId); console.log("Attempting to peek into room %s", roomId);
this.setState({ this.setState({
peekLoading: true, peekLoading: true,
isPeeking: true, // this will change to false if peeking fails
}); });
MatrixClientPeg.get().peekInRoom(roomId).then((room) => { MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
this.setState({ this.setState({
@ -274,6 +277,11 @@ module.exports = React.createClass({
}); });
this._onRoomLoaded(room); this._onRoomLoaded(room);
}, (err) => { }, (err) => {
// Stop peeking if anything went wrong
this.setState({
isPeeking: false,
});
// This won't necessarily be a MatrixError, but we duck-type // This won't necessarily be a MatrixError, but we duck-type
// here and say if it's got an 'errcode' key with the right value, // here and say if it's got an 'errcode' key with the right value,
// it means we can't peek. // it means we can't peek.
@ -290,6 +298,7 @@ module.exports = React.createClass({
} else if (room) { } else if (room) {
// Stop peeking because we have joined this room previously // Stop peeking because we have joined this room previously
MatrixClientPeg.get().stopPeeking(); MatrixClientPeg.get().stopPeeking();
this.setState({isPeeking: false});
} }
}, },
@ -307,10 +316,10 @@ module.exports = React.createClass({
}, },
componentDidMount: function() { componentDidMount: function() {
var call = this._getCallForRoom(); const call = this._getCallForRoom();
var callState = call ? call.call_state : "ended"; const callState = call ? call.call_state : "ended";
this.setState({ this.setState({
callState: callState callState: callState,
}); });
this._updateConfCallNotification(); this._updateConfCallNotification();
@ -327,9 +336,8 @@ module.exports = React.createClass({
this.state.room.getJoinedMembers().length == 1 && this.state.room.getJoinedMembers().length == 1 &&
this.state.room.getLiveTimeline() && this.state.room.getLiveTimeline() &&
this.state.room.getLiveTimeline().getEvents() && this.state.room.getLiveTimeline().getEvents() &&
this.state.room.getLiveTimeline().getEvents().length <= 6) this.state.room.getLiveTimeline().getEvents().length <= 6) {
{ const inviteBox = document.getElementById("mx_SearchableEntityList_query");
var inviteBox = document.getElementById("mx_SearchableEntityList_query");
setTimeout(function() { setTimeout(function() {
if (inviteBox) { if (inviteBox) {
inviteBox.focus(); inviteBox.focus();
@ -345,7 +353,7 @@ module.exports = React.createClass({
componentDidUpdate: function() { componentDidUpdate: function() {
if (this.refs.roomView) { if (this.refs.roomView) {
var roomView = ReactDOM.findDOMNode(this.refs.roomView); const roomView = ReactDOM.findDOMNode(this.refs.roomView);
if (!roomView.ondrop) { if (!roomView.ondrop) {
roomView.addEventListener('drop', this.onDrop); roomView.addEventListener('drop', this.onDrop);
roomView.addEventListener('dragover', this.onDragOver); roomView.addEventListener('dragover', this.onDragOver);
@ -372,7 +380,7 @@ module.exports = React.createClass({
// is really just for hygiene - we're going to be // is really just for hygiene - we're going to be
// deleted anyway, so it doesn't matter if the event listeners // deleted anyway, so it doesn't matter if the event listeners
// don't get cleaned up. // don't get cleaned up.
var roomView = ReactDOM.findDOMNode(this.refs.roomView); const roomView = ReactDOM.findDOMNode(this.refs.roomView);
roomView.removeEventListener('drop', this.onDrop); roomView.removeEventListener('drop', this.onDrop);
roomView.removeEventListener('dragover', this.onDragOver); roomView.removeEventListener('dragover', this.onDragOver);
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd); roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
@ -454,6 +462,8 @@ module.exports = React.createClass({
switch (payload.action) { switch (payload.action) {
case 'message_send_failed': case 'message_send_failed':
case 'message_sent': case 'message_sent':
this._checkIfAlone(this.state.room);
// no break; to intentionally fall through
case 'message_send_cancelled': case 'message_send_cancelled':
this.setState({ this.setState({
unsentMessageError: this._getUnsentMessageError(this.state.room), unsentMessageError: this._getUnsentMessageError(this.state.room),
@ -478,8 +488,7 @@ module.exports = React.createClass({
if (call) { if (call) {
callState = call.call_state; callState = call.call_state;
} } else {
else {
callState = "ended"; callState = "ended";
} }
@ -591,17 +600,17 @@ module.exports = React.createClass({
}, },
_calculatePeekRules: function(room) { _calculatePeekRules: function(room) {
var guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", ""); const guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", "");
if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") { if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") {
this.setState({ this.setState({
guestsCanJoin: true guestsCanJoin: true,
}); });
} }
var historyVisibility = room.currentState.getStateEvents("m.room.history_visibility", ""); const historyVisibility = room.currentState.getStateEvents("m.room.history_visibility", "");
if (historyVisibility && historyVisibility.getContent().history_visibility === "world_readable") { if (historyVisibility && historyVisibility.getContent().history_visibility === "world_readable") {
this.setState({ this.setState({
canPeek: true canPeek: true,
}); });
} }
}, },
@ -610,35 +619,35 @@ module.exports = React.createClass({
// console.log("_updatePreviewUrlVisibility"); // console.log("_updatePreviewUrlVisibility");
// check our per-room overrides // check our per-room overrides
var roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls"); const roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls");
if (roomPreviewUrls && roomPreviewUrls.getContent().disable !== undefined) { if (roomPreviewUrls && roomPreviewUrls.getContent().disable !== undefined) {
this.setState({ this.setState({
showUrlPreview: !roomPreviewUrls.getContent().disable showUrlPreview: !roomPreviewUrls.getContent().disable,
}); });
return; return;
} }
// check our global disable override // check our global disable override
var userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); const userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
if (userRoomPreviewUrls && userRoomPreviewUrls.getContent().disable) { if (userRoomPreviewUrls && userRoomPreviewUrls.getContent().disable) {
this.setState({ this.setState({
showUrlPreview: false showUrlPreview: false,
}); });
return; return;
} }
// check the room state event // check the room state event
var roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', ''); const roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
if (roomStatePreviewUrls && roomStatePreviewUrls.getContent().disable) { if (roomStatePreviewUrls && roomStatePreviewUrls.getContent().disable) {
this.setState({ this.setState({
showUrlPreview: false showUrlPreview: false,
}); });
return; return;
} }
// otherwise, we assume they're on. // otherwise, we assume they're on.
this.setState({ this.setState({
showUrlPreview: true showUrlPreview: true,
}); });
}, },
@ -654,11 +663,11 @@ module.exports = React.createClass({
}, },
updateTint: function() { updateTint: function() {
var room = this.state.room; const room = this.state.room;
if (!room) return; if (!room) return;
var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); const color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
var color_scheme = {}; let color_scheme = {};
if (color_scheme_event) { if (color_scheme_event) {
color_scheme = color_scheme_event.getContent(); color_scheme = color_scheme_event.getContent();
// XXX: we should validate the event // XXX: we should validate the event
@ -676,12 +685,11 @@ module.exports = React.createClass({
onRoomAccountData: function(event, room) { onRoomAccountData: function(event, room) {
if (room.roomId == this.state.roomId) { if (room.roomId == this.state.roomId) {
if (event.getType() === "org.matrix.room.color_scheme") { if (event.getType() === "org.matrix.room.color_scheme") {
var color_scheme = event.getContent(); const color_scheme = event.getContent();
// XXX: we should validate the event // XXX: we should validate the event
console.log("Tinter.tint from onRoomAccountData"); console.log("Tinter.tint from onRoomAccountData");
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
} } else if (event.getType() === "org.matrix.room.preview_urls") {
else if (event.getType() === "org.matrix.room.preview_urls") {
this._updatePreviewUrlVisibility(room); this._updatePreviewUrlVisibility(room);
} }
} }
@ -720,7 +728,7 @@ module.exports = React.createClass({
// if we are now a member of the room, where we were not before, that // if we are now a member of the room, where we were not before, that
// means we have finished joining a room we were previously peeking // means we have finished joining a room we were previously peeking
// into. // into.
var me = MatrixClientPeg.get().credentials.userId; const me = MatrixClientPeg.get().credentials.userId;
if (this.state.joining && this.state.room.hasMembershipState(me, "join")) { if (this.state.joining && this.state.room.hasMembershipState(me, "join")) {
// Having just joined a room, check to see if it looks like a DM room, and if so, // Having just joined a room, check to see if it looks like a DM room, and if so,
// mark it as one. This is to work around the fact that some clients don't support // mark it as one. This is to work around the fact that some clients don't support
@ -735,9 +743,34 @@ module.exports = React.createClass({
} }
}, 500), }, 500),
_checkIfAlone: function(room) {
let warnedAboutLonelyRoom = false;
if (localStorage) {
warnedAboutLonelyRoom = localStorage.getItem('mx_user_alone_warned_' + this.state.room.roomId);
}
if (warnedAboutLonelyRoom) {
if (this.state.isAlone) this.setState({isAlone: false});
return;
}
const joinedMembers = room.currentState.getMembers().filter(m => m.membership === "join" || m.membership === "invite");
this.setState({isAlone: joinedMembers.length === 1});
},
_getUnsentMessageError: function(room) { _getUnsentMessageError: function(room) {
const unsentMessages = this._getUnsentMessages(room); const unsentMessages = this._getUnsentMessages(room);
if (!unsentMessages.length) return ""; if (!unsentMessages.length) return "";
if (
unsentMessages.length === 1 &&
unsentMessages[0].error &&
unsentMessages[0].error.data &&
unsentMessages[0].error.data.error &&
unsentMessages[0].error.name !== "UnknownDeviceError"
) {
return unsentMessages[0].error.data.error;
}
for (const event of unsentMessages) { for (const event of unsentMessages) {
if (!event.error || event.error.name !== "UnknownDeviceError") { if (!event.error || event.error.name !== "UnknownDeviceError") {
return _t("Some of your messages have not been sent."); return _t("Some of your messages have not been sent.");
@ -754,18 +787,18 @@ module.exports = React.createClass({
}, },
_updateConfCallNotification: function() { _updateConfCallNotification: function() {
var room = this.state.room; const room = this.state.room;
if (!room || !this.props.ConferenceHandler) { if (!room || !this.props.ConferenceHandler) {
return; return;
} }
var confMember = room.getMember( const confMember = room.getMember(
this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId) this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId),
); );
if (!confMember) { if (!confMember) {
return; return;
} }
var confCall = this.props.ConferenceHandler.getConferenceCallForRoom(confMember.roomId); const confCall = this.props.ConferenceHandler.getConferenceCallForRoom(confMember.roomId);
// A conf call notification should be displayed if there is an ongoing // A conf call notification should be displayed if there is an ongoing
// conf call but this cilent isn't a part of it. // conf call but this cilent isn't a part of it.
@ -773,7 +806,7 @@ module.exports = React.createClass({
displayConfCallNotification: ( displayConfCallNotification: (
(!confCall || confCall.call_state === "ended") && (!confCall || confCall.call_state === "ended") &&
confMember.membership === "join" confMember.membership === "join"
) ),
}); });
}, },
@ -788,7 +821,7 @@ module.exports = React.createClass({
if (this.state.searchResults.next_batch) { if (this.state.searchResults.next_batch) {
debuglog("requesting more search results"); debuglog("requesting more search results");
var searchPromise = MatrixClientPeg.get().backPaginateRoomEventsSearch( const searchPromise = MatrixClientPeg.get().backPaginateRoomEventsSearch(
this.state.searchResults); this.state.searchResults);
return this._handleSearchResult(searchPromise); return this._handleSearchResult(searchPromise);
} else { } else {
@ -805,6 +838,22 @@ module.exports = React.createClass({
Resend.cancelUnsentEvents(this.state.room); Resend.cancelUnsentEvents(this.state.room);
}, },
onInviteButtonClick: function() {
// call AddressPickerDialog
dis.dispatch({
action: 'view_invite',
roomId: this.state.room.roomId,
});
this.setState({isAlone: false}); // there's a good chance they'll invite someone
},
onStopAloneWarningClick: function() {
if (localStorage) {
localStorage.setItem('mx_user_alone_warned_' + this.state.room.roomId, true);
}
this.setState({isAlone: false});
},
onJoinButtonClicked: function(ev) { onJoinButtonClicked: function(ev) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -883,8 +932,7 @@ module.exports = React.createClass({
numUnreadMessages: 0, numUnreadMessages: 0,
atEndOfLiveTimeline: true, atEndOfLiveTimeline: true,
}); });
} } else {
else {
this.setState({ this.setState({
atEndOfLiveTimeline: false, atEndOfLiveTimeline: false,
}); });
@ -898,10 +946,10 @@ module.exports = React.createClass({
ev.dataTransfer.dropEffect = 'none'; ev.dataTransfer.dropEffect = 'none';
var items = ev.dataTransfer.items; const items = ev.dataTransfer.items;
if (items.length == 1) { if (items.length == 1) {
if (items[0].kind == 'file') { if (items[0].kind == 'file') {
this.setState({ draggingFile : true }); this.setState({ draggingFile: true });
ev.dataTransfer.dropEffect = 'copy'; ev.dataTransfer.dropEffect = 'copy';
} }
} }
@ -910,8 +958,8 @@ module.exports = React.createClass({
onDrop: function(ev) { onDrop: function(ev) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.setState({ draggingFile : false }); this.setState({ draggingFile: false });
var files = ev.dataTransfer.files; const files = ev.dataTransfer.files;
if (files.length == 1) { if (files.length == 1) {
this.uploadFile(files[0]); this.uploadFile(files[0]);
} }
@ -920,7 +968,7 @@ module.exports = React.createClass({
onDragLeaveOrEnd: function(ev) { onDragLeaveOrEnd: function(ev) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.setState({ draggingFile : false }); this.setState({ draggingFile: false });
}, },
uploadFile: function(file) { uploadFile: function(file) {
@ -930,7 +978,7 @@ module.exports = React.createClass({
} }
ContentMessages.sendContentToRoom( ContentMessages.sendContentToRoom(
file, this.state.room.roomId, MatrixClientPeg.get() file, this.state.room.roomId, MatrixClientPeg.get(),
).done(undefined, (error) => { ).done(undefined, (error) => {
if (error.name === "UnknownDeviceError") { if (error.name === "UnknownDeviceError") {
dis.dispatch({ dis.dispatch({
@ -969,19 +1017,19 @@ module.exports = React.createClass({
// todo: should cancel any previous search requests. // todo: should cancel any previous search requests.
this.searchId = new Date().getTime(); this.searchId = new Date().getTime();
var filter; let filter;
if (scope === "Room") { if (scope === "Room") {
filter = { filter = {
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :(
rooms: [ rooms: [
this.state.room.roomId this.state.room.roomId,
] ],
}; };
} }
debuglog("sending search request"); debuglog("sending search request");
var searchPromise = MatrixClientPeg.get().searchRoomEvents({ const searchPromise = MatrixClientPeg.get().searchRoomEvents({
filter: filter, filter: filter,
term: term, term: term,
}); });
@ -989,11 +1037,11 @@ module.exports = React.createClass({
}, },
_handleSearchResult: function(searchPromise) { _handleSearchResult: function(searchPromise) {
var self = this; const self = this;
// keep a record of the current search id, so that if the search terms // keep a record of the current search id, so that if the search terms
// change before we get a response, we can ignore the results. // change before we get a response, we can ignore the results.
var localSearchId = this.searchId; const localSearchId = this.searchId;
this.setState({ this.setState({
searchInProgress: true, searchInProgress: true,
@ -1012,7 +1060,7 @@ module.exports = React.createClass({
// In either case, we want to highlight the literal search term // In either case, we want to highlight the literal search term
// whether it was used by the search engine or not. // whether it was used by the search engine or not.
var highlights = results.highlights; let highlights = results.highlights;
if (highlights.indexOf(self.state.searchTerm) < 0) { if (highlights.indexOf(self.state.searchTerm) < 0) {
highlights = highlights.concat(self.state.searchTerm); highlights = highlights.concat(self.state.searchTerm);
} }
@ -1020,14 +1068,15 @@ module.exports = React.createClass({
// For overlapping highlights, // For overlapping highlights,
// favour longer (more specific) terms first // favour longer (more specific) terms first
highlights = highlights.sort(function(a, b) { highlights = highlights.sort(function(a, b) {
return b.length - a.length; }); return b.length - a.length;
});
self.setState({ self.setState({
searchHighlights: highlights, searchHighlights: highlights,
searchResults: results, searchResults: results,
}); });
}, function(error) { }, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Search failed: " + error); console.error("Search failed: " + error);
Modal.createTrackedDialog('Search failed', '', ErrorDialog, { Modal.createTrackedDialog('Search failed', '', ErrorDialog, {
title: _t("Search failed"), title: _t("Search failed"),
@ -1035,17 +1084,17 @@ module.exports = React.createClass({
}); });
}).finally(function() { }).finally(function() {
self.setState({ self.setState({
searchInProgress: false searchInProgress: false,
}); });
}); });
}, },
getSearchResultTiles: function() { getSearchResultTiles: function() {
var EventTile = sdk.getComponent('rooms.EventTile'); const EventTile = sdk.getComponent('rooms.EventTile');
var SearchResultTile = sdk.getComponent('rooms.SearchResultTile'); const SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
var Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
var cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
// XXX: todo: merge overlapping results somehow? // XXX: todo: merge overlapping results somehow?
// XXX: why doesn't searching on name work? // XXX: why doesn't searching on name work?
@ -1055,7 +1104,7 @@ module.exports = React.createClass({
return []; return [];
} }
var ret = []; const ret = [];
if (this.state.searchInProgress) { if (this.state.searchInProgress) {
ret.push(<li key="search-spinner"> ret.push(<li key="search-spinner">
@ -1067,32 +1116,32 @@ module.exports = React.createClass({
if (this.state.searchResults.results.length == 0) { if (this.state.searchResults.results.length == 0) {
ret.push(<li key="search-top-marker"> ret.push(<li key="search-top-marker">
<h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2> <h2 className="mx_RoomView_topMarker">{ _t("No results") }</h2>
</li> </li>,
); );
} else { } else {
ret.push(<li key="search-top-marker"> ret.push(<li key="search-top-marker">
<h2 className="mx_RoomView_topMarker">{ _t("No more results") }</h2> <h2 className="mx_RoomView_topMarker">{ _t("No more results") }</h2>
</li> </li>,
); );
} }
} }
// once dynamic content in the search results load, make the scrollPanel check // once dynamic content in the search results load, make the scrollPanel check
// the scroll offsets. // the scroll offsets.
var onWidgetLoad = () => { const onWidgetLoad = () => {
var scrollPanel = this.refs.searchResultsPanel; const scrollPanel = this.refs.searchResultsPanel;
if (scrollPanel) { if (scrollPanel) {
scrollPanel.checkScroll(); scrollPanel.checkScroll();
} }
}; };
var lastRoomId; let lastRoomId;
for (var i = this.state.searchResults.results.length - 1; i >= 0; i--) { for (let i = this.state.searchResults.results.length - 1; i >= 0; i--) {
var result = this.state.searchResults.results[i]; const result = this.state.searchResults.results[i];
var mxEv = result.context.getEvent(); const mxEv = result.context.getEvent();
var roomId = mxEv.getRoomId(); const roomId = mxEv.getRoomId();
if (!EventTile.haveTileForEvent(mxEv)) { if (!EventTile.haveTileForEvent(mxEv)) {
// XXX: can this ever happen? It will make the result count // XXX: can this ever happen? It will make the result count
@ -1102,13 +1151,13 @@ module.exports = React.createClass({
if (this.state.searchScope === 'All') { if (this.state.searchScope === 'All') {
if(roomId != lastRoomId) { if(roomId != lastRoomId) {
var room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
// XXX: if we've left the room, we might not know about // XXX: if we've left the room, we might not know about
// it. We should tell the js sdk to go and find out about // it. We should tell the js sdk to go and find out about
// it. But that's not an issue currently, as synapse only // it. But that's not an issue currently, as synapse only
// returns results for rooms we're joined to. // returns results for rooms we're joined to.
var roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId }); const roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId });
ret.push(<li key={mxEv.getId() + "-room"}> ret.push(<li key={mxEv.getId() + "-room"}>
<h1>{ _t("Room") }: { roomName }</h1> <h1>{ _t("Room") }: { roomName }</h1>
@ -1117,13 +1166,13 @@ module.exports = React.createClass({
} }
} }
var resultLink = "#/room/"+roomId+"/"+mxEv.getId(); const resultLink = "#/room/"+roomId+"/"+mxEv.getId();
ret.push(<SearchResultTile key={mxEv.getId()} ret.push(<SearchResultTile key={mxEv.getId()}
searchResult={result} searchResult={result}
searchHighlights={this.state.searchHighlights} searchHighlights={this.state.searchHighlights}
resultLink={resultLink} resultLink={resultLink}
onWidgetLoad={onWidgetLoad}/>); onWidgetLoad={onWidgetLoad} />);
} }
return ret; return ret;
}, },
@ -1143,38 +1192,37 @@ module.exports = React.createClass({
uploadingRoomSettings: true, uploadingRoomSettings: true,
}); });
var newName = this.refs.header.getEditedName(); const newName = this.refs.header.getEditedName();
if (newName !== undefined) { if (newName !== undefined) {
this.refs.room_settings.setName(newName); this.refs.room_settings.setName(newName);
} }
var newTopic = this.refs.header.getEditedTopic(); const newTopic = this.refs.header.getEditedTopic();
if (newTopic !== undefined) { if (newTopic !== undefined) {
this.refs.room_settings.setTopic(newTopic); this.refs.room_settings.setTopic(newTopic);
} }
this.refs.room_settings.save().then((results) => { this.refs.room_settings.save().then((results) => {
var fails = results.filter(function(result) { return result.state !== "fulfilled"; }); const fails = results.filter(function(result) { return result.state !== "fulfilled"; });
console.log("Settings saved with %s errors", fails.length); console.log("Settings saved with %s errors", fails.length);
if (fails.length) { if (fails.length) {
fails.forEach(function(result) { fails.forEach(function(result) {
console.error(result.reason); console.error(result.reason);
}); });
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to save room settings', '', ErrorDialog, { Modal.createTrackedDialog('Failed to save room settings', '', ErrorDialog, {
title: _t("Failed to save settings"), title: _t("Failed to save settings"),
description: fails.map(function(result) { return result.reason; }).join("\n"), description: fails.map(function(result) { return result.reason; }).join("\n"),
}); });
// still editing room settings // still editing room settings
} } else {
else {
this.setState({ this.setState({
editingRoomSettings: false editingRoomSettings: false,
}); });
} }
}).finally(() => { }).finally(() => {
this.setState({ this.setState({
uploadingRoomSettings: false, uploadingRoomSettings: false,
editingRoomSettings: false editingRoomSettings: false,
}); });
}).done(); }).done();
}, },
@ -1205,8 +1253,8 @@ module.exports = React.createClass({
MatrixClientPeg.get().forget(this.state.room.roomId).done(function() { MatrixClientPeg.get().forget(this.state.room.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' }); dis.dispatch({ action: 'view_next_room' });
}, function(err) { }, function(err) {
var errCode = err.errcode || _t("unknown error code"); const errCode = err.errcode || _t("unknown error code");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, { Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
title: _t("Error"), title: _t("Error"),
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }), description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
@ -1215,20 +1263,20 @@ module.exports = React.createClass({
}, },
onRejectButtonClicked: function(ev) { onRejectButtonClicked: function(ev) {
var self = this; const self = this;
this.setState({ this.setState({
rejecting: true rejecting: true,
}); });
MatrixClientPeg.get().leave(this.state.roomId).done(function() { MatrixClientPeg.get().leave(this.state.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' }); dis.dispatch({ action: 'view_next_room' });
self.setState({ self.setState({
rejecting: false rejecting: false,
}); });
}, function(error) { }, function(error) {
console.error("Failed to reject invite: %s", error); console.error("Failed to reject invite: %s", error);
var msg = error.message ? error.message : JSON.stringify(error); const msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, { Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
title: _t("Failed to reject invite"), title: _t("Failed to reject invite"),
description: msg, description: msg,
@ -1236,7 +1284,7 @@ module.exports = React.createClass({
self.setState({ self.setState({
rejecting: false, rejecting: false,
rejectError: error rejectError: error,
}); });
}); });
}, },
@ -1296,7 +1344,7 @@ module.exports = React.createClass({
// restored when we switch back to it. // restored when we switch back to it.
// //
_getScrollState: function() { _getScrollState: function() {
var messagePanel = this.refs.messagePanel; const messagePanel = this.refs.messagePanel;
if (!messagePanel) return null; if (!messagePanel) return null;
// if we're following the live timeline, we want to return null; that // if we're following the live timeline, we want to return null; that
@ -1311,7 +1359,7 @@ module.exports = React.createClass({
return null; return null;
} }
var scrollState = messagePanel.getScrollState(); const scrollState = messagePanel.getScrollState();
if (scrollState.stuckAtBottom) { if (scrollState.stuckAtBottom) {
// we don't really expect to be in this state, but it will // we don't really expect to be in this state, but it will
@ -1338,7 +1386,7 @@ module.exports = React.createClass({
// a maxHeight on the underlying remote video tag. // a maxHeight on the underlying remote video tag.
// header + footer + status + give us at least 120px of scrollback at all times. // header + footer + status + give us at least 120px of scrollback at all times.
var auxPanelMaxHeight = window.innerHeight - let auxPanelMaxHeight = window.innerHeight -
(83 + // height of RoomHeader (83 + // height of RoomHeader
36 + // height of the status area 36 + // height of the status area
72 + // minimum height of the message compmoser 72 + // minimum height of the message compmoser
@ -1357,26 +1405,26 @@ module.exports = React.createClass({
onFullscreenClick: function() { onFullscreenClick: function() {
dis.dispatch({ dis.dispatch({
action: 'video_fullscreen', action: 'video_fullscreen',
fullscreen: true fullscreen: true,
}, true); }, true);
}, },
onMuteAudioClick: function() { onMuteAudioClick: function() {
var call = this._getCallForRoom(); const call = this._getCallForRoom();
if (!call) { if (!call) {
return; return;
} }
var newState = !call.isMicrophoneMuted(); const newState = !call.isMicrophoneMuted();
call.setMicrophoneMuted(newState); call.setMicrophoneMuted(newState);
this.forceUpdate(); // TODO: just update the voip buttons this.forceUpdate(); // TODO: just update the voip buttons
}, },
onMuteVideoClick: function() { onMuteVideoClick: function() {
var call = this._getCallForRoom(); const call = this._getCallForRoom();
if (!call) { if (!call) {
return; return;
} }
var newState = !call.isLocalVideoMuted(); const newState = !call.isLocalVideoMuted();
call.setLocalVideoMuted(newState); call.setLocalVideoMuted(newState);
this.forceUpdate(); // TODO: just update the voip buttons this.forceUpdate(); // TODO: just update the voip buttons
}, },
@ -1412,7 +1460,7 @@ module.exports = React.createClass({
* We pass it down to the scroll panel. * We pass it down to the scroll panel.
*/ */
handleScrollKey: function(ev) { handleScrollKey: function(ev) {
var panel; let panel;
if(this.refs.searchResultsPanel) { if(this.refs.searchResultsPanel) {
panel = this.refs.searchResultsPanel; panel = this.refs.searchResultsPanel;
} else if(this.refs.messagePanel) { } else if(this.refs.messagePanel) {
@ -1483,13 +1531,13 @@ module.exports = React.createClass({
<RoomHeader ref="header" <RoomHeader ref="header"
room={this.state.room} room={this.state.room}
oobData={this.props.oobData} oobData={this.props.oobData}
collapsedRhs={ this.props.collapsedRhs } collapsedRhs={this.props.collapsedRhs}
/> />
<div className="mx_RoomView_auxPanel"> <div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked } <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={ this.onForgetClick } onForgetClick={this.onForgetClick}
onRejectClick={ this.onRejectThreepidInviteButtonClicked } onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={ false } error={ this.state.roomLoadError } canPreview={false} error={this.state.roomLoadError}
roomAlias={roomAlias} roomAlias={roomAlias}
spinner={this.state.joining} spinner={this.state.joining}
inviterName={inviterName} inviterName={inviterName}
@ -1503,8 +1551,8 @@ module.exports = React.createClass({
} }
} }
var myUserId = MatrixClientPeg.get().credentials.userId; const myUserId = MatrixClientPeg.get().credentials.userId;
var myMember = this.state.room.getMember(myUserId); const myMember = this.state.room.getMember(myUserId);
if (myMember && myMember.membership == 'invite') { if (myMember && myMember.membership == 'invite') {
if (this.state.joining || this.state.rejecting) { if (this.state.joining || this.state.rejecting) {
return ( return (
@ -1513,7 +1561,7 @@ module.exports = React.createClass({
</div> </div>
); );
} else { } else {
var inviteEvent = myMember.events.member; const inviteEvent = myMember.events.member;
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender(); var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
// We deliberately don't try to peek into invites, even if we have permission to peek // We deliberately don't try to peek into invites, even if we have permission to peek
@ -1526,14 +1574,14 @@ module.exports = React.createClass({
<RoomHeader <RoomHeader
ref="header" ref="header"
room={this.state.room} room={this.state.room}
collapsedRhs={ this.props.collapsedRhs } collapsedRhs={this.props.collapsedRhs}
/> />
<div className="mx_RoomView_auxPanel"> <div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked } <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={ this.onForgetClick } onForgetClick={this.onForgetClick}
onRejectClick={ this.onRejectButtonClicked } onRejectClick={this.onRejectButtonClicked}
inviterName={ inviterName } inviterName={inviterName}
canPreview={ false } canPreview={false}
spinner={this.state.joining} spinner={this.state.joining}
room={this.state.room} room={this.state.room}
/> />
@ -1547,33 +1595,36 @@ module.exports = React.createClass({
// We have successfully loaded this room, and are not previewing. // We have successfully loaded this room, and are not previewing.
// Display the "normal" room view. // Display the "normal" room view.
var call = this._getCallForRoom(); const call = this._getCallForRoom();
var inCall = false; let inCall = false;
if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) { if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) {
inCall = true; inCall = true;
} }
var scrollheader_classes = classNames({ const scrollheader_classes = classNames({
mx_RoomView_scrollheader: true, mx_RoomView_scrollheader: true,
}); });
var statusBar; let statusBar;
let isStatusAreaExpanded = true; let isStatusAreaExpanded = true;
if (ContentMessages.getCurrentUploads().length > 0) { if (ContentMessages.getCurrentUploads().length > 0) {
var UploadBar = sdk.getComponent('structures.UploadBar'); const UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = <UploadBar room={this.state.room} />; statusBar = <UploadBar room={this.state.room} />;
} else if (!this.state.searchResults) { } else if (!this.state.searchResults) {
var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar'); const RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
isStatusAreaExpanded = this.state.statusBarVisible; isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = <RoomStatusBar statusBar = <RoomStatusBar
room={this.state.room} room={this.state.room}
numUnreadMessages={this.state.numUnreadMessages} numUnreadMessages={this.state.numUnreadMessages}
unsentMessageError={this.state.unsentMessageError} unsentMessageError={this.state.unsentMessageError}
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline} atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
sentMessageAndIsAlone={this.state.isAlone}
hasActiveCall={inCall} hasActiveCall={inCall}
onResendAllClick={this.onResendAllClick} onResendAllClick={this.onResendAllClick}
onCancelAllClick={this.onCancelAllClick} onCancelAllClick={this.onCancelAllClick}
onInviteClick={this.onInviteButtonClick}
onStopWarningClick={this.onStopAloneWarningClick}
onScrollToBottomClick={this.jumpToLiveTimeline} onScrollToBottomClick={this.jumpToLiveTimeline}
onResize={this.onChildResize} onResize={this.onChildResize}
onVisible={this.onStatusBarVisible} onVisible={this.onStatusBarVisible}
@ -1587,13 +1638,12 @@ module.exports = React.createClass({
if (this.state.editingRoomSettings) { if (this.state.editingRoomSettings) {
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSettingsSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />; aux = <RoomSettings ref="room_settings" onSaveClick={this.onSettingsSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />;
} else if (this.state.uploadingRoomSettings) { } else if (this.state.uploadingRoomSettings) {
aux = <Loader/>; aux = <Loader />;
} else if (this.state.forwardingEvent !== null) { } else if (this.state.forwardingEvent !== null) {
aux = <ForwardMessage onCancelClick={this.onCancelClick} />; aux = <ForwardMessage onCancelClick={this.onCancelClick} />;
} else if (this.state.searching) { } else if (this.state.searching) {
hideCancel = true; // has own cancel hideCancel = true; // has own cancel
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress} aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>;
} else if (this.state.showingPinned) { } else if (this.state.showingPinned) {
hideCancel = true; // has own cancel hideCancel = true; // has own cancel
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />; aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
@ -1611,7 +1661,7 @@ module.exports = React.createClass({
hideCancel = true; hideCancel = true;
aux = ( aux = (
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} <RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={ this.onForgetClick } onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked} onRejectClick={this.onRejectThreepidInviteButtonClicked}
spinner={this.state.joining} spinner={this.state.joining}
inviterName={inviterName} inviterName={inviterName}
@ -1622,7 +1672,7 @@ module.exports = React.createClass({
); );
} }
var auxPanel = ( const auxPanel = (
<AuxPanel ref="auxPanel" room={this.state.room} <AuxPanel ref="auxPanel" room={this.state.room}
userId={MatrixClientPeg.get().credentials.userId} userId={MatrixClientPeg.get().credentials.userId}
conferenceHandler={this.props.ConferenceHandler} conferenceHandler={this.props.ConferenceHandler}
@ -1635,8 +1685,8 @@ module.exports = React.createClass({
</AuxPanel> </AuxPanel>
); );
var messageComposer, searchInfo; let messageComposer, searchInfo;
var canSpeak = ( const canSpeak = (
// joined and not showing search results // joined and not showing search results
myMember && (myMember.membership == 'join') && !this.state.searchResults myMember && (myMember.membership == 'join') && !this.state.searchResults
); );
@ -1647,8 +1697,8 @@ module.exports = React.createClass({
onResize={this.onChildResize} onResize={this.onChildResize}
uploadFile={this.uploadFile} uploadFile={this.uploadFile}
callState={this.state.callState} callState={this.state.callState}
opacity={ this.props.opacity } opacity={this.props.opacity}
showApps={ this.state.showApps } showApps={this.state.showApps}
/>; />;
} }
@ -1656,19 +1706,19 @@ module.exports = React.createClass({
// in this.state if this is what RoomHeader desires? // in this.state if this is what RoomHeader desires?
if (this.state.searchResults) { if (this.state.searchResults) {
searchInfo = { searchInfo = {
searchTerm : this.state.searchTerm, searchTerm: this.state.searchTerm,
searchScope : this.state.searchScope, searchScope: this.state.searchScope,
searchCount : this.state.searchResults.count, searchCount: this.state.searchResults.count,
}; };
} }
if (inCall) { if (inCall) {
var zoomButton, voiceMuteButton, videoMuteButton; let zoomButton, voiceMuteButton, videoMuteButton;
if (call.type === "video") { if (call.type === "video") {
zoomButton = ( zoomButton = (
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={ _t("Fill screen") }> <div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={_t("Fill screen")}>
<TintableSvg src="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/> <TintableSvg src="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }} />
</div> </div>
); );
@ -1676,14 +1726,14 @@ module.exports = React.createClass({
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}> <div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
<TintableSvg src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"} <TintableSvg src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
alt={call.isLocalVideoMuted() ? _t("Click to unmute video") : _t("Click to mute video")} alt={call.isLocalVideoMuted() ? _t("Click to unmute video") : _t("Click to mute video")}
width="31" height="27"/> width="31" height="27" />
</div>; </div>;
} }
voiceMuteButton = voiceMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}> <div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
<TintableSvg src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"} <TintableSvg src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
alt={call.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")} alt={call.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")}
width="21" height="26"/> width="21" height="26" />
</div>; </div>;
// wrap the existing status bar into a 'callStatusBar' which adds more knobs. // wrap the existing status bar into a 'callStatusBar' which adds more knobs.
@ -1693,25 +1743,25 @@ module.exports = React.createClass({
{ videoMuteButton } { videoMuteButton }
{ zoomButton } { zoomButton }
{ statusBar } { statusBar }
<TintableSvg className="mx_RoomView_voipChevron" src="img/voip-chevron.svg" width="22" height="17"/> <TintableSvg className="mx_RoomView_voipChevron" src="img/voip-chevron.svg" width="22" height="17" />
</div>; </div>;
} }
// if we have search results, we keep the messagepanel (so that it preserves its // if we have search results, we keep the messagepanel (so that it preserves its
// scroll state), but hide it. // scroll state), but hide it.
var searchResultsPanel; let searchResultsPanel;
var hideMessagePanel = false; let hideMessagePanel = false;
if (this.state.searchResults) { if (this.state.searchResults) {
searchResultsPanel = ( searchResultsPanel = (
<ScrollPanel ref="searchResultsPanel" <ScrollPanel ref="searchResultsPanel"
className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel" className="mx_RoomView_messagePanel mx_RoomView_searchResultsPanel"
onFillRequest={ this.onSearchResultsFillRequest } onFillRequest={this.onSearchResultsFillRequest}
onResize={ this.onSearchResultsResize } onResize={this.onSearchResultsResize}
style={{ opacity: this.props.opacity }} style={{ opacity: this.props.opacity }}
> >
<li className={scrollheader_classes}></li> <li className={scrollheader_classes}></li>
{this.getSearchResultTiles()} { this.getSearchResultTiles() }
</ScrollPanel> </ScrollPanel>
); );
hideMessagePanel = true; hideMessagePanel = true;
@ -1726,26 +1776,26 @@ module.exports = React.createClass({
} }
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); // console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
var messagePanel = ( const messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef} <TimelinePanel ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()} timelineSet={this.state.room.getUnfilteredTimelineSet()}
showReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)} showReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
manageReadReceipts={true} manageReadReceipts={!this.state.isPeeking}
manageReadMarkers={true} manageReadMarkers={!this.state.isPeeking}
hidden={hideMessagePanel} hidden={hideMessagePanel}
highlightedEventId={highlightedEventId} highlightedEventId={highlightedEventId}
eventId={this.state.initialEventId} eventId={this.state.initialEventId}
eventPixelOffset={this.state.initialEventPixelOffset} eventPixelOffset={this.state.initialEventPixelOffset}
onScroll={ this.onMessageListScroll } onScroll={this.onMessageListScroll}
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar } onReadMarkerUpdated={this._updateTopUnreadMessagesBar}
showUrlPreview = { this.state.showUrlPreview } showUrlPreview = {this.state.showUrlPreview}
opacity={ this.props.opacity } opacity={this.props.opacity}
className="mx_RoomView_messagePanel" className="mx_RoomView_messagePanel"
/>); />);
var topUnreadMessagesBar = null; let topUnreadMessagesBar = null;
if (this.state.showTopUnreadMessagesBar) { if (this.state.showTopUnreadMessagesBar) {
var TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar'); const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
topUnreadMessagesBar = ( topUnreadMessagesBar = (
<div className="mx_RoomView_topUnreadMessagesBar mx_fadable" style={{ opacity: this.props.opacity }}> <div className="mx_RoomView_topUnreadMessagesBar mx_fadable" style={{ opacity: this.props.opacity }}>
<TopUnreadMessagesBar <TopUnreadMessagesBar
@ -1761,13 +1811,13 @@ module.exports = React.createClass({
} }
return ( return (
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView"> <div className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo} <RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
oobData={this.props.oobData} oobData={this.props.oobData}
editing={this.state.editingRoomSettings} editing={this.state.editingRoomSettings}
saving={this.state.uploadingRoomSettings} saving={this.state.uploadingRoomSettings}
inRoom={myMember && myMember.membership === 'join'} inRoom={myMember && myMember.membership === 'join'}
collapsedRhs={ this.props.collapsedRhs } collapsedRhs={this.props.collapsedRhs}
onSearchClick={this.onSearchClick} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick} onPinnedClick={this.onPinnedClick}

View file

@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require("react"); const React = require("react");
var ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
var GeminiScrollbar = require('react-gemini-scrollbar'); const GeminiScrollbar = require('react-gemini-scrollbar');
import Promise from 'bluebird'; import Promise from 'bluebird';
var KeyCode = require('../../KeyCode'); const KeyCode = require('../../KeyCode');
var DEBUG_SCROLL = false; const DEBUG_SCROLL = false;
// var DEBUG_SCROLL = true; // var DEBUG_SCROLL = true;
// The amount of extra scroll distance to allow prior to unfilling. // The amount of extra scroll distance to allow prior to unfilling.
@ -178,7 +178,7 @@ module.exports = React.createClass({
}, },
onScroll: function(ev) { onScroll: function(ev) {
var sn = this._getScrollNode(); const sn = this._getScrollNode();
debuglog("Scroll event: offset now:", sn.scrollTop, debuglog("Scroll event: offset now:", sn.scrollTop,
"_lastSetScroll:", this._lastSetScroll); "_lastSetScroll:", this._lastSetScroll);
@ -238,7 +238,7 @@ module.exports = React.createClass({
// about whether the the content is scrolled down right now, irrespective of // about whether the the content is scrolled down right now, irrespective of
// whether it will stay that way when the children update. // whether it will stay that way when the children update.
isAtBottom: function() { isAtBottom: function() {
var sn = this._getScrollNode(); const sn = this._getScrollNode();
// there seems to be some bug with flexbox/gemini/chrome/richvdh's // there seems to be some bug with flexbox/gemini/chrome/richvdh's
// understanding of the box model, wherein the scrollNode ends up 2 // understanding of the box model, wherein the scrollNode ends up 2
@ -281,7 +281,7 @@ module.exports = React.createClass({
// |#########| | // |#########| |
// `---------' - // `---------' -
_getExcessHeight: function(backwards) { _getExcessHeight: function(backwards) {
var sn = this._getScrollNode(); const sn = this._getScrollNode();
if (backwards) { if (backwards) {
return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING; return sn.scrollTop - sn.clientHeight - UNPAGINATION_PADDING;
} else { } else {
@ -295,7 +295,7 @@ module.exports = React.createClass({
return; return;
} }
var sn = this._getScrollNode(); const sn = this._getScrollNode();
// if there is less than a screenful of messages above or below the // if there is less than a screenful of messages above or below the
// viewport, try to get some more messages. // viewport, try to get some more messages.
@ -377,7 +377,7 @@ module.exports = React.createClass({
// check if there is already a pending fill request. If not, set one off. // check if there is already a pending fill request. If not, set one off.
_maybeFill: function(backwards) { _maybeFill: function(backwards) {
var dir = backwards ? 'b' : 'f'; const dir = backwards ? 'b' : 'f';
if (this._pendingFillRequests[dir]) { if (this._pendingFillRequests[dir]) {
debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another"); debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another");
return; return;
@ -470,8 +470,8 @@ module.exports = React.createClass({
* mult: -1 to page up, +1 to page down * mult: -1 to page up, +1 to page down
*/ */
scrollRelative: function(mult) { scrollRelative: function(mult) {
var scrollNode = this._getScrollNode(); const scrollNode = this._getScrollNode();
var delta = mult * scrollNode.clientHeight * 0.5; const delta = mult * scrollNode.clientHeight * 0.5;
this._setScrollTop(scrollNode.scrollTop + delta); this._setScrollTop(scrollNode.scrollTop + delta);
this._saveScrollState(); this._saveScrollState();
}, },
@ -535,7 +535,7 @@ module.exports = React.createClass({
this.scrollState = { this.scrollState = {
stuckAtBottom: false, stuckAtBottom: false,
trackedScrollToken: scrollToken, trackedScrollToken: scrollToken,
pixelOffset: pixelOffset pixelOffset: pixelOffset,
}; };
// ... then make it so. // ... then make it so.
@ -546,10 +546,10 @@ module.exports = React.createClass({
// given offset in the window. A helper for _restoreSavedScrollState. // given offset in the window. A helper for _restoreSavedScrollState.
_scrollToToken: function(scrollToken, pixelOffset) { _scrollToToken: function(scrollToken, pixelOffset) {
/* find the dom node with the right scrolltoken */ /* find the dom node with the right scrolltoken */
var node; let node;
var messages = this.refs.itemlist.children; const messages = this.refs.itemlist.children;
for (var i = messages.length-1; i >= 0; --i) { for (let i = messages.length-1; i >= 0; --i) {
var m = messages[i]; const m = messages[i];
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
// There might only be one scroll token // There might only be one scroll token
if (m.dataset.scrollTokens && if (m.dataset.scrollTokens &&
@ -564,10 +564,10 @@ module.exports = React.createClass({
return; return;
} }
var scrollNode = this._getScrollNode(); const scrollNode = this._getScrollNode();
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect(); const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
var boundingRect = node.getBoundingClientRect(); const boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom; const scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" + debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")"); pixelOffset + " (delta: "+scrollDelta+")");
@ -575,7 +575,6 @@ module.exports = React.createClass({
if(scrollDelta != 0) { if(scrollDelta != 0) {
this._setScrollTop(scrollNode.scrollTop + scrollDelta); this._setScrollTop(scrollNode.scrollTop + scrollDelta);
} }
}, },
_saveScrollState: function() { _saveScrollState: function() {
@ -585,16 +584,16 @@ module.exports = React.createClass({
return; return;
} }
var itemlist = this.refs.itemlist; const itemlist = this.refs.itemlist;
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect(); const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
var messages = itemlist.children; const messages = itemlist.children;
let newScrollState = null; let newScrollState = null;
for (var i = messages.length-1; i >= 0; --i) { for (let i = messages.length-1; i >= 0; --i) {
var node = messages[i]; const node = messages[i];
if (!node.dataset.scrollTokens) continue; if (!node.dataset.scrollTokens) continue;
var boundingRect = node.getBoundingClientRect(); const boundingRect = node.getBoundingClientRect();
newScrollState = { newScrollState = {
stuckAtBottom: false, stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollTokens.split(',')[0], trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
@ -619,8 +618,8 @@ module.exports = React.createClass({
}, },
_restoreSavedScrollState: function() { _restoreSavedScrollState: function() {
var scrollState = this.scrollState; const scrollState = this.scrollState;
var scrollNode = this._getScrollNode(); const scrollNode = this._getScrollNode();
if (scrollState.stuckAtBottom) { if (scrollState.stuckAtBottom) {
this._setScrollTop(Number.MAX_VALUE); this._setScrollTop(Number.MAX_VALUE);
@ -631,9 +630,9 @@ module.exports = React.createClass({
}, },
_setScrollTop: function(scrollTop) { _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 // FF ignores attempts to set scrollTop to very large numbers
scrollNode.scrollTop = Math.min(scrollTop, scrollNode.scrollHeight); scrollNode.scrollTop = Math.min(scrollTop, scrollNode.scrollHeight);
@ -676,7 +675,7 @@ module.exports = React.createClass({
className={this.props.className} style={this.props.style}> className={this.props.className} style={this.props.style}>
<div className="mx_RoomView_messageListWrapper"> <div className="mx_RoomView_messageListWrapper">
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite"> <ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
{this.props.children} { this.props.children }
</ol> </ol>
</div> </div>
</GeminiScrollbar> </GeminiScrollbar>

View file

@ -15,27 +15,27 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); const React = require('react');
var ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import Promise from 'bluebird'; import Promise from 'bluebird';
var Matrix = require("matrix-js-sdk"); const Matrix = require("matrix-js-sdk");
var EventTimeline = Matrix.EventTimeline; const EventTimeline = Matrix.EventTimeline;
var sdk = require('../../index'); const sdk = require('../../index');
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
var MatrixClientPeg = require("../../MatrixClientPeg"); const MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher"); const dis = require("../../dispatcher");
var ObjectUtils = require('../../ObjectUtils'); const ObjectUtils = require('../../ObjectUtils');
var Modal = require("../../Modal"); const Modal = require("../../Modal");
var UserActivity = require("../../UserActivity"); const UserActivity = require("../../UserActivity");
var KeyCode = require('../../KeyCode'); const KeyCode = require('../../KeyCode');
import UserSettingsStore from '../../UserSettingsStore'; import UserSettingsStore from '../../UserSettingsStore';
var PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
var INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
var DEBUG = false; const DEBUG = false;
if (DEBUG) { if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console // using bind means that we get to keep useful line numbers in the console
@ -260,7 +260,7 @@ var TimelinePanel = React.createClass({
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
var client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
if (client) { if (client) {
client.removeListener("Room.timeline", this.onRoomTimeline); client.removeListener("Room.timeline", this.onRoomTimeline);
client.removeListener("Room.timelineReset", this.onRoomTimelineReset); client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
@ -275,20 +275,20 @@ var TimelinePanel = React.createClass({
onMessageListUnfillRequest: function(backwards, scrollToken) { onMessageListUnfillRequest: function(backwards, scrollToken) {
// If backwards, unpaginate from the back (i.e. the start of the timeline) // 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); debuglog("TimelinePanel: unpaginating events in direction", dir);
// All tiles are inserted by MessagePanel to have a scrollToken === eventId, and // All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
// this particular event should be the first or last to be unpaginated. // 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) => { (ev) => {
return ev.getId() === eventId; 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) { if (count > 0) {
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir); debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
@ -305,9 +305,9 @@ var TimelinePanel = React.createClass({
// set off a pagination request. // set off a pagination request.
onMessageListFillRequest: function(backwards) { onMessageListFillRequest: function(backwards) {
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
var canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate'; const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
var paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating'; const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
if (!this.state[canPaginateKey]) { if (!this.state[canPaginateKey]) {
debuglog("TimelinePanel: have given up", dir, "paginating this timeline"); debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
@ -328,7 +328,7 @@ var TimelinePanel = React.createClass({
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r); debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
var newState = { const newState = {
[paginatingKey]: false, [paginatingKey]: false,
[canPaginateKey]: r, [canPaginateKey]: r,
events: this._getEvents(), events: this._getEvents(),
@ -336,8 +336,8 @@ var TimelinePanel = React.createClass({
// moving the window in this direction may mean that we can now // moving the window in this direction may mean that we can now
// paginate in the other where we previously could not. // paginate in the other where we previously could not.
var otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS; const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
var canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate'; const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
if (!this.state[canPaginateOtherWayKey] && if (!this.state[canPaginateOtherWayKey] &&
this._timelineWindow.canPaginate(otherDirection)) { this._timelineWindow.canPaginate(otherDirection)) {
debuglog('TimelinePanel: can now', otherDirection, 'paginate again'); debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
@ -420,15 +420,15 @@ var TimelinePanel = React.createClass({
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => { this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
if (this.unmounted) { return; } if (this.unmounted) { return; }
var events = this._timelineWindow.getEvents(); const events = this._timelineWindow.getEvents();
var lastEv = events[events.length-1]; const lastEv = events[events.length-1];
// if we're at the end of the live timeline, append the pending events // if we're at the end of the live timeline, append the pending events
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(...this.props.timelineSet.room.getPendingEvents()); events.push(...this.props.timelineSet.room.getPendingEvents());
} }
var updatedState = {events: events}; const updatedState = {events: events};
if (this.props.manageReadMarkers) { if (this.props.manageReadMarkers) {
// when a new event arrives when the user is not watching the // when a new event arrives when the user is not watching the
@ -439,8 +439,8 @@ var TimelinePanel = React.createClass({
// read-marker when a remote echo of an event we have just sent takes // read-marker when a remote echo of an event we have just sent takes
// more than the timeout on userCurrentlyActive. // more than the timeout on userCurrentlyActive.
// //
var myUserId = MatrixClientPeg.get().credentials.userId; const myUserId = MatrixClientPeg.get().credentials.userId;
var sender = ev.sender ? ev.sender.userId : null; const sender = ev.sender ? ev.sender.userId : null;
var callback = null; var callback = null;
if (sender != myUserId && !UserActivity.userCurrentlyActive()) { if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
updatedState.readMarkerVisible = true; updatedState.readMarkerVisible = true;
@ -646,7 +646,7 @@ var TimelinePanel = React.createClass({
// and we'll get confused when their ID changes and we can't figure out // 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 // where the RM is pointing to. The read marker will be invisible for
// now anyway, so this doesn't really matter. // now anyway, so this doesn't really matter.
var lastDisplayedIndex = this._getLastDisplayedEventIndex({ const lastDisplayedIndex = this._getLastDisplayedEventIndex({
allowPartial: true, allowPartial: true,
ignoreEchoes: true, ignoreEchoes: true,
}); });
@ -655,7 +655,7 @@ var TimelinePanel = React.createClass({
return; return;
} }
var lastDisplayedEvent = this.state.events[lastDisplayedIndex]; const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
this._setReadMarker(lastDisplayedEvent.getId(), this._setReadMarker(lastDisplayedEvent.getId(),
lastDisplayedEvent.getTs()); lastDisplayedEvent.getTs());
@ -676,7 +676,7 @@ var TimelinePanel = React.createClass({
// we call _timelineWindow.getEvents() rather than using // we call _timelineWindow.getEvents() rather than using
// this.state.events, because react batches the update to the latter, so it // this.state.events, because react batches the update to the latter, so it
// may not have been updated yet. // may not have been updated yet.
var events = this._timelineWindow.getEvents(); const events = this._timelineWindow.getEvents();
// first find where the current RM is // first find where the current RM is
for (var i = 0; i < events.length; i++) { for (var i = 0; i < events.length; i++) {
@ -689,7 +689,7 @@ var TimelinePanel = React.createClass({
} }
// now think about advancing it // now think about advancing it
var myUserId = MatrixClientPeg.get().credentials.userId; const myUserId = MatrixClientPeg.get().credentials.userId;
for (i++; i < events.length; i++) { for (i++; i < events.length; i++) {
var ev = events[i]; var ev = events[i];
if (!ev.sender || ev.sender.userId != myUserId) { if (!ev.sender || ev.sender.userId != myUserId) {
@ -734,7 +734,7 @@ var TimelinePanel = React.createClass({
// //
// a quick way to figure out if we've loaded the relevant event is // 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. // 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) { if (ret !== null) {
// The messagepanel knows where the RM is, so we must have loaded // The messagepanel knows where the RM is, so we must have loaded
// the relevant event. // the relevant event.
@ -755,13 +755,13 @@ var TimelinePanel = React.createClass({
forgetReadMarker: function() { forgetReadMarker: function() {
if (!this.props.manageReadMarkers) return; if (!this.props.manageReadMarkers) return;
var rmId = this._getCurrentReadReceipt(); const rmId = this._getCurrentReadReceipt();
// see if we know the timestamp for the rr event // see if we know the timestamp for the rr event
var tl = this.props.timelineSet.getTimelineForEvent(rmId); const tl = this.props.timelineSet.getTimelineForEvent(rmId);
var rmTs; let rmTs;
if (tl) { if (tl) {
var event = tl.getEvents().find((e) => { return e.getId() == rmId; }); const event = tl.getEvents().find((e) => { return e.getId() == rmId; });
if (event) { if (event) {
rmTs = event.getTs(); rmTs = event.getTs();
} }
@ -801,7 +801,7 @@ var TimelinePanel = React.createClass({
if (!this.props.manageReadMarkers) return null; if (!this.props.manageReadMarkers) return null;
if (!this.refs.messagePanel) return null; if (!this.refs.messagePanel) return null;
var ret = this.refs.messagePanel.getReadMarkerPosition(); const ret = this.refs.messagePanel.getReadMarkerPosition();
if (ret !== null) { if (ret !== null) {
return ret; return ret;
} }
@ -844,8 +844,7 @@ var TimelinePanel = React.createClass({
// jump to the live timeline on ctrl-end, rather than the end of the // jump to the live timeline on ctrl-end, rather than the end of the
// timeline window. // timeline window.
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
ev.keyCode == KeyCode.END) ev.keyCode == KeyCode.END) {
{
this.jumpToLiveTimeline(); this.jumpToLiveTimeline();
} else { } else {
this.refs.messagePanel.handleScrollKey(ev); this.refs.messagePanel.handleScrollKey(ev);
@ -853,12 +852,12 @@ var TimelinePanel = React.createClass({
}, },
_initTimeline: function(props) { _initTimeline: function(props) {
var initialEvent = props.eventId; const initialEvent = props.eventId;
var pixelOffset = props.eventPixelOffset; const pixelOffset = props.eventPixelOffset;
// if a pixelOffset is given, it is relative to the bottom of the // 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. // container. If not, put the event in the middle of the container.
var offsetBase = 1; let offsetBase = 1;
if (pixelOffset == null) { if (pixelOffset == null) {
offsetBase = 0.5; offsetBase = 0.5;
} }
@ -887,7 +886,7 @@ var TimelinePanel = React.createClass({
MatrixClientPeg.get(), this.props.timelineSet, MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap}); {windowLimit: this.props.timelineCap});
var onLoaded = () => { const onLoaded = () => {
this._reloadEvents(); this._reloadEvents();
// If we switched away from the room while there were pending // If we switched away from the room while there were pending
@ -922,15 +921,15 @@ var TimelinePanel = React.createClass({
}); });
}; };
var onError = (error) => { const onError = (error) => {
this.setState({timelineLoading: false}); this.setState({timelineLoading: false});
console.error( console.error(
`Error loading timeline panel at ${eventId}: ${error}`, `Error loading timeline panel at ${eventId}: ${error}`,
); );
var msg = error.message ? error.message : JSON.stringify(error); const msg = error.message ? error.message : JSON.stringify(error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var onFinished; let onFinished;
// if we were given an event ID, then when the user closes the // 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, // dialog, let's jump to the end of the timeline. If we weren't,
@ -945,7 +944,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 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."); : _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, { Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
@ -955,7 +954,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 // if we already have the event in question, TimelineWindow.load
// returns a resolved promise. // returns a resolved promise.
@ -996,7 +995,7 @@ var TimelinePanel = React.createClass({
// get the list of events from the timeline window and the pending event list // get the list of events from the timeline window and the pending event list
_getEvents: function() { _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 we're at the end of the live timeline, append the pending events
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
@ -1007,7 +1006,7 @@ var TimelinePanel = React.createClass({
}, },
_indexForEventId: function(evId) { _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()) { if (evId == this.state.events[i].getId()) {
return i; return i;
} }
@ -1017,18 +1016,18 @@ var TimelinePanel = React.createClass({
_getLastDisplayedEventIndex: function(opts) { _getLastDisplayedEventIndex: function(opts) {
opts = opts || {}; opts = opts || {};
var ignoreOwn = opts.ignoreOwn || false; const ignoreOwn = opts.ignoreOwn || false;
var ignoreEchoes = opts.ignoreEchoes || false; const ignoreEchoes = opts.ignoreEchoes || false;
var allowPartial = opts.allowPartial || false; const allowPartial = opts.allowPartial || false;
var messagePanel = this.refs.messagePanel; const messagePanel = this.refs.messagePanel;
if (messagePanel === undefined) return null; if (messagePanel === undefined) return null;
var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect(); const wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
var myUserId = MatrixClientPeg.get().credentials.userId; const myUserId = MatrixClientPeg.get().credentials.userId;
for (var i = this.state.events.length-1; i >= 0; --i) { for (let i = this.state.events.length-1; i >= 0; --i) {
var ev = this.state.events[i]; const ev = this.state.events[i];
if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) { if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) {
continue; continue;
@ -1039,10 +1038,10 @@ var TimelinePanel = React.createClass({
continue; continue;
} }
var node = messagePanel.getNodeForEventId(ev.getId()); const node = messagePanel.getNodeForEventId(ev.getId());
if (!node) continue; if (!node) continue;
var boundingRect = node.getBoundingClientRect(); const boundingRect = node.getBoundingClientRect();
if ((allowPartial && boundingRect.top < wrapperRect.bottom) || if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) { (!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
return i; return i;
@ -1060,18 +1059,18 @@ var TimelinePanel = React.createClass({
* SDK. * SDK.
*/ */
_getCurrentReadReceipt: function(ignoreSynthesized) { _getCurrentReadReceipt: function(ignoreSynthesized) {
var client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
// the client can be null on logout // the client can be null on logout
if (client == null) { if (client == null) {
return null; return null;
} }
var myUserId = client.credentials.userId; const myUserId = client.credentials.userId;
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized); return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
}, },
_setReadMarker: function(eventId, eventTs, inhibitSetState) { _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 // don't update the state (and cause a re-render) if there is
// no change to the RM. // no change to the RM.
@ -1096,8 +1095,8 @@ var TimelinePanel = React.createClass({
}, },
render: function() { render: function() {
var MessagePanel = sdk.getComponent("structures.MessagePanel"); const MessagePanel = sdk.getComponent("structures.MessagePanel");
var Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
// just show a spinner while the timeline loads. // just show a spinner while the timeline loads.
// //
@ -1112,7 +1111,7 @@ var TimelinePanel = React.createClass({
// exist. // exist.
if (this.state.timelineLoading) { if (this.state.timelineLoading) {
return ( return (
<div className={ this.props.className + " mx_RoomView_messageListWrapper" }> <div className={this.props.className + " mx_RoomView_messageListWrapper"}>
<Loader /> <Loader />
</div> </div>
); );
@ -1120,7 +1119,7 @@ var TimelinePanel = React.createClass({
if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) { if (this.state.events.length == 0 && !this.state.canBackPaginate && this.props.empty) {
return ( 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 className="mx_RoomView_empty">{ this.props.empty }</div>
</div> </div>
); );
@ -1134,7 +1133,7 @@ var TimelinePanel = React.createClass({
// forwards, otherwise if somebody hits the bottom of the loaded // forwards, otherwise if somebody hits the bottom of the loaded
// events when viewing historical messages, we get stuck in a loop // events when viewing historical messages, we get stuck in a loop
// of paginating our way through the entire history of the room. // 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 // 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. // the HS and fetch the latest events, so we are effectively forward paginating.
@ -1143,26 +1142,26 @@ var TimelinePanel = React.createClass({
); );
return ( return (
<MessagePanel ref="messagePanel" <MessagePanel ref="messagePanel"
hidden={ this.props.hidden } hidden={this.props.hidden}
backPaginating={ this.state.backPaginating } backPaginating={this.state.backPaginating}
forwardPaginating={ forwardPaginating } forwardPaginating={forwardPaginating}
events={ this.state.events } events={this.state.events}
highlightedEventId={ this.props.highlightedEventId } highlightedEventId={this.props.highlightedEventId}
readMarkerEventId={ this.state.readMarkerEventId } readMarkerEventId={this.state.readMarkerEventId}
readMarkerVisible={ this.state.readMarkerVisible } readMarkerVisible={this.state.readMarkerVisible}
suppressFirstDateSeparator={ this.state.canBackPaginate } suppressFirstDateSeparator={this.state.canBackPaginate}
showUrlPreview={ this.props.showUrlPreview } showUrlPreview={this.props.showUrlPreview}
showReadReceipts={ this.props.showReadReceipts } showReadReceipts={this.props.showReadReceipts}
ourUserId={ MatrixClientPeg.get().credentials.userId } ourUserId={MatrixClientPeg.get().credentials.userId}
stickyBottom={ stickyBottom } stickyBottom={stickyBottom}
onScroll={ this.onMessageListScroll } onScroll={this.onMessageListScroll}
onFillRequest={ this.onMessageListFillRequest } onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={ this.onMessageListUnfillRequest } onUnfillRequest={this.onMessageListUnfillRequest}
opacity={ this.props.opacity } opacity={this.props.opacity}
isTwelveHour={ this.state.isTwelveHour } isTwelveHour={this.state.isTwelveHour}
alwaysShowTimestamps={ this.state.alwaysShowTimestamps } alwaysShowTimestamps={this.state.alwaysShowTimestamps}
className={ this.props.className } className={this.props.className}
tileShape={ this.props.tileShape } tileShape={this.props.tileShape}
/> />
); );
}, },

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); const React = require('react');
var ContentMessages = require('../../ContentMessages'); const ContentMessages = require('../../ContentMessages');
var dis = require('../../dispatcher'); const dis = require('../../dispatcher');
var filesize = require('filesize'); const filesize = require('filesize');
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
module.exports = React.createClass({displayName: 'UploadBar', module.exports = React.createClass({displayName: 'UploadBar',
propTypes: { propTypes: {
room: React.PropTypes.object room: React.PropTypes.object,
}, },
componentDidMount: function() { componentDidMount: function() {
@ -46,7 +46,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
}, },
render: function() { render: function() {
var uploads = ContentMessages.getCurrentUploads(); const uploads = ContentMessages.getCurrentUploads();
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length // for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
// check in RoomView // check in RoomView
@ -62,8 +62,8 @@ module.exports = React.createClass({displayName: 'UploadBar',
return <div />; return <div />;
} }
var upload; let upload;
for (var i = 0; i < uploads.length; ++i) { for (let i = 0; i < uploads.length; ++i) {
if (uploads[i].roomId == this.props.room.roomId) { if (uploads[i].roomId == this.props.room.roomId) {
upload = uploads[i]; upload = uploads[i];
break; break;
@ -73,32 +73,32 @@ module.exports = React.createClass({displayName: 'UploadBar',
return <div />; return <div />;
} }
var innerProgressStyle = { const innerProgressStyle = {
width: ((upload.loaded / (upload.total || 1)) * 100) + '%' width: ((upload.loaded / (upload.total || 1)) * 100) + '%',
}; };
var uploadedSize = filesize(upload.loaded); let uploadedSize = filesize(upload.loaded);
var totalSize = filesize(upload.total); const totalSize = filesize(upload.total);
if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) { if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
uploadedSize = uploadedSize.replace(/ .*/, ''); uploadedSize = uploadedSize.replace(/ .*/, '');
} }
// MUST use var name 'count' for pluralization to kick in // 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 ( return (
<div className="mx_UploadBar"> <div className="mx_UploadBar">
<div className="mx_UploadBar_uploadProgressOuter"> <div className="mx_UploadBar_uploadProgressOuter">
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div> <div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
</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" <img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src="img/cancel.svg" width="18" height="18"
onClick={function() { ContentMessages.cancelUpload(upload.promise); }} onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
/> />
<div className="mx_UploadBar_uploadBytes"> <div className="mx_UploadBar_uploadBytes">
{ uploadedSize } / { totalSize } { uploadedSize } / { totalSize }
</div> </div>
<div className="mx_UploadBar_uploadFilename">{uploadText}</div> <div className="mx_UploadBar_uploadFilename">{ uploadText }</div>
</div> </div>
); );
} },
}); });

View file

@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -32,7 +33,7 @@ const AddThreepid = require('../../AddThreepid');
const SdkConfig = require('../../SdkConfig'); const SdkConfig = require('../../SdkConfig');
import Analytics from '../../Analytics'; import Analytics from '../../Analytics';
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
import { _t } from '../../languageHandler'; import { _t, _td } from '../../languageHandler';
import * as languageHandler from '../../languageHandler'; import * as languageHandler from '../../languageHandler';
import * as FormattingUtils from '../../utils/FormattingUtils'; import * as FormattingUtils from '../../utils/FormattingUtils';
@ -63,55 +64,59 @@ const gHVersionLabel = function(repo, token='') {
const SETTINGS_LABELS = [ const SETTINGS_LABELS = [
{ {
id: 'autoplayGifsAndVideos', id: 'autoplayGifsAndVideos',
label: 'Autoplay GIFs and videos', label: _td('Autoplay GIFs and videos'),
}, },
{ {
id: 'hideReadReceipts', id: 'hideReadReceipts',
label: 'Hide read receipts', label: _td('Hide read receipts'),
}, },
{ {
id: 'dontSendTypingNotifications', id: 'dontSendTypingNotifications',
label: "Don't send typing notifications", label: _td("Don't send typing notifications"),
}, },
{ {
id: 'alwaysShowTimestamps', id: 'alwaysShowTimestamps',
label: 'Always show message timestamps', label: _td('Always show message timestamps'),
}, },
{ {
id: 'showTwelveHourTimestamps', id: 'showTwelveHourTimestamps',
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)', label: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
}, },
{ {
id: 'hideJoinLeaves', id: 'hideJoinLeaves',
label: 'Hide join/leave messages (invites/kicks/bans unaffected)', label: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
}, },
{ {
id: 'hideAvatarDisplaynameChanges', id: 'hideAvatarDisplaynameChanges',
label: 'Hide avatar and display name changes', label: _td('Hide avatar and display name changes'),
}, },
{ {
id: 'useCompactLayout', id: 'useCompactLayout',
label: 'Use compact timeline layout', label: _td('Use compact timeline layout'),
}, },
{ {
id: 'hideRedactions', id: 'hideRedactions',
label: 'Hide removed messages', label: _td('Hide removed messages'),
}, },
{ {
id: 'enableSyntaxHighlightLanguageDetection', id: 'enableSyntaxHighlightLanguageDetection',
label: 'Enable automatic language detection for syntax highlighting', label: _td('Enable automatic language detection for syntax highlighting'),
}, },
{ {
id: 'MessageComposerInput.autoReplaceEmoji', id: 'MessageComposerInput.autoReplaceEmoji',
label: 'Automatically replace plain text Emoji', label: _td('Automatically replace plain text Emoji'),
}, },
{ {
id: 'MessageComposerInput.dontSuggestEmoji', id: 'MessageComposerInput.dontSuggestEmoji',
label: 'Disable Emoji suggestions while typing', label: _td('Disable Emoji suggestions while typing'),
}, },
{ {
id: 'Pill.shouldHidePillAvatar', id: 'Pill.shouldHidePillAvatar',
label: 'Hide avatars in user and room mentions', label: _td('Hide avatars in user and room mentions'),
},
{
id: 'TextualBody.disableBigEmoji',
label: _td('Disable big emoji in chat'),
}, },
/* /*
{ {
@ -124,7 +129,7 @@ const SETTINGS_LABELS = [
const ANALYTICS_SETTINGS_LABELS = [ const ANALYTICS_SETTINGS_LABELS = [
{ {
id: 'analyticsOptOut', id: 'analyticsOptOut',
label: 'Opt out of analytics', label: _td('Opt out of analytics'),
fn: function(checked) { fn: function(checked) {
Analytics[checked ? 'disable' : 'enable'](); Analytics[checked ? 'disable' : 'enable']();
}, },
@ -134,7 +139,7 @@ const ANALYTICS_SETTINGS_LABELS = [
const WEBRTC_SETTINGS_LABELS = [ const WEBRTC_SETTINGS_LABELS = [
{ {
id: 'webRtcForceTURN', id: 'webRtcForceTURN',
label: 'Disable Peer-to-Peer for 1:1 calls', label: _td('Disable Peer-to-Peer for 1:1 calls'),
}, },
]; ];
@ -143,7 +148,7 @@ const WEBRTC_SETTINGS_LABELS = [
const CRYPTO_SETTINGS_LABELS = [ const CRYPTO_SETTINGS_LABELS = [
{ {
id: 'blacklistUnverifiedDevices', id: 'blacklistUnverifiedDevices',
label: 'Never send encrypted messages to unverified devices from this device', label: _td('Never send encrypted messages to unverified devices from this device'),
fn: function(checked) { fn: function(checked) {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
}, },
@ -166,12 +171,12 @@ const CRYPTO_SETTINGS_LABELS = [
const THEMES = [ const THEMES = [
{ {
id: 'theme', id: 'theme',
label: 'Light theme', label: _td('Light theme'),
value: 'light', value: 'light',
}, },
{ {
id: 'theme', id: 'theme',
label: 'Dark theme', label: _td('Dark theme'),
value: 'dark', value: 'dark',
}, },
]; ];
@ -212,9 +217,6 @@ module.exports = React.createClass({
// The brand string given when creating email pushers // The brand string given when creating email pushers
brand: React.PropTypes.string, 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. // The base URL to use in the referral link. Defaults to window.location.origin.
referralBaseUrl: React.PropTypes.string, referralBaseUrl: React.PropTypes.string,
@ -226,7 +228,6 @@ module.exports = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
onClose: function() {}, onClose: function() {},
enableLabs: true,
}; };
}, },
@ -426,6 +427,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) { onLogoutClicked: function(ev) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, { Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, {
@ -793,7 +799,7 @@ module.exports = React.createClass({
onChange={onChange} onChange={onChange}
/> />
<label htmlFor={setting.id + "_" + setting.value}> <label htmlFor={setting.id + "_" + setting.value}>
{ setting.label } { _t(setting.label) }
</label> </label>
</div>; </div>;
}, },
@ -923,34 +929,25 @@ module.exports = React.createClass({
}, },
_renderLabs: function() { _renderLabs: function() {
// default to enabled if undefined
if (this.props.enableLabs === false) return null;
UserSettingsStore.doTranslations();
const features = []; const features = [];
UserSettingsStore.LABS_FEATURES.forEach((feature) => { UserSettingsStore.getLabsFeatures().forEach((featureId) => {
// This feature has an override and will be set to the default, so do not
// show it here.
if (feature.override) {
return;
}
// TODO: this ought to be a separate component so that we don't need // TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render // to rebind the onChange each time we render
const onChange = (e) => { const onChange = (e) => {
UserSettingsStore.setFeatureEnabled(feature.id, e.target.checked); UserSettingsStore.setFeatureEnabled(featureId, e.target.checked);
this.forceUpdate(); this.forceUpdate();
}; };
features.push( features.push(
<div key={feature.id} className="mx_UserSettings_toggle"> <div key={featureId} className="mx_UserSettings_toggle">
<input <input
type="checkbox" type="checkbox"
id={feature.id} id={featureId}
name={feature.id} name={featureId}
defaultChecked={UserSettingsStore.isFeatureEnabled(feature.id)} defaultChecked={UserSettingsStore.isFeatureEnabled(featureId)}
onChange={onChange} onChange={onChange}
/> />
<label htmlFor={feature.id}>{ feature.name }</label> <label htmlFor={featureId}>{ UserSettingsStore.translatedNameForFeature(featureId) }</label>
</div>); </div>);
}); });
@ -1330,7 +1327,11 @@ module.exports = React.createClass({
</div> </div>
<div className="mx_UserSettings_avatarPicker"> <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"
alt={_t("Remove avatar")} title={_t("Remove avatar")} />
</div>
<div onClick={this.onAvatarPickerClick} className="mx_UserSettings_avatarPicker_imgContainer">
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl} <ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
showUploadSection={false} className="mx_UserSettings_avatarPicker_img" /> showUploadSection={false} className="mx_UserSettings_avatarPicker_img" />
</div> </div>

View file

@ -17,13 +17,13 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
var sdk = require('../../../index'); const sdk = require('../../../index');
var Modal = require("../../../Modal"); const Modal = require("../../../Modal");
var MatrixClientPeg = require('../../../MatrixClientPeg'); const MatrixClientPeg = require('../../../MatrixClientPeg');
var PasswordReset = require("../../../PasswordReset"); const PasswordReset = require("../../../PasswordReset");
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ForgotPassword', displayName: 'ForgotPassword',
@ -35,30 +35,30 @@ module.exports = React.createClass({
customIsUrl: React.PropTypes.string, customIsUrl: React.PropTypes.string,
onLoginClick: React.PropTypes.func, onLoginClick: React.PropTypes.func,
onRegisterClick: React.PropTypes.func, onRegisterClick: React.PropTypes.func,
onComplete: React.PropTypes.func.isRequired onComplete: React.PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl, enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl, enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
progress: null progress: null,
}; };
}, },
submitPasswordReset: function(hsUrl, identityUrl, email, password) { submitPasswordReset: function(hsUrl, identityUrl, email, password) {
this.setState({ this.setState({
progress: "sending_email" progress: "sending_email",
}); });
this.reset = new PasswordReset(hsUrl, identityUrl); this.reset = new PasswordReset(hsUrl, identityUrl);
this.reset.resetPassword(email, password).done(() => { this.reset.resetPassword(email, password).done(() => {
this.setState({ this.setState({
progress: "sent_email" progress: "sent_email",
}); });
}, (err) => { }, (err) => {
this.showErrorDialog(_t('Failed to send email') + ": " + err.message); this.showErrorDialog(_t('Failed to send email') + ": " + err.message);
this.setState({ this.setState({
progress: null progress: null,
}); });
}); });
}, },
@ -81,15 +81,12 @@ module.exports = React.createClass({
if (!this.state.email) { if (!this.state.email) {
this.showErrorDialog(_t('The email address linked to your account must be entered.')); 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.')); 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.')); this.showErrorDialog(_t('New passwords must match each other.'));
} } else {
else { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, { Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
title: _t('Warning!'), title: _t('Warning!'),
description: description:
@ -99,7 +96,7 @@ module.exports = React.createClass({
'end-to-end encryption keys on all devices, ' + 'end-to-end encryption keys on all devices, ' +
'making encrypted chat history unreadable, ' + 'making encrypted chat history unreadable, ' +
'unless you first export your room keys and re-import ' + '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>, </div>,
button: _t('Continue'), button: _t('Continue'),
@ -107,13 +104,13 @@ module.exports = React.createClass({
<button className="mx_Dialog_primary" <button className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}> onClick={this._onExportE2eKeysClicked}>
{ _t('Export E2E room keys') } { _t('Export E2E room keys') }
</button> </button>,
], ],
onFinished: (confirmed) => { onFinished: (confirmed) => {
if (confirmed) { if (confirmed) {
this.submitPasswordReset( this.submitPasswordReset(
this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl, this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl,
this.state.email, this.state.password this.state.email, this.state.password,
); );
} }
}, },
@ -133,7 +130,7 @@ module.exports = React.createClass({
onInputChanged: function(stateKey, ev) { onInputChanged: function(stateKey, ev) {
this.setState({ this.setState({
[stateKey]: ev.target.value [stateKey]: ev.target.value,
}); });
}, },
@ -149,7 +146,7 @@ module.exports = React.createClass({
}, },
showErrorDialog: function(body, title) { showErrorDialog: function(body, title) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, { Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, {
title: title, title: title,
description: body, description: body,
@ -157,37 +154,34 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var LoginHeader = sdk.getComponent("login.LoginHeader"); const LoginHeader = sdk.getComponent("login.LoginHeader");
var LoginFooter = sdk.getComponent("login.LoginFooter"); const LoginFooter = sdk.getComponent("login.LoginFooter");
var ServerConfig = sdk.getComponent("login.ServerConfig"); const ServerConfig = sdk.getComponent("login.ServerConfig");
var Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
var resetPasswordJsx; let resetPasswordJsx;
if (this.state.progress === "sending_email") { if (this.state.progress === "sending_email") {
resetPasswordJsx = <Spinner />; resetPasswordJsx = <Spinner />;
} } else if (this.state.progress === "sent_email") {
else if (this.state.progress === "sent_email") {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
{ _t('An email has been sent to') } {this.state.email}. { _t("Once you've followed the link it contains, click below") }. { _t('An email has been sent to') } { this.state.email }. { _t("Once you've followed the link it contains, click below") }.
<br /> <br />
<input className="mx_Login_submit" type="button" onClick={this.onVerify} <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> </div>
); );
} } else if (this.state.progress === "complete") {
else if (this.state.progress === "complete") {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
<p>{ _t('Your password has been reset') }.</p> <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> <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} <input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
value={ _t('Return to login screen') } /> value={_t('Return to login screen')} />
</div> </div>
); );
} } else {
else {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
<div className="mx_Login_prompt"> <div className="mx_Login_prompt">
@ -199,21 +193,21 @@ module.exports = React.createClass({
name="reset_email" // define a name so browser's password autofill gets less confused name="reset_email" // define a name so browser's password autofill gets less confused
value={this.state.email} value={this.state.email}
onChange={this.onInputChanged.bind(this, "email")} onChange={this.onInputChanged.bind(this, "email")}
placeholder={ _t('Email address') } autoFocus /> placeholder={_t('Email address')} autoFocus />
<br /> <br />
<input className="mx_Login_field" ref="pass" type="password" <input className="mx_Login_field" ref="pass" type="password"
name="reset_password" name="reset_password"
value={this.state.password} value={this.state.password}
onChange={this.onInputChanged.bind(this, "password")} onChange={this.onInputChanged.bind(this, "password")}
placeholder={ _t('New password') } /> placeholder={_t('New password')} />
<br /> <br />
<input className="mx_Login_field" ref="pass" type="password" <input className="mx_Login_field" ref="pass" type="password"
name="reset_password_confirm" name="reset_password_confirm"
value={this.state.password2} value={this.state.password2}
onChange={this.onInputChanged.bind(this, "password2")} onChange={this.onInputChanged.bind(this, "password2")}
placeholder={ _t('Confirm your new password') } /> placeholder={_t('Confirm your new password')} />
<br /> <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> </form>
<ServerConfig ref="serverConfig" <ServerConfig ref="serverConfig"
withToggleButton={true} withToggleButton={true}
@ -222,11 +216,11 @@ module.exports = React.createClass({
customHsUrl={this.props.customHsUrl} customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl} customIsUrl={this.props.customIsUrl}
onServerConfigChange={this.onServerConfigChange} onServerConfigChange={this.onServerConfigChange}
delayTimeMs={0}/> delayTimeMs={0} />
<div className="mx_Login_error"> <div className="mx_Login_error">
</div> </div>
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> <a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{_t('Return to login screen')} { _t('Return to login screen') }
</a> </a>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#"> <a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
{ _t('Create an account') } { _t('Create an account') }
@ -242,9 +236,9 @@ module.exports = React.createClass({
<div className="mx_Login"> <div className="mx_Login">
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader /> <LoginHeader />
{resetPasswordJsx} { resetPasswordJsx }
</div> </div>
</div> </div>
); );
} },
}); });

View file

@ -134,7 +134,7 @@ module.exports = React.createClass({
}, },
_onLoginAsGuestClick: function() { _onLoginAsGuestClick: function() {
var self = this; const self = this;
self.setState({ self.setState({
busy: true, busy: true,
errorText: null, errorText: null,
@ -156,7 +156,7 @@ module.exports = React.createClass({
}); });
}).finally(function() { }).finally(function() {
self.setState({ self.setState({
busy: false busy: false,
}); });
}).done(); }).done();
}, },
@ -183,8 +183,8 @@ module.exports = React.createClass({
}, },
onServerConfigChange: function(config) { onServerConfigChange: function(config) {
var self = this; const self = this;
let newState = { const newState = {
errorText: null, // reset err messages errorText: null, // reset err messages
}; };
if (config.hsUrl !== undefined) { if (config.hsUrl !== undefined) {
@ -199,13 +199,13 @@ module.exports = React.createClass({
}, },
_initLoginLogic: function(hsUrl, isUrl) { _initLoginLogic: function(hsUrl, isUrl) {
var self = this; const self = this;
hsUrl = hsUrl || this.state.enteredHomeserverUrl; hsUrl = hsUrl || this.state.enteredHomeserverUrl;
isUrl = isUrl || this.state.enteredIdentityServerUrl; 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, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
}); });
this._loginLogic = loginLogic; this._loginLogic = loginLogic;
@ -259,15 +259,15 @@ module.exports = React.createClass({
{ _tJsx("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + { _tJsx("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>.", "Either use HTTPS or <a>enable unsafe scripts</a>.",
/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/,
(sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; } (sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; },
)} ) }
</span>; </span>;
} else { } else {
errorText = <span> 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.", { _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>/, /<a>(.*?)<\/a>/,
(sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; } (sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; },
)} ) }
</span>; </span>;
} }
} }
@ -290,6 +290,7 @@ module.exports = React.createClass({
onPhoneNumberChanged={this.onPhoneNumberChanged} onPhoneNumberChanged={this.onPhoneNumberChanged}
onForgotPasswordClick={this.props.onForgotPasswordClick} onForgotPasswordClick={this.props.onForgotPasswordClick}
loginIncorrect={this.state.loginIncorrect} loginIncorrect={this.state.loginIncorrect}
hsUrl={this.state.enteredHomeserverUrl}
/> />
); );
case 'm.login.cas': case 'm.login.cas':
@ -303,7 +304,7 @@ module.exports = React.createClass({
} }
return ( return (
<div> <div>
{ _t('Sorry, this homeserver is using a login which is not recognised ')}({step}) { _t('Sorry, this homeserver is using a login which is not recognised ') }({ step })
</div> </div>
); );
} }
@ -333,19 +334,19 @@ module.exports = React.createClass({
const ServerConfig = sdk.getComponent("login.ServerConfig"); const ServerConfig = sdk.getComponent("login.ServerConfig");
const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null; const loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
var loginAsGuestJsx; let loginAsGuestJsx;
if (this.props.enableGuest) { if (this.props.enableGuest) {
loginAsGuestJsx = loginAsGuestJsx =
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#"> <a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
{ _t('Login as guest')} { _t('Login as guest') }
</a>; </a>;
} }
var returnToAppJsx; let returnToAppJsx;
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
returnToAppJsx = returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#"> <a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
{ _t('Return to app')} { _t('Return to app') }
</a>; </a>;
} }
@ -354,7 +355,7 @@ module.exports = React.createClass({
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader /> <LoginHeader />
<div> <div>
<h2>{ _t('Sign in')} <h2>{ _t('Sign in') }
{ loader } { loader }
</h2> </h2>
{ this.componentForStep(this.state.currentFlow) } { this.componentForStep(this.state.currentFlow) }
@ -365,12 +366,12 @@ module.exports = React.createClass({
defaultHsUrl={this.props.defaultHsUrl} defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl} defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange} onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}/> delayTimeMs={1000} />
<div className="mx_Login_error"> <div className="mx_Login_error">
{ this.state.errorText } { this.state.errorText }
</div> </div>
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#"> <a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
{ _t('Create an account')} { _t('Create an account') }
</a> </a>
{ loginAsGuestJsx } { loginAsGuestJsx }
{ returnToAppJsx } { returnToAppJsx }
@ -380,5 +381,5 @@ module.exports = React.createClass({
</div> </div>
</div> </div>
); );
} },
}); });

View file

@ -25,14 +25,14 @@ module.exports = React.createClass({
displayName: 'PostRegistration', displayName: 'PostRegistration',
propTypes: { propTypes: {
onComplete: React.PropTypes.func.isRequired onComplete: React.PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
avatarUrl: null, avatarUrl: null,
errorString: null, errorString: null,
busy: false busy: false,
}; };
}, },
@ -40,26 +40,26 @@ module.exports = React.createClass({
// There is some assymetry between ChangeDisplayName and ChangeAvatar, // There is some assymetry between ChangeDisplayName and ChangeAvatar,
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects // 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). // 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}); this.setState({busy: true});
var self = this; const self = this;
cli.getProfileInfo(cli.credentials.userId).done(function(result) { cli.getProfileInfo(cli.credentials.userId).done(function(result) {
self.setState({ self.setState({
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url), avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
busy: false busy: false,
}); });
}, function(error) { }, function(error) {
self.setState({ self.setState({
errorString: _t("Failed to fetch avatar URL"), errorString: _t("Failed to fetch avatar URL"),
busy: false busy: false,
}); });
}); });
}, },
render: function() { render: function() {
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName'); const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var LoginHeader = sdk.getComponent('login.LoginHeader'); const LoginHeader = sdk.getComponent('login.LoginHeader');
return ( return (
<div className="mx_Login"> <div className="mx_Login">
<div className="mx_Login_box"> <div className="mx_Login_box">
@ -71,10 +71,10 @@ module.exports = React.createClass({
<ChangeAvatar <ChangeAvatar
initialAvatarUrl={this.state.avatarUrl} /> initialAvatarUrl={this.state.avatarUrl} />
<button onClick={this.props.onComplete}>{ _t('Continue') }</button> <button onClick={this.props.onComplete}>{ _t('Continue') }</button>
{this.state.errorString} { this.state.errorString }
</div> </div>
</div> </div>
</div> </div>
); );
} },
}); });

View file

@ -57,7 +57,7 @@ module.exports = React.createClass({
// registration shouldn't know or care how login is done. // registration shouldn't know or care how login is done.
onLoginClick: React.PropTypes.func.isRequired, onLoginClick: React.PropTypes.func.isRequired,
onCancelClick: React.PropTypes.func onCancelClick: React.PropTypes.func,
}, },
getInitialState: function() { getInitialState: function() {
@ -121,7 +121,7 @@ module.exports = React.createClass({
}, },
onServerConfigChange: function(config) { onServerConfigChange: function(config) {
let newState = {}; const newState = {};
if (config.hsUrl !== undefined) { if (config.hsUrl !== undefined) {
newState.hsUrl = config.hsUrl; newState.hsUrl = config.hsUrl;
} }
@ -195,7 +195,7 @@ module.exports = React.createClass({
this._rtsClient.getTeam(teamToken).then((team) => { this._rtsClient.getTeam(teamToken).then((team) => {
console.log( console.log(
`User successfully registered with team ${team.name}` `User successfully registered with team ${team.name}`,
); );
if (!team.rooms) { if (!team.rooms) {
return; return;
@ -223,7 +223,7 @@ module.exports = React.createClass({
deviceId: response.device_id, deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(), homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(), identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token accessToken: response.access_token,
}, teamToken); }, teamToken);
}).then((cli) => { }).then((cli) => {
return this._setupPushers(cli); return this._setupPushers(cli);
@ -253,7 +253,7 @@ module.exports = React.createClass({
}, },
onFormValidationFailed: function(errCode) { onFormValidationFailed: function(errCode) {
var errMsg; let errMsg;
switch (errCode) { switch (errCode) {
case "RegistrationForm.ERR_PASSWORD_MISSING": case "RegistrationForm.ERR_PASSWORD_MISSING":
errMsg = _t('Missing password.'); errMsg = _t('Missing password.');
@ -282,7 +282,7 @@ module.exports = React.createClass({
break; break;
} }
this.setState({ this.setState({
errorText: errMsg errorText: errMsg,
}); });
}, },
@ -316,7 +316,7 @@ module.exports = React.createClass({
emailAddress: this.state.formVals.email, emailAddress: this.state.formVals.email,
phoneCountry: this.state.formVals.phoneCountry, phoneCountry: this.state.formVals.phoneCountry,
phoneNumber: this.state.formVals.phoneNumber, phoneNumber: this.state.formVals.phoneNumber,
} };
}, },
render: function() { render: function() {
@ -346,7 +346,7 @@ module.exports = React.createClass({
} else { } else {
let errorSection; let errorSection;
if (this.state.errorText) { if (this.state.errorText) {
errorSection = <div className="mx_Login_error">{this.state.errorText}</div>; errorSection = <div className="mx_Login_error">{ this.state.errorText }</div>;
} }
registerBody = ( registerBody = (
<div> <div>
@ -362,7 +362,7 @@ module.exports = React.createClass({
onRegisterClick={this.onFormSubmit} onRegisterClick={this.onFormSubmit}
onTeamSelected={this.onTeamSelected} onTeamSelected={this.onTeamSelected}
/> />
{errorSection} { errorSection }
<ServerConfig ref="serverConfig" <ServerConfig ref="serverConfig"
withToggleButton={true} withToggleButton={true}
customHsUrl={this.props.customHsUrl} customHsUrl={this.props.customHsUrl}
@ -380,7 +380,7 @@ module.exports = React.createClass({
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
returnToAppJsx = ( returnToAppJsx = (
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#"> <a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
{_t('Return to app')} { _t('Return to app') }
</a> </a>
); );
} }
@ -393,15 +393,15 @@ module.exports = React.createClass({
this.state.teamSelected.domain + "/icon.png" : this.state.teamSelected.domain + "/icon.png" :
null} null}
/> />
<h2>{_t('Create an account')}</h2> <h2>{ _t('Create an account') }</h2>
{registerBody} { registerBody }
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> <a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{_t('I already have an account')} { _t('I already have an account') }
</a> </a>
{returnToAppJsx} { returnToAppJsx }
<LoginFooter /> <LoginFooter />
</div> </div>
</div> </div>
); );
} },
}); });

View file

@ -32,7 +32,7 @@ module.exports = React.createClass({
height: React.PropTypes.number, height: React.PropTypes.number,
// XXX resizeMethod not actually used. // XXX resizeMethod not actually used.
resizeMethod: React.PropTypes.string, resizeMethod: React.PropTypes.string,
defaultToInitialLetter: React.PropTypes.bool // true to add default url defaultToInitialLetter: React.PropTypes.bool, // true to add default url
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -40,7 +40,7 @@ module.exports = React.createClass({
width: 40, width: 40,
height: 40, height: 40,
resizeMethod: 'crop', resizeMethod: 'crop',
defaultToInitialLetter: true defaultToInitialLetter: true,
}; };
}, },
@ -50,15 +50,14 @@ module.exports = React.createClass({
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps: function(nextProps) {
// work out if we need to call setState (if the image URLs array has changed) // work out if we need to call setState (if the image URLs array has changed)
var newState = this._getState(nextProps); const newState = this._getState(nextProps);
var newImageUrls = newState.imageUrls; const newImageUrls = newState.imageUrls;
var oldImageUrls = this.state.imageUrls; const oldImageUrls = this.state.imageUrls;
if (newImageUrls.length !== oldImageUrls.length) { if (newImageUrls.length !== oldImageUrls.length) {
this.setState(newState); // detected a new entry this.setState(newState); // detected a new entry
} } else {
else {
// check each one to see if they are the same // 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]) { if (oldImageUrls[i] !== newImageUrls[i]) {
this.setState(newState); // detected a diff this.setState(newState); // detected a diff
break; break;
@ -71,31 +70,31 @@ module.exports = React.createClass({
// work out the full set of urls to try to load. This is formed like so: // work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, props.urls, default image ] // imageUrls: [ props.url, props.urls, default image ]
var urls = props.urls || []; const urls = props.urls || [];
if (props.url) { if (props.url) {
urls.unshift(props.url); // put in urls[0] urls.unshift(props.url); // put in urls[0]
} }
var defaultImageUrl = null; let defaultImageUrl = null;
if (props.defaultToInitialLetter) { if (props.defaultToInitialLetter) {
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString( defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
props.idName || props.name props.idName || props.name,
); );
urls.push(defaultImageUrl); // lowest priority urls.push(defaultImageUrl); // lowest priority
} }
return { return {
imageUrls: urls, imageUrls: urls,
defaultImageUrl: defaultImageUrl, defaultImageUrl: defaultImageUrl,
urlsIndex: 0 urlsIndex: 0,
}; };
}, },
onError: function(ev) { onError: function(ev) {
var nextIndex = this.state.urlsIndex + 1; const nextIndex = this.state.urlsIndex + 1;
if (nextIndex < this.state.imageUrls.length) { if (nextIndex < this.state.imageUrls.length) {
// try the next one // try the next one
this.setState({ this.setState({
urlsIndex: nextIndex urlsIndex: nextIndex,
}); });
} }
}, },
@ -109,32 +108,32 @@ module.exports = React.createClass({
return undefined; return undefined;
} }
var idx = 0; let idx = 0;
var initial = name[0]; const initial = name[0];
if ((initial === '@' || initial === '#') && name[1]) { if ((initial === '@' || initial === '#') && name[1]) {
idx++; idx++;
} }
// string.codePointAt(0) would do this, but that isn't supported by // string.codePointAt(0) would do this, but that isn't supported by
// some browsers (notably PhantomJS). // some browsers (notably PhantomJS).
var chars = 1; let chars = 1;
var first = name.charCodeAt(idx); const first = name.charCodeAt(idx);
// check if its the start of a surrogate pair // check if its the start of a surrogate pair
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) { 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) { if (second >= 0xDC00 && second <= 0xDFFF) {
chars++; chars++;
} }
} }
var firstChar = name.substring(idx, idx+chars); const firstChar = name.substring(idx, idx+chars);
return firstChar.toUpperCase(); return firstChar.toUpperCase();
}, },
render: function() { render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText'); const EmojiText = sdk.getComponent('elements.EmojiText');
var imageUrl = this.state.imageUrls[this.state.urlsIndex]; const imageUrl = this.state.imageUrls[this.state.urlsIndex];
const { const {
name, idName, title, url, urls, width, height, resizeMethod, name, idName, title, url, urls, width, height, resizeMethod,
@ -150,7 +149,7 @@ module.exports = React.createClass({
width: width + "px", width: width + "px",
lineHeight: height + "px" }} lineHeight: height + "px" }}
> >
{initialLetter} { initialLetter }
</EmojiText> </EmojiText>
); );
const imgNode = ( const imgNode = (
@ -163,15 +162,15 @@ module.exports = React.createClass({
<AccessibleButton element='span' className="mx_BaseAvatar" <AccessibleButton element='span' className="mx_BaseAvatar"
onClick={onClick} {...otherProps} onClick={onClick} {...otherProps}
> >
{textNode} { textNode }
{imgNode} { imgNode }
</AccessibleButton> </AccessibleButton>
); );
} else { } else {
return ( return (
<span className="mx_BaseAvatar" {...otherProps}> <span className="mx_BaseAvatar" {...otherProps}>
{textNode} { textNode }
{imgNode} { imgNode }
</span> </span>
); );
} }
@ -196,5 +195,5 @@ module.exports = React.createClass({
{...otherProps} /> {...otherProps} />
); );
} }
} },
}); });

View file

@ -16,9 +16,9 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
var Avatar = require('../../../Avatar'); const Avatar = require('../../../Avatar');
var sdk = require("../../../index"); const sdk = require("../../../index");
const dispatcher = require("../../../dispatcher"); const dispatcher = require("../../../dispatcher");
module.exports = React.createClass({ module.exports = React.createClass({
@ -63,14 +63,14 @@ module.exports = React.createClass({
imageUrl: Avatar.avatarUrlForMember(props.member, imageUrl: Avatar.avatarUrlForMember(props.member,
props.width, props.width,
props.height, props.height,
props.resizeMethod) props.resizeMethod),
}; };
}, },
render: function() { 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) { if (viewUserOnClick) {
onClick = () => { onClick = () => {
@ -83,7 +83,7 @@ module.exports = React.createClass({
return ( return (
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title} <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} />
); );
} },
}); });

View file

@ -36,7 +36,7 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<button className="mx_CreateRoomButton" onClick={this.onClick}>{_t("Create Room")}</button> <button className="mx_CreateRoomButton" onClick={this.onClick}>{ _t("Create Room") }</button>
); );
} },
}); });

View file

@ -16,10 +16,10 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
var Presets = { const Presets = {
PrivateChat: "private_chat", PrivateChat: "private_chat",
PublicChat: "public_chat", PublicChat: "public_chat",
Custom: "custom", Custom: "custom",
@ -29,7 +29,7 @@ module.exports = React.createClass({
displayName: 'CreateRoomPresets', displayName: 'CreateRoomPresets',
propTypes: { propTypes: {
onChange: React.PropTypes.func, onChange: React.PropTypes.func,
preset: React.PropTypes.string preset: React.PropTypes.string,
}, },
Presets: Presets, Presets: Presets,
@ -47,10 +47,10 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}> <select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
<option value={this.Presets.PrivateChat}>{_t("Private Chat")}</option> <option value={this.Presets.PrivateChat}>{ _t("Private Chat") }</option>
<option value={this.Presets.PublicChat}>{_t("Public Chat")}</option> <option value={this.Presets.PublicChat}>{ _t("Public Chat") }</option>
<option value={this.Presets.Custom}>{_t("Custom")}</option> <option value={this.Presets.Custom}>{ _t("Custom") }</option>
</select> </select>
); );
} },
}); });

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); const React = require('react');
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
@ -35,10 +35,10 @@ module.exports = React.createClass({
}, },
getAliasLocalpart: function() { getAliasLocalpart: function() {
var room_alias = this.props.alias; let room_alias = this.props.alias;
if (room_alias && this.props.homeserver) { if (room_alias && this.props.homeserver) {
var suffix = ":" + this.props.homeserver; const suffix = ":" + this.props.homeserver;
if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) { if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
room_alias = room_alias.slice(1, -suffix.length); room_alias = room_alias.slice(1, -suffix.length);
} }
@ -52,22 +52,22 @@ module.exports = React.createClass({
}, },
onFocus: function(ev) { onFocus: function(ev) {
var target = ev.target; const target = ev.target;
var curr_val = ev.target.value; const curr_val = ev.target.value;
if (this.props.homeserver) { if (this.props.homeserver) {
if (curr_val == "") { if (curr_val == "") {
var self = this; const self = this;
setTimeout(function() { setTimeout(function() {
target.value = "#:" + self.props.homeserver; target.value = "#:" + self.props.homeserver;
target.setSelectionRange(1, 1); target.setSelectionRange(1, 1);
}, 0); }, 0);
} else { } else {
var suffix = ":" + this.props.homeserver; const suffix = ":" + this.props.homeserver;
setTimeout(function() { setTimeout(function() {
target.setSelectionRange( target.setSelectionRange(
curr_val.startsWith("#") ? 1 : 0, 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); }, 0);
} }
@ -75,7 +75,7 @@ module.exports = React.createClass({
}, },
onBlur: function(ev) { onBlur: function(ev) {
var curr_val = ev.target.value; const curr_val = ev.target.value;
if (this.props.homeserver) { if (this.props.homeserver) {
if (curr_val == "#:" + this.props.homeserver) { if (curr_val == "#:" + this.props.homeserver) {
@ -84,8 +84,8 @@ module.exports = React.createClass({
} }
if (curr_val != "") { if (curr_val != "") {
var new_val = ev.target.value; let new_val = ev.target.value;
var suffix = ":" + this.props.homeserver; const suffix = ":" + this.props.homeserver;
if (!curr_val.startsWith("#")) new_val = "#" + new_val; if (!curr_val.startsWith("#")) new_val = "#" + new_val;
if (!curr_val.endsWith(suffix)) new_val = new_val + suffix; if (!curr_val.endsWith(suffix)) new_val = new_val + suffix;
ev.target.value = new_val; ev.target.value = new_val;
@ -97,7 +97,7 @@ module.exports = React.createClass({
return ( return (
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")} <input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur} onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
value={this.props.alias}/> value={this.props.alias} />
); );
} },
}); });

View file

@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import Promise from 'bluebird'; import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js'; import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStoreCache from '../../../stores/GroupStoreCache';
const TRUNCATE_QUERY_LIST = 40; const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
@ -241,32 +242,25 @@ module.exports = React.createClass({
_doNaiveGroupRoomSearch: function(query) { _doNaiveGroupRoomSearch: function(query) {
const lowerCaseQuery = query.toLowerCase(); const lowerCaseQuery = query.toLowerCase();
MatrixClientPeg.get().getGroupRooms(this.props.groupId).then((resp) => { const groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), this.props.groupId);
const results = []; const results = [];
resp.chunk.forEach((r) => { groupStore.getGroupRooms().forEach((r) => {
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery); const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery); const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery); const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
if (!(nameMatch || topicMatch || aliasMatch)) { if (!(nameMatch || topicMatch || aliasMatch)) {
return; return;
} }
results.push({ results.push({
room_id: r.room_id, room_id: r.room_id,
avatar_url: r.avatar_url, avatar_url: r.avatar_url,
name: r.name || r.canonical_alias, name: r.name || r.canonical_alias,
});
});
this._processResults(results, query);
}).catch((err) => {
console.error('Error whilst searching group users: ', err);
this.setState({
searchError: err.errcode ? err.message : _t('Something went wrong!'),
});
}).done(() => {
this.setState({
busy: false,
}); });
}); });
this._processResults(results, query);
this.setState({
busy: false,
});
}, },
_doRoomSearch: function(query) { _doRoomSearch: function(query) {

View file

@ -155,7 +155,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
width={48} height={48} width={48} height={48}
/> />
<div className="mx_ChatCreateOrReuseDialog_profile_name"> <div className="mx_ChatCreateOrReuseDialog_profile_name">
{this.state.profile.displayName || this.props.userId} { this.state.profile.displayName || this.props.userId }
</div> </div>
</div>; </div>;
} }
@ -177,7 +177,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog className='mx_ChatCreateOrReuseDialog' <BaseDialog className='mx_ChatCreateOrReuseDialog'
onFinished={ this.props.onFinished.bind(false) } onFinished={this.props.onFinished.bind(false)}
title={title} title={title}
> >
{ content } { content }

View 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>
);
},
});

View file

@ -83,7 +83,7 @@ export default class DeactivateAccountDialog extends React.Component {
let error = null; let error = null;
if (this.state.errStr) { if (this.state.errStr) {
error = <div className="error"> error = <div className="error">
{this.state.errStr} { this.state.errStr }
</div>; </div>;
passwordBoxClass = 'error'; passwordBoxClass = 'error';
} }
@ -94,30 +94,30 @@ export default class DeactivateAccountDialog extends React.Component {
let cancelButton = null; let cancelButton = null;
if (!this.state.busy) { if (!this.state.busy) {
cancelButton = <button onClick={this._onCancel} autoFocus={true}> cancelButton = <button onClick={this._onCancel} autoFocus={true}>
{_t("Cancel")} { _t("Cancel") }
</button>; </button>;
} }
return ( return (
<div className="mx_DeactivateAccountDialog"> <div className="mx_DeactivateAccountDialog">
<div className="mx_Dialog_title danger"> <div className="mx_Dialog_title danger">
{_t("Deactivate Account")} { _t("Deactivate Account") }
</div> </div>
<div className="mx_Dialog_content"> <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 <input
type="password" type="password"
onChange={this._onPasswordFieldChange} onChange={this._onPasswordFieldChange}
ref={(e) => {this._passwordField = e;}} ref={(e) => {this._passwordField = e;}}
className={passwordBoxClass} className={passwordBoxClass}
/> />
{error} { error }
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button <button
@ -125,10 +125,10 @@ export default class DeactivateAccountDialog extends React.Component {
onClick={this._onOk} onClick={this._onOk}
disabled={!okEnabled} disabled={!okEnabled}
> >
{okLabel} { okLabel }
</button> </button>
{cancelButton} { cancelButton }
</div> </div>
</div> </div>
); );

View file

@ -48,7 +48,7 @@ export default React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
authError: null, authError: null,
} };
}, },
_onAuthFinished: function(success, result) { _onAuthFinished: function(success, result) {
@ -73,12 +73,12 @@ export default React.createClass({
if (this.state.authError) { if (this.state.authError) {
content = ( content = (
<div> <div>
<div>{this.state.authError.message || this.state.authError.toString()}</div> <div>{ this.state.authError.message || this.state.authError.toString() }</div>
<br /> <br />
<AccessibleButton onClick={this._onDismissClick} <AccessibleButton onClick={this._onDismissClick}
className="mx_UserSettings_button" className="mx_UserSettings_button"
> >
{_t("Dismiss")} { _t("Dismiss") }
</AccessibleButton> </AccessibleButton>
</div> </div>
); );
@ -100,7 +100,7 @@ export default React.createClass({
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))} title={this.state.authError ? 'Error' : (this.props.title || _t('Authentication'))}
> >
{content} { content }
</BaseDialog> </BaseDialog>
); );
}, },

View file

@ -18,7 +18,7 @@ import Modal from '../../../Modal';
import React from 'react'; import React from 'react';
import sdk from '../../../index'; 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 * Dialog which asks the user whether they want to share their keys with
@ -116,11 +116,11 @@ export default React.createClass({
let text; let text;
if (this.state.wasNewDevice) { if (this.state.wasNewDevice) {
text = "You added a new device '%(displayName)s', which is" text = _td("You added a new device '%(displayName)s', which is"
+ " requesting encryption keys."; + " requesting encryption keys.");
} else { } else {
text = "Your unverified device '%(displayName)s' is requesting" text = _td("Your unverified device '%(displayName)s' is requesting"
+ " encryption keys."; + " encryption keys.");
} }
text = _t(text, {displayName: displayName}); text = _t(text, {displayName: displayName});

View file

@ -68,7 +68,7 @@ export default React.createClass({
<label htmlFor="textinput"> { this.props.description } </label> <label htmlFor="textinput"> { this.props.description } </label>
</div> </div>
<div> <div>
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" onKeyDown={this.onKeyDown} /> <input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
</div> </div>
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">

View file

@ -28,9 +28,9 @@ function DeviceListEntry(props) {
return ( return (
<li> <li>
<DeviceVerifyButtons device={ device } userId={ userId } /> <DeviceVerifyButtons device={device} userId={userId} />
{ device.deviceId } { device.deviceId }
<br/> <br />
{ device.getDisplayName() } { device.getDisplayName() }
</li> </li>
); );
@ -48,13 +48,13 @@ function UserUnknownDeviceList(props) {
const {userId, userDevices} = props; const {userId, userDevices} = props;
const deviceListEntries = Object.keys(userDevices).map((deviceId) => const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
<DeviceListEntry key={ deviceId } userId={ userId } <DeviceListEntry key={deviceId} userId={userId}
device={ userDevices[deviceId] } />, device={userDevices[deviceId]} />,
); );
return ( return (
<ul className="mx_UnknownDeviceDialog_deviceList"> <ul className="mx_UnknownDeviceDialog_deviceList">
{deviceListEntries} { deviceListEntries }
</ul> </ul>
); );
} }
@ -71,13 +71,13 @@ function UnknownDeviceList(props) {
const {devices} = props; const {devices} = props;
const userListEntries = Object.keys(devices).map((userId) => const userListEntries = Object.keys(devices).map((userId) =>
<li key={ userId }> <li key={userId}>
<p>{ userId }:</p> <p>{ userId }:</p>
<UserUnknownDeviceList userId={ userId } userDevices={ devices[userId] } /> <UserUnknownDeviceList userId={userId} userDevices={devices[userId]} />
</li>, </li>,
); );
return <ul>{userListEntries}</ul>; return <ul>{ userListEntries }</ul>;
} }
UnknownDeviceList.propTypes = { UnknownDeviceList.propTypes = {
@ -120,17 +120,17 @@ export default React.createClass({
if (blacklistUnverified) { if (blacklistUnverified) {
warning = ( warning = (
<h4> <h4>
{_t("You are currently blacklisting unverified devices; to send " + { _t("You are currently blacklisting unverified devices; to send " +
"messages to these devices you must verify them.")} "messages to these devices you must verify them.") }
</h4> </h4>
); );
} else { } else {
warning = ( warning = (
<div> <div>
<p> <p>
{_t("We recommend you go through the verification process " + { _t("We recommend you go through the verification process " +
"for each device to confirm they belong to their legitimate owner, " + "for each device to confirm they belong to their legitimate owner, " +
"but you can resend the message without verifying if you prefer.")} "but you can resend the message without verifying if you prefer.") }
</p> </p>
</div> </div>
); );
@ -149,22 +149,22 @@ export default React.createClass({
> >
<GeminiScrollbar autoshow={false} className="mx_Dialog_content"> <GeminiScrollbar autoshow={false} className="mx_Dialog_content">
<h4> <h4>
{_t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name})} { _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }
</h4> </h4>
{ warning } { warning }
{_t("Unknown devices")}: { _t("Unknown devices") }:
<UnknownDeviceList devices={this.props.devices} /> <UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar> </GeminiScrollbar>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" autoFocus={ true } <button className="mx_Dialog_primary" autoFocus={true}
onClick={() => { onClick={() => {
this.props.onFinished(); this.props.onFinished();
Resend.resendUnsentEvents(this.props.room); Resend.resendUnsentEvents(this.props.room);
}}> }}>
{_t("Send anyway")} { _t("Send anyway") }
</button> </button>
<button className="mx_Dialog_primary" autoFocus={ true } <button className="mx_Dialog_primary" autoFocus={true}
onClick={() => { onClick={() => {
// XXX: temporary logging to try to diagnose // XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148 // https://github.com/vector-im/riot-web/issues/3148

View file

@ -32,7 +32,7 @@ export default function AccessibleButton(props) {
}; };
restProps.tabIndex = restProps.tabIndex || "0"; restProps.tabIndex = restProps.tabIndex || "0";
restProps.role = "button"; restProps.role = "button";
restProps.className = (restProps.className ? restProps.className + " " : "") + restProps.className = (restProps.className ? restProps.className + " " : "") +
"mx_AccessibleButton"; "mx_AccessibleButton";
return React.createElement(element, restProps, children); return React.createElement(element, restProps, children);
} }

View file

@ -79,8 +79,8 @@ export default React.createClass({
onMouseLeave={this._onMouseLeave} onMouseLeave={this._onMouseLeave}
> >
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} /> <TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
{tooltip} { tooltip }
</AccessibleButton> </AccessibleButton>
); );
} },
}); });

View file

@ -46,8 +46,8 @@ export default React.createClass({
componentWillReceiveProps: function(props) { componentWillReceiveProps: function(props) {
// Make sure the selected item isn't outside the list bounds // Make sure the selected item isn't outside the list bounds
var selected = this.state.selected; const selected = this.state.selected;
var maxSelected = this._maxSelected(props.addressList); const maxSelected = this._maxSelected(props.addressList);
if (selected > maxSelected) { if (selected > maxSelected) {
this.setState({ selected: maxSelected }); this.setState({ selected: maxSelected });
} }
@ -57,7 +57,7 @@ export default React.createClass({
// As the user scrolls with the arrow keys keep the selected item // As the user scrolls with the arrow keys keep the selected item
// at the top of the window. // at the top of the window.
if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) { if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) {
var elementHeight = this.addressListElement.getBoundingClientRect().height; const elementHeight = this.addressListElement.getBoundingClientRect().height;
this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight; this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight;
} }
}, },
@ -75,7 +75,7 @@ export default React.createClass({
if (this.state.selected > 0) { if (this.state.selected > 0) {
this.setState({ this.setState({
selected: this.state.selected - 1, selected: this.state.selected - 1,
hover : false, hover: false,
}); });
} }
}, },
@ -84,7 +84,7 @@ export default React.createClass({
if (this.state.selected < this._maxSelected(this.props.addressList)) { if (this.state.selected < this._maxSelected(this.props.addressList)) {
this.setState({ this.setState({
selected: this.state.selected + 1, selected: this.state.selected + 1,
hover : false, hover: false,
}); });
} }
}, },
@ -105,7 +105,7 @@ export default React.createClass({
}, },
onMouseLeave: function() { onMouseLeave: function() {
this.setState({ hover : false }); this.setState({ hover: false });
}, },
selectAddress: function(index) { selectAddress: function(index) {
@ -117,15 +117,15 @@ export default React.createClass({
}, },
createAddressListTiles: function() { createAddressListTiles: function() {
var self = this; const self = this;
var AddressTile = sdk.getComponent("elements.AddressTile"); const AddressTile = sdk.getComponent("elements.AddressTile");
var maxSelected = this._maxSelected(this.props.addressList); const maxSelected = this._maxSelected(this.props.addressList);
var addressList = []; const addressList = [];
// Only create the address elements if there are address // Only create the address elements if there are address
if (this.props.addressList.length > 0) { if (this.props.addressList.length > 0) {
for (var i = 0; i <= maxSelected; i++) { for (let i = 0; i <= maxSelected; i++) {
var classes = classNames({ const classes = classNames({
"mx_AddressSelector_addressListElement": true, "mx_AddressSelector_addressListElement": true,
"mx_AddressSelector_selected": this.state.selected === i, "mx_AddressSelector_selected": this.state.selected === i,
}); });
@ -143,7 +143,7 @@ export default React.createClass({
ref={(ref) => { this.addressListElement = ref; }} ref={(ref) => { this.addressListElement = ref; }}
> >
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" /> <AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
</div> </div>,
); );
} }
} }
@ -151,13 +151,13 @@ export default React.createClass({
}, },
_maxSelected: function(list) { _maxSelected: function(list) {
var listSize = list.length === 0 ? 0 : list.length - 1; const listSize = list.length === 0 ? 0 : list.length - 1;
var maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize; const maxSelected = listSize > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize;
return maxSelected; return maxSelected;
}, },
render: function() { render: function() {
var classes = classNames({ const classes = classNames({
"mx_AddressSelector": true, "mx_AddressSelector": true,
"mx_AddressSelector_empty": this.props.addressList.length === 0, "mx_AddressSelector_empty": this.props.addressList.length === 0,
}); });
@ -168,5 +168,5 @@ export default React.createClass({
{ this.createAddressListTiles() } { this.createAddressListTiles() }
</div> </div>
); );
} },
}); });

View file

@ -107,24 +107,24 @@ export default React.createClass({
let nameNode = null; let nameNode = null;
if (address.displayName) { if (address.displayName) {
nameNode = <div className={nameClasses}>{ address.displayName }</div> nameNode = <div className={nameClasses}>{ address.displayName }</div>;
} }
info = ( info = (
<div className="mx_AddressTile_mx"> <div className="mx_AddressTile_mx">
<div className={emailClasses}>{ address.address }</div> <div className={emailClasses}>{ address.address }</div>
{nameNode} { nameNode }
</div> </div>
); );
} else { } else {
error = true; error = true;
var unknownClasses = classNames({ const unknownClasses = classNames({
"mx_AddressTile_unknown": true, "mx_AddressTile_unknown": true,
"mx_AddressTile_justified": this.props.justified, "mx_AddressTile_justified": this.props.justified,
}); });
info = ( info = (
<div className={unknownClasses}>{_t("Unknown Address")}</div> <div className={unknownClasses}>{ _t("Unknown Address") }</div>
); );
} }
@ -151,5 +151,5 @@ export default React.createClass({
{ dismiss } { dismiss }
</div> </div>
); );
} },
}); });

View file

@ -23,7 +23,7 @@ import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarAuthClient from '../../../ScalarAuthClient';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import AppPermission from './AppPermission'; import AppPermission from './AppPermission';
import AppWarning from './AppWarning'; import AppWarning from './AppWarning';
@ -195,9 +195,9 @@ export default React.createClass({
// These strings are translated at the point that they are inserted in to the DOM, in the render method // These strings are translated at the point that they are inserted in to the DOM, in the render method
_deleteWidgetLabel() { _deleteWidgetLabel() {
if (this._canUserModify()) { if (this._canUserModify()) {
return 'Delete widget'; return _td('Delete widget');
} }
return 'Revoke widget access'; return _td('Revoke widget access');
}, },
/* TODO -- Store permission in account data so that it is persisted across multiple devices */ /* TODO -- Store permission in account data so that it is persisted across multiple devices */

View file

@ -24,7 +24,7 @@ const CreateRoomButton = function(props) {
return ( return (
<ActionButton action="view_create_room" <ActionButton action="view_create_room"
mouseOverAction={props.callout ? "callout_create_room" : null} mouseOverAction={props.callout ? "callout_create_room" : null}
label={ _t("Create new room") } label={_t("Create new room")}
iconPath="img/icons-create-room.svg" iconPath="img/icons-create-room.svg"
size={props.size} size={props.size}
tooltip={props.tooltip} tooltip={props.tooltip}

View file

@ -30,7 +30,7 @@ export default React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
device: this.props.device device: this.props.device,
}; };
}, },
@ -60,37 +60,37 @@ export default React.createClass({
onUnverifyClick: function() { onUnverifyClick: function() {
MatrixClientPeg.get().setDeviceVerified( MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.state.device.deviceId, false this.props.userId, this.state.device.deviceId, false,
); );
}, },
onBlacklistClick: function() { onBlacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked( MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.state.device.deviceId, true this.props.userId, this.state.device.deviceId, true,
); );
}, },
onUnblacklistClick: function() { onUnblacklistClick: function() {
MatrixClientPeg.get().setDeviceBlocked( MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.state.device.deviceId, false this.props.userId, this.state.device.deviceId, false,
); );
}, },
render: function() { render: function() {
var blacklistButton = null, verifyButton = null; let blacklistButton = null, verifyButton = null;
if (this.state.device.isBlocked()) { if (this.state.device.isBlocked()) {
blacklistButton = ( blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblacklist"
onClick={this.onUnblacklistClick}> onClick={this.onUnblacklistClick}>
{_t("Unblacklist")} { _t("Unblacklist") }
</button> </button>
); );
} else { } else {
blacklistButton = ( blacklistButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_blacklist"
onClick={this.onBlacklistClick}> onClick={this.onBlacklistClick}>
{_t("Blacklist")} { _t("Blacklist") }
</button> </button>
); );
} }
@ -99,14 +99,14 @@ export default React.createClass({
verifyButton = ( verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}> onClick={this.onUnverifyClick}>
{_t("Unverify")} { _t("Unverify") }
</button> </button>
); );
} else { } else {
verifyButton = ( verifyButton = (
<button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify" <button className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_verify"
onClick={this.onVerifyClick}> onClick={this.onVerifyClick}>
{_t("Verify...")} { _t("Verify...") }
</button> </button>
); );
} }

View file

@ -95,7 +95,7 @@ export default class DirectorySearchBox extends React.Component {
onChange={this._onChange} onKeyUp={this._onKeyUp} onChange={this._onChange} onKeyUp={this._onKeyUp}
placeholder={this.props.placeholder} autoFocus placeholder={this.props.placeholder} autoFocus
/> />
{join_button} { join_button }
<span className="mx_DirectorySearchBox_clear_wrapper"> <span className="mx_DirectorySearchBox_clear_wrapper">
<span className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} /> <span className="mx_DirectorySearchBox_clear" onClick={this._onClearClick} />
</span> </span>

View file

@ -26,6 +26,12 @@ class MenuOption extends React.Component {
this._onClick = this._onClick.bind(this); this._onClick = this._onClick.bind(this);
} }
getDefaultProps() {
return {
disabled: false,
};
}
_onMouseEnter() { _onMouseEnter() {
this.props.onMouseEnter(this.props.dropdownKey); this.props.onMouseEnter(this.props.dropdownKey);
} }
@ -46,15 +52,15 @@ class MenuOption extends React.Component {
onClick={this._onClick} onKeyPress={this._onKeyPress} onClick={this._onClick} onKeyPress={this._onKeyPress}
onMouseEnter={this._onMouseEnter} onMouseEnter={this._onMouseEnter}
> >
{this.props.children} { this.props.children }
</div> </div>;
} }
}; }
MenuOption.propTypes = { MenuOption.propTypes = {
children: React.PropTypes.oneOfType([ children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.node), React.PropTypes.arrayOf(React.PropTypes.node),
React.PropTypes.node React.PropTypes.node,
]), ]),
highlighted: React.PropTypes.bool, highlighted: React.PropTypes.bool,
dropdownKey: React.PropTypes.string, dropdownKey: React.PropTypes.string,
@ -153,6 +159,8 @@ export default class Dropdown extends React.Component {
} }
_onInputClick(ev) { _onInputClick(ev) {
if (this.props.disabled) return;
if (!this.state.expanded) { if (!this.state.expanded) {
this.setState({ this.setState({
expanded: true, expanded: true,
@ -250,13 +258,13 @@ export default class Dropdown extends React.Component {
onMouseEnter={this._setHighlightedOption} onMouseEnter={this._setHighlightedOption}
onClick={this._onMenuOptionClick} onClick={this._onMenuOptionClick}
> >
{child} { child }
</MenuOption> </MenuOption>
); );
}); });
if (options.length === 0) { if (options.length === 0) {
return [<div key="0" className="mx_Dropdown_option"> return [<div key="0" className="mx_Dropdown_option">
{_t("No results")} { _t("No results") }
</div>]; </div>];
} }
return options; return options;
@ -279,7 +287,7 @@ export default class Dropdown extends React.Component {
/>; />;
} }
menu = <div className="mx_Dropdown_menu" style={menuStyle}> menu = <div className="mx_Dropdown_menu" style={menuStyle}>
{this._getMenuOptions()} { this._getMenuOptions() }
</div>; </div>;
} }
@ -288,12 +296,13 @@ export default class Dropdown extends React.Component {
this.props.getShortOption(this.props.value) : this.props.getShortOption(this.props.value) :
this.childrenByKey[this.props.value]; this.childrenByKey[this.props.value];
currentValue = <div className="mx_Dropdown_option"> currentValue = <div className="mx_Dropdown_option">
{selectedChild} { selectedChild }
</div> </div>;
} }
const dropdownClasses = { const dropdownClasses = {
mx_Dropdown: true, mx_Dropdown: true,
mx_Dropdown_disabled: this.props.disabled,
}; };
if (this.props.className) { if (this.props.className) {
dropdownClasses[this.props.className] = true; dropdownClasses[this.props.className] = true;
@ -303,9 +312,9 @@ export default class Dropdown extends React.Component {
// to the input, but overflows below it. The root contains both. // to the input, but overflows below it. The root contains both.
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}> return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
<AccessibleButton className="mx_Dropdown_input" onClick={this._onInputClick}> <AccessibleButton className="mx_Dropdown_input" onClick={this._onInputClick}>
{currentValue} { currentValue }
<span className="mx_Dropdown_arrow"></span> <span className="mx_Dropdown_arrow"></span>
{menu} { menu }
</AccessibleButton> </AccessibleButton>
</div>; </div>;
} }
@ -329,4 +338,6 @@ Dropdown.propTypes = {
// in the dropped-down menu. // in the dropped-down menu.
getShortOption: React.PropTypes.func, getShortOption: React.PropTypes.func,
value: React.PropTypes.string, value: React.PropTypes.string,
} // negative for consistency with HTML
disabled: React.PropTypes.bool,
};

View file

@ -0,0 +1,149 @@
/*
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 sdk from '../../../index';
import {_t} from '../../../languageHandler.js';
const EditableItem = React.createClass({
displayName: 'EditableItem',
propTypes: {
initialValue: PropTypes.string,
index: PropTypes.number,
placeholder: PropTypes.string,
onChange: PropTypes.func,
onRemove: PropTypes.func,
onAdd: PropTypes.func,
addOnChange: PropTypes.bool,
},
onChange: function(value) {
this.setState({ value });
if (this.props.onChange) this.props.onChange(value, this.props.index);
if (this.props.addOnChange && this.props.onAdd) this.props.onAdd(value);
},
onRemove: function() {
if (this.props.onRemove) this.props.onRemove(this.props.index);
},
onAdd: function() {
if (this.props.onAdd) this.props.onAdd(this.state.value);
},
render: function() {
const EditableText = sdk.getComponent('elements.EditableText');
return <div className="mx_EditableItem">
<EditableText
className="mx_EditableItem_editable"
placeholderClassName="mx_EditableItem_editablePlaceholder"
placeholder={this.props.placeholder}
blurToCancel={false}
editable={true}
initialValue={this.props.initialValue}
onValueChanged={this.onChange} />
{ this.props.onAdd ?
<div className="mx_EditableItem_addButton">
<img className="mx_filterFlipColor"
src="img/plus.svg" width="14" height="14"
alt={_t("Add")} onClick={this.onAdd} />
</div>
:
<div className="mx_EditableItem_removeButton">
<img className="mx_filterFlipColor"
src="img/cancel-small.svg" width="14" height="14"
alt={_t("Delete")} onClick={this.onRemove} />
</div>
}
</div>;
},
});
module.exports = React.createClass({
displayName: 'EditableItemList',
propTypes: {
items: PropTypes.arrayOf(PropTypes.string).isRequired,
onNewItemChanged: PropTypes.func,
onItemAdded: PropTypes.func,
onItemEdited: PropTypes.func,
onItemRemoved: PropTypes. func,
},
getDefaultProps: function() {
return {
onItemAdded: () => {},
onItemEdited: () => {},
onItemRemoved: () => {},
onNewItemChanged: () => {},
};
},
onItemAdded: function(value) {
this.props.onItemAdded(value);
},
onItemEdited: function(value, index) {
if (value.length === 0) {
this.onItemRemoved(index);
} else {
this.props.onItemEdited(value, index);
}
},
onItemRemoved: function(index) {
this.props.onItemRemoved(index);
},
onNewItemChanged: function(value) {
this.props.onNewItemChanged(value);
},
render: function() {
const editableItems = this.props.items.map((item, index) => {
return <EditableItem
key={index}
index={index}
initialValue={item}
onChange={this.onItemEdited}
onRemove={this.onItemRemoved}
placeholder={this.props.placeholder}
/>;
});
const label = this.props.items.length > 0 ?
this.props.itemsLabel : this.props.noItemsLabel;
return (<div className="mx_EditableItemList">
<div className="mx_EditableItemList_label">
{ label }
</div>
{ editableItems }
<EditableItem
key={-1}
initialValue={this.props.newItem}
onAdd={this.onItemAdded}
onChange={this.onNewItemChanged}
addOnChange={true}
placeholder={this.props.placeholder}
/>
</div>);
},
});

View file

@ -16,7 +16,7 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
const KEY_TAB = 9; const KEY_TAB = 9;
const KEY_SHIFT = 16; const KEY_SHIFT = 16;
@ -65,7 +65,9 @@ module.exports = React.createClass({
}, },
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps: function(nextProps) {
if (nextProps.initialValue !== this.props.initialValue) { if (nextProps.initialValue !== this.props.initialValue ||
nextProps.initialValue !== this.value
) {
this.value = nextProps.initialValue; this.value = nextProps.initialValue;
if (this.refs.editable_div) { if (this.refs.editable_div) {
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
@ -93,8 +95,7 @@ module.exports = React.createClass({
this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName); this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName);
this.placeholder = true; this.placeholder = true;
this.value = ''; this.value = '';
} } else {
else {
this.refs.editable_div.textContent = this.value; this.refs.editable_div.textContent = this.value;
this.refs.editable_div.setAttribute("class", this.props.className); this.refs.editable_div.setAttribute("class", this.props.className);
this.placeholder = false; this.placeholder = false;
@ -150,8 +151,7 @@ module.exports = React.createClass({
if (!ev.target.textContent) { if (!ev.target.textContent) {
this.showPlaceholder(true); this.showPlaceholder(true);
} } else if (!this.placeholder) {
else if (!this.placeholder) {
this.value = ev.target.textContent; this.value = ev.target.textContent;
} }
@ -175,21 +175,21 @@ module.exports = React.createClass({
onFocus: function(ev) { onFocus: function(ev) {
//ev.target.setSelectionRange(0, ev.target.textContent.length); //ev.target.setSelectionRange(0, ev.target.textContent.length);
var node = ev.target.childNodes[0]; const node = ev.target.childNodes[0];
if (node) { if (node) {
var range = document.createRange(); const range = document.createRange();
range.setStart(node, 0); range.setStart(node, 0);
range.setEnd(node, node.length); range.setEnd(node, node.length);
var sel = window.getSelection(); const sel = window.getSelection();
sel.removeAllRanges(); sel.removeAllRanges();
sel.addRange(range); sel.addRange(range);
} }
}, },
onFinish: function(ev, shouldSubmit) { onFinish: function(ev, shouldSubmit) {
var self = this; const self = this;
var submit = (ev.key === "Enter") || shouldSubmit; const submit = (ev.key === "Enter") || shouldSubmit;
this.setState({ this.setState({
phase: this.Phases.Display, phase: this.Phases.Display,
}, function() { }, function() {
@ -200,19 +200,16 @@ module.exports = React.createClass({
}, },
onBlur: function(ev) { onBlur: function(ev) {
var sel = window.getSelection(); const sel = window.getSelection();
sel.removeAllRanges(); sel.removeAllRanges();
if (this.props.blurToCancel) if (this.props.blurToCancel) {this.cancelEdit();} else {this.onFinish(ev, this.props.blurToSubmit);}
{this.cancelEdit();}
else
{this.onFinish(ev, this.props.blurToSubmit);}
this.showPlaceholder(!this.value); this.showPlaceholder(!this.value);
}, },
render: function() { render: function() {
var editable_el; let editable_el;
if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) { if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
// show the label // show the label
@ -224,5 +221,5 @@ module.exports = React.createClass({
} }
return editable_el; return editable_el;
} },
}); });

View file

@ -64,7 +64,7 @@ export default class EditableTextContainer extends React.Component {
errorString: error.toString(), errorString: error.toString(),
busy: false, busy: false,
}); });
} },
); );
} }
@ -96,22 +96,22 @@ export default class EditableTextContainer extends React.Component {
errorString: error.toString(), errorString: error.toString(),
busy: false, busy: false,
}); });
} },
); );
} }
render() { render() {
if (this.state.busy) { if (this.state.busy) {
var Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
return ( return (
<Loader /> <Loader />
); );
} else if (this.state.errorString) { } else if (this.state.errorString) {
return ( return (
<div className="error">{this.state.errorString}</div> <div className="error">{ this.state.errorString }</div>
); );
} else { } else {
var EditableText = sdk.getComponent('elements.EditableText'); const EditableText = sdk.getComponent('elements.EditableText');
return ( return (
<EditableText initialValue={this.state.value} <EditableText initialValue={this.state.value}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}

View file

@ -183,10 +183,12 @@ export default class Flair extends React.Component {
this.state = { this.state = {
profiles: [], profiles: [],
}; };
this.onRoomStateEvents = this.onRoomStateEvents.bind(this);
} }
componentWillUnmount() { componentWillUnmount() {
this._unmounted = true; this._unmounted = true;
this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents);
} }
componentWillMount() { componentWillMount() {
@ -194,6 +196,13 @@ export default class Flair extends React.Component {
if (UserSettingsStore.isFeatureEnabled('feature_groups') && groupSupport) { if (UserSettingsStore.isFeatureEnabled('feature_groups') && groupSupport) {
this._generateAvatars(); this._generateAvatars();
} }
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
}
onRoomStateEvents(event) {
if (event.getType() === 'm.room.related_groups' && groupSupport) {
this._generateAvatars();
}
} }
async _getGroupProfiles(groups) { async _getGroupProfiles(groups) {
@ -224,6 +233,21 @@ export default class Flair extends React.Component {
} }
console.error('Could not get groups for user', this.props.userId, err); console.error('Could not get groups for user', this.props.userId, err);
} }
if (this.props.roomId && this.props.showRelated) {
const relatedGroupsEvent = this.context.matrixClient
.getRoom(this.props.roomId)
.currentState
.getStateEvents('m.room.related_groups', '');
const relatedGroups = relatedGroupsEvent ?
relatedGroupsEvent.getContent().groups || [] : [];
if (relatedGroups && relatedGroups.length > 0) {
groups = groups.filter((groupId) => {
return relatedGroups.includes(groupId);
});
} else {
groups = [];
}
}
if (!groups || groups.length === 0) { if (!groups || groups.length === 0) {
return; return;
} }
@ -241,7 +265,7 @@ export default class Flair extends React.Component {
return <FlairAvatar key={index} groupProfile={profile} />; return <FlairAvatar key={index} groupProfile={profile} />;
}); });
return ( return (
<span className="mx_Flair" style={{"marginLeft": "5px", "verticalAlign": "-3px"}}> <span className="mx_Flair">
{ avatars } { avatars }
</span> </span>
); );
@ -250,6 +274,12 @@ export default class Flair extends React.Component {
Flair.propTypes = { Flair.propTypes = {
userId: PropTypes.string, userId: PropTypes.string,
// Whether to show only the flair associated with related groups of the given room,
// or all flair associated with a user.
showRelated: PropTypes.bool,
// The room that this flair will be displayed in. Optional. Only applies when showRelated = true.
roomId: PropTypes.string,
}; };
// TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using // TODO: We've decided that all components should follow this pattern, which means removing withMatrixClient and using

View file

@ -23,7 +23,7 @@ const HomeButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton'); const ActionButton = sdk.getComponent('elements.ActionButton');
return ( return (
<ActionButton action="view_home_page" <ActionButton action="view_home_page"
label={ _t("Home") } label={_t("Home")}
iconPath="img/icons-home.svg" iconPath="img/icons-home.svg"
size={props.size} size={props.size}
tooltip={props.tooltip} tooltip={props.tooltip}

View file

@ -35,12 +35,12 @@ export default class LanguageDropdown extends React.Component {
this.state = { this.state = {
searchQuery: '', searchQuery: '',
langs: null, langs: null,
} };
} }
componentWillMount() { componentWillMount() {
languageHandler.getAllLanguagesFromJson().then((langs) => { languageHandler.getAllLanguagesFromJson().then((langs) => {
langs.sort(function(a, b){ langs.sort(function(a, b) {
if(a.label < b.label) return -1; if(a.label < b.label) return -1;
if(a.label > b.label) return 1; if(a.label > b.label) return 1;
return 0; return 0;
@ -89,7 +89,7 @@ export default class LanguageDropdown extends React.Component {
const options = displayedLanguages.map((language) => { const options = displayedLanguages.map((language) => {
return <div key={language.value}> return <div key={language.value}>
{language.label} { language.label }
</div>; </div>;
}); });
@ -108,8 +108,8 @@ export default class LanguageDropdown extends React.Component {
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange} onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
searchEnabled={true} value={value} searchEnabled={true} value={value}
> >
{options} { options }
</Dropdown> </Dropdown>;
} }
} }

View file

@ -96,12 +96,12 @@ module.exports = React.createClass({
// Transform into consecutive repetitions of the same transition (like 5 // Transform into consecutive repetitions of the same transition (like 5
// consecutive 'joined_and_left's) // consecutive 'joined_and_left's)
const coalescedTransitions = this._coalesceRepeatedTransitions( const coalescedTransitions = this._coalesceRepeatedTransitions(
canonicalTransitions canonicalTransitions,
); );
const descs = coalescedTransitions.map((t) => { const descs = coalescedTransitions.map((t) => {
return this._getDescriptionForTransition( return this._getDescriptionForTransition(
t.transitionType, plural, t.repeats t.transitionType, plural, t.repeats,
); );
}); });
@ -119,7 +119,7 @@ module.exports = React.createClass({
return ( return (
<span className="mx_TextualEvent mx_MemberEventListSummary_summary"> <span className="mx_TextualEvent mx_MemberEventListSummary_summary">
<EmojiText> <EmojiText>
{summaries.join(", ")} { summaries.join(", ") }
</EmojiText> </EmojiText>
</span> </span>
); );
@ -370,7 +370,7 @@ module.exports = React.createClass({
*/ */
_renderCommaSeparatedList(items, itemLimit) { _renderCommaSeparatedList(items, itemLimit) {
const remaining = itemLimit === undefined ? 0 : Math.max( const remaining = itemLimit === undefined ? 0 : Math.max(
items.length - itemLimit, 0 items.length - itemLimit, 0,
); );
if (items.length === 0) { if (items.length === 0) {
return ""; return "";
@ -394,8 +394,8 @@ module.exports = React.createClass({
); );
}); });
return ( return (
<span className="mx_MemberEventListSummary_avatars" onClick={ this._toggleSummary }> <span className="mx_MemberEventListSummary_avatars" onClick={this._toggleSummary}>
{avatars} { avatars }
</span> </span>
); );
}, },
@ -419,19 +419,15 @@ module.exports = React.createClass({
case 'join': case 'join':
if (e.mxEvent.getPrevContent().membership === 'join') { if (e.mxEvent.getPrevContent().membership === 'join') {
if (e.mxEvent.getContent().displayname !== if (e.mxEvent.getContent().displayname !==
e.mxEvent.getPrevContent().displayname) e.mxEvent.getPrevContent().displayname) {
{
return 'changed_name'; return 'changed_name';
} } else if (e.mxEvent.getContent().avatar_url !==
else if (e.mxEvent.getContent().avatar_url !== e.mxEvent.getPrevContent().avatar_url) {
e.mxEvent.getPrevContent().avatar_url)
{
return 'changed_avatar'; return 'changed_avatar';
} }
// console.log("MELS ignoring duplicate membership join event"); // console.log("MELS ignoring duplicate membership join event");
return null; return null;
} } else {
else {
return 'joined'; return 'joined';
} }
case 'leave': case 'leave':
@ -483,7 +479,7 @@ module.exports = React.createClass({
firstEvent.index < aggregateIndices[seq]) { firstEvent.index < aggregateIndices[seq]) {
aggregateIndices[seq] = firstEvent.index; aggregateIndices[seq] = firstEvent.index;
} }
} },
); );
return { return {
@ -494,7 +490,7 @@ module.exports = React.createClass({
render: function() { render: function() {
const eventsToRender = this.props.events; const eventsToRender = this.props.events;
const eventIds = eventsToRender.map(e => e.getId()).join(','); const eventIds = eventsToRender.map((e) => e.getId()).join(',');
const fewEvents = eventsToRender.length < this.props.threshold; const fewEvents = eventsToRender.length < this.props.threshold;
const expanded = this.state.expanded || fewEvents; const expanded = this.state.expanded || fewEvents;
@ -506,7 +502,7 @@ module.exports = React.createClass({
if (fewEvents) { if (fewEvents) {
return ( return (
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}> <div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{expandedEvents} { expandedEvents }
</div> </div>
); );
} }
@ -542,7 +538,7 @@ module.exports = React.createClass({
// Sort types by order of lowest event index within sequence // Sort types by order of lowest event index within sequence
const orderedTransitionSequences = Object.keys(aggregate.names).sort( const orderedTransitionSequences = Object.keys(aggregate.names).sort(
(seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2] (seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2],
); );
let summaryContainer = null; let summaryContainer = null;
@ -550,24 +546,24 @@ module.exports = React.createClass({
summaryContainer = ( summaryContainer = (
<div className="mx_EventTile_line"> <div className="mx_EventTile_line">
<div className="mx_EventTile_info"> <div className="mx_EventTile_info">
{this._renderAvatars(avatarMembers)} { this._renderAvatars(avatarMembers) }
{this._renderSummary(aggregate.names, orderedTransitionSequences)} { this._renderSummary(aggregate.names, orderedTransitionSequences) }
</div> </div>
</div> </div>
); );
} }
const toggleButton = ( const toggleButton = (
<div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}> <div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
{expanded ? 'collapse' : 'expand'} { expanded ? 'collapse' : 'expand' }
</div> </div>
); );
return ( return (
<div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}> <div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
{toggleButton} { toggleButton }
{summaryContainer} { summaryContainer }
{expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null} { expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null }
{expandedEvents} { expandedEvents }
</div> </div>
); );
}, },

View file

@ -20,8 +20,8 @@ import React from 'react';
import * as Roles from '../../../Roles'; import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
var LEVEL_ROLE_MAP = {}; let LEVEL_ROLE_MAP = {};
var reverseRoles = {}; const reverseRoles = {};
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'PowerSelector', displayName: 'PowerSelector',
@ -46,7 +46,7 @@ module.exports = React.createClass({
custom: (LEVEL_ROLE_MAP[this.props.value] === undefined), custom: (LEVEL_ROLE_MAP[this.props.value] === undefined),
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
LEVEL_ROLE_MAP = Roles.levelRoleMap(); LEVEL_ROLE_MAP = Roles.levelRoleMap();
Object.keys(LEVEL_ROLE_MAP).forEach(function(key) { Object.keys(LEVEL_ROLE_MAP).forEach(function(key) {
@ -72,7 +72,7 @@ module.exports = React.createClass({
}, },
getValue: function() { getValue: function() {
var value; let value;
if (this.refs.select) { if (this.refs.select) {
value = reverseRoles[this.refs.select.value]; value = reverseRoles[this.refs.select.value];
if (this.refs.custom) { if (this.refs.custom) {
@ -83,30 +83,27 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var customPicker; let customPicker;
if (this.state.custom) { if (this.state.custom) {
var input; let input;
if (this.props.disabled) { if (this.props.disabled) {
input = <span>{ this.props.value }</span>; input = <span>{ this.props.value }</span>;
} } else {
else { input = <input ref="custom" type="text" size="3" defaultValue={this.props.value} onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} />;
input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/>;
} }
customPicker = <span> of { input }</span>; customPicker = <span> of { input }</span>;
} }
var selectValue; let selectValue;
if (this.state.custom) { if (this.state.custom) {
selectValue = "Custom"; selectValue = "Custom";
} } else {
else {
selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom"; selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom";
} }
var select; let select;
if (this.props.disabled) { if (this.props.disabled) {
select = <span>{ selectValue }</span>; select = <span>{ selectValue }</span>;
} } else {
else {
// Each level must have a definition in LEVEL_ROLE_MAP // Each level must have a definition in LEVEL_ROLE_MAP
const levels = [0, 50, 100]; const levels = [0, 50, 100];
let options = levels.map((level) => { let options = levels.map((level) => {
@ -115,18 +112,18 @@ module.exports = React.createClass({
// Give a userDefault (users_default in the power event) of 0 but // Give a userDefault (users_default in the power event) of 0 but
// because level !== undefined, this should never be used. // because level !== undefined, this should never be used.
text: Roles.textualPowerLevel(level, 0), text: Roles.textualPowerLevel(level, 0),
} };
}); });
options.push({ value: "Custom", text: _t("Custom level") }); options.push({ value: "Custom", text: _t("Custom level") });
options = options.map((op) => { options = options.map((op) => {
return <option value={op.value} key={op.value}>{op.text}</option>; return <option value={op.value} key={op.value}>{ op.text }</option>;
}); });
select = select =
<select ref="select" <select ref="select"
value={ this.props.controlled ? selectValue : undefined } value={this.props.controlled ? selectValue : undefined}
defaultValue={ !this.props.controlled ? selectValue : undefined } defaultValue={!this.props.controlled ? selectValue : undefined}
onChange={ this.onSelectChange }> onChange={this.onSelectChange}>
{ options } { options }
</select>; </select>;
} }
@ -137,5 +134,5 @@ module.exports = React.createClass({
{ customPicker } { customPicker }
</span> </span>
); );
} },
}); });

View file

@ -16,23 +16,23 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ProgressBar', displayName: 'ProgressBar',
propTypes: { propTypes: {
value: React.PropTypes.number, value: React.PropTypes.number,
max: React.PropTypes.number max: React.PropTypes.number,
}, },
render: function() { render: function() {
// Would use an HTML5 progress tag but if that doesn't animate if you // Would use an HTML5 progress tag but if that doesn't animate if you
// use the HTML attributes rather than styles // use the HTML attributes rather than styles
var progressStyle = { const progressStyle = {
width: ((this.props.value / this.props.max) * 100)+"%" width: ((this.props.value / this.props.max) * 100)+"%",
}; };
return ( return (
<div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div> <div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div>
); );
} },
}); });

View file

@ -24,7 +24,7 @@ const RoomDirectoryButton = function(props) {
return ( return (
<ActionButton action="view_room_directory" <ActionButton action="view_room_directory"
mouseOverAction={props.callout ? "callout_room_directory" : null} mouseOverAction={props.callout ? "callout_room_directory" : null}
label={ _t("Room directory") } label={_t("Room directory")}
iconPath="img/icons-directory.svg" iconPath="img/icons-directory.svg"
size={props.size} size={props.size}
tooltip={props.tooltip} tooltip={props.tooltip}

View file

@ -23,7 +23,7 @@ const SettingsButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton'); const ActionButton = sdk.getComponent('elements.ActionButton');
return ( return (
<ActionButton action="view_user_settings" <ActionButton action="view_user_settings"
label={ _t("Settings") } label={_t("Settings")}
iconPath="img/icons-settings.svg" iconPath="img/icons-settings.svg"
size={props.size} size={props.size}
tooltip={props.tooltip} tooltip={props.tooltip}

View file

@ -24,7 +24,7 @@ const StartChatButton = function(props) {
return ( return (
<ActionButton action="view_create_chat" <ActionButton action="view_create_chat"
mouseOverAction={props.callout ? "callout_start_chat" : null} mouseOverAction={props.callout ? "callout_start_chat" : null}
label={ _t("Start chat") } label={_t("Start chat")}
iconPath="img/icons-people.svg" iconPath="img/icons-people.svg"
size={props.size} size={props.size}
tooltip={props.tooltip} tooltip={props.tooltip}

View file

@ -16,9 +16,9 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
var ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
var Tinter = require("../../../Tinter"); const Tinter = require("../../../Tinter");
var TintableSvg = React.createClass({ var TintableSvg = React.createClass({
displayName: 'TintableSvg', displayName: 'TintableSvg',
@ -63,16 +63,16 @@ var TintableSvg = React.createClass({
render: function() { render: function() {
return ( return (
<object className={ "mx_TintableSvg " + (this.props.className ? this.props.className : "") } <object className={"mx_TintableSvg " + (this.props.className ? this.props.className : "")}
type="image/svg+xml" type="image/svg+xml"
data={ this.props.src } data={this.props.src}
width={ this.props.width } width={this.props.width}
height={ this.props.height } height={this.props.height}
onLoad={ this.onLoad } onLoad={this.onLoad}
tabIndex="-1" tabIndex="-1"
/> />
); );
} },
}); });
// Register with the Tinter so that we will be told if the tint changes // Register with the Tinter so that we will be told if the tint changes

View file

@ -52,19 +52,19 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var self = this; const self = this;
return ( return (
<div> <div>
<ul className="mx_UserSelector_UserIdList" ref="list"> <ul className="mx_UserSelector_UserIdList" ref="list">
{this.props.selected_users.map(function(user_id, i) { { this.props.selected_users.map(function(user_id, i) {
return <li key={user_id}>{user_id} - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>; return <li key={user_id}>{ user_id } - <span onClick={function() {self.removeUser(user_id);}}>X</span></li>;
})} }) }
</ul> </ul>
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")}/> <input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder={_t("ex. @bob:example.com")} />
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId"> <button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
{_t("Add User")} { _t("Add User") }
</button> </button>
</div> </div>
); );
} },
}); });

View file

@ -17,6 +17,7 @@ import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import { groupRoomFromApiObject } from '../../../groups'; import { groupRoomFromApiObject } from '../../../groups';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk'; import {MatrixClient} from 'matrix-js-sdk';
@ -34,7 +35,6 @@ export default React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
fetching: false,
rooms: null, rooms: null,
truncateAt: INITIAL_LOAD_NUM_ROOMS, truncateAt: INITIAL_LOAD_NUM_ROOMS,
searchQuery: "", searchQuery: "",
@ -43,21 +43,29 @@ export default React.createClass({
componentWillMount: function() { componentWillMount: function() {
this._unmounted = false; this._unmounted = false;
this._initGroupStore(this.props.groupId);
},
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
this._groupStore.on('update', () => {
this._fetchRooms();
});
this._groupStore.on('error', (err) => {
console.error('Error in group store (listened to by GroupRoomList)', err);
this.setState({
rooms: null,
});
});
this._fetchRooms(); this._fetchRooms();
}, },
_fetchRooms: function() { _fetchRooms: function() {
this.setState({fetching: true}); if (this._unmounted) return;
this.context.matrixClient.getGroupRooms(this.props.groupId).then((result) => { this.setState({
this.setState({ rooms: this._groupStore.getGroupRooms().map((apiRoom) => {
rooms: result.chunk.map((apiRoom) => { return groupRoomFromApiObject(apiRoom);
return groupRoomFromApiObject(apiRoom); }),
}),
fetching: false,
});
}).catch((e) => {
this.setState({fetching: false});
console.error("Failed to get group room list: ", e);
}); });
}, },
@ -110,12 +118,7 @@ export default React.createClass({
}, },
render: function() { render: function() {
if (this.state.fetching) { if (this.state.rooms === null) {
const Spinner = sdk.getComponent("elements.Spinner");
return (<div className="mx_GroupRoomList">
<Spinner />
</div>);
} else if (this.state.rooms === null) {
return null; return null;
} }

View file

@ -21,6 +21,8 @@ import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import { GroupRoomType } from '../../../groups'; import { GroupRoomType } from '../../../groups';
import GroupStoreCache from '../../../stores/GroupStoreCache';
import Modal from '../../../Modal';
const GroupRoomTile = React.createClass({ const GroupRoomTile = React.createClass({
displayName: 'GroupRoomTile', displayName: 'GroupRoomTile',
@ -31,7 +33,35 @@ const GroupRoomTile = React.createClass({
}, },
getInitialState: function() { getInitialState: function() {
return {}; return {
name: this.calculateRoomName(this.props.groupRoom),
};
},
componentWillReceiveProps: function(newProps) {
this.setState({
name: this.calculateRoomName(newProps.groupRoom),
});
},
calculateRoomName: function(groupRoom) {
return groupRoom.name || groupRoom.canonicalAlias || _t("Unnamed Room");
},
removeRoomFromGroup: function() {
const groupId = this.props.groupId;
const groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
const roomName = this.state.name;
const roomId = this.props.groupRoom.roomId;
groupStore.removeRoomFromGroup(roomId)
.catch((err) => {
console.error(`Error whilst removing ${roomId} from ${groupId}`, err);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
title: _t("Failed to remove room from group"),
description: _t("Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName}),
});
});
}, },
onClick: function(e) { onClick: function(e) {
@ -49,20 +79,34 @@ const GroupRoomTile = React.createClass({
}); });
}, },
onDeleteClick: function(e) {
const groupId = this.props.groupId;
const roomName = this.state.name;
e.preventDefault();
e.stopPropagation();
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Confirm removal of group from room', '', QuestionDialog, {
title: _t("Are you sure you want to remove '%(roomName)s' from %(groupId)s?", {roomName, groupId}),
description: _t("Removing a room from the group will also remove it from the group page."),
button: _t("Remove"),
onFinished: (success) => {
if (success) {
this.removeRoomFromGroup();
}
},
});
},
render: function() { render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const name = this.props.groupRoom.name ||
this.props.groupRoom.canonicalAlias ||
_t("Unnamed Room");
const avatarUrl = this.context.matrixClient.mxcUrlToHttp( const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
this.props.groupRoom.avatarUrl, this.props.groupRoom.avatarUrl,
36, 36, 'crop', 36, 36, 'crop',
); );
const av = ( const av = (
<BaseAvatar name={name} <BaseAvatar name={this.state.name}
width={36} height={36} width={36} height={36}
url={avatarUrl} url={avatarUrl}
/> />
@ -74,8 +118,11 @@ const GroupRoomTile = React.createClass({
{ av } { av }
</div> </div>
<div className="mx_GroupRoomTile_name"> <div className="mx_GroupRoomTile_name">
{ name } { this.state.name }
</div> </div>
<AccessibleButton className="mx_GroupRoomTile_delete" onClick={this.onDeleteClick}>
<img src="img/cancel-small.svg" />
</AccessibleButton>
</AccessibleButton> </AccessibleButton>
); );
}, },

View file

@ -20,7 +20,7 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { _t, _tJsx } from '../../../languageHandler'; import { _t, _tJsx } from '../../../languageHandler';
var DIV_ID = 'mx_recaptcha'; const DIV_ID = 'mx_recaptcha';
/** /**
* A pure UI component which displays a captcha form. * A pure UI component which displays a captcha form.
@ -60,9 +60,9 @@ module.exports = React.createClass({
} else { } else {
console.log("Loading recaptcha script..."); console.log("Loading recaptcha script...");
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();}; window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
var protocol = global.location.protocol; const protocol = global.location.protocol;
if (protocol === "file:") { if (protocol === "file:") {
var warning = document.createElement('div'); const warning = document.createElement('div');
// XXX: fix hardcoded app URL. Better solutions include: // XXX: fix hardcoded app URL. Better solutions include:
// * jumping straight to a hosted captcha page (but we don't support that yet) // * jumping straight to a hosted captcha page (but we don't support that yet)
// * embedding the captcha in an iframe (if that works) // * embedding the captcha in an iframe (if that works)
@ -72,11 +72,10 @@ module.exports = React.createClass({
/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/,
(sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }), warning); (sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }), warning);
this.refs.recaptchaContainer.appendChild(warning); this.refs.recaptchaContainer.appendChild(warning);
} } else {
else { const scriptTag = document.createElement('script');
var scriptTag = document.createElement('script');
scriptTag.setAttribute( scriptTag.setAttribute(
'src', protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit" 'src', protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit",
); );
this.refs.recaptchaContainer.appendChild(scriptTag); this.refs.recaptchaContainer.appendChild(scriptTag);
} }
@ -93,7 +92,7 @@ module.exports = React.createClass({
throw new Error("Recaptcha did not load successfully"); throw new Error("Recaptcha did not load successfully");
} }
var publicKey = this.props.sitePublicKey; const publicKey = this.props.sitePublicKey;
if (!publicKey) { if (!publicKey) {
console.error("No public key for recaptcha!"); console.error("No public key for recaptcha!");
throw new Error( throw new Error(
@ -130,18 +129,18 @@ module.exports = React.createClass({
if (this.state.errorText) { if (this.state.errorText) {
error = ( error = (
<div className="error"> <div className="error">
{this.state.errorText} { this.state.errorText }
</div> </div>
); );
} }
return ( return (
<div ref="recaptchaContainer"> <div ref="recaptchaContainer">
{_t("This Home Server would like to make sure you are not a robot")} { _t("This Home Server would like to make sure you are not a robot") }
<br/> <br />
<div id={DIV_ID}></div> <div id={DIV_ID}></div>
{error} { error }
</div> </div>
); );
} },
}); });

View file

@ -29,9 +29,9 @@ module.exports = React.createClass({
render: function() { render: function() {
return ( return (
<div> <div>
<button onClick={this.props.onSubmit}>{_t("Sign in with CAS")}</button> <button onClick={this.props.onSubmit}>{ _t("Sign in with CAS") }</button>
</div> </div>
); );
} },
}); });

View file

@ -69,7 +69,7 @@ export default class CountryDropdown extends React.Component {
} }
_flagImgForIso2(iso2) { _flagImgForIso2(iso2) {
return <img src={`flags/${iso2}.png`}/>; return <img src={`flags/${iso2}.png`} />;
} }
_getShortOption(iso2) { _getShortOption(iso2) {
@ -111,8 +111,8 @@ export default class CountryDropdown extends React.Component {
const options = displayedCountries.map((country) => { const options = displayedCountries.map((country) => {
return <div key={country.iso2}> return <div key={country.iso2}>
{this._flagImgForIso2(country.iso2)} { this._flagImgForIso2(country.iso2) }
{country.name} <span>(+{country.prefix})</span> { country.name } <span>(+{ country.prefix })</span>
</div>; </div>;
}); });
@ -123,9 +123,9 @@ export default class CountryDropdown extends React.Component {
return <Dropdown className={this.props.className + " left_aligned"} return <Dropdown className={this.props.className + " left_aligned"}
onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange} onOptionChange={this._onOptionChange} onSearchChange={this._onSearchChange}
menuWidth={298} getShortOption={this._getShortOption} menuWidth={298} getShortOption={this._getShortOption}
value={value} searchEnabled={true} value={value} searchEnabled={true} disabled={this.props.disabled}
> >
{options} { options }
</Dropdown>; </Dropdown>;
} }
} }
@ -137,4 +137,5 @@ CountryDropdown.propTypes = {
showPrefix: React.PropTypes.bool, showPrefix: React.PropTypes.bool,
onOptionChange: React.PropTypes.func.isRequired, onOptionChange: React.PropTypes.func.isRequired,
value: React.PropTypes.string, value: React.PropTypes.string,
disabled: React.PropTypes.bool,
}; };

View file

@ -24,27 +24,27 @@ module.exports = React.createClass({
return ( return (
<div className="mx_ErrorDialog"> <div className="mx_ErrorDialog">
<div className="mx_Dialog_title"> <div className="mx_Dialog_title">
{_t("Custom Server Options")} { _t("Custom Server Options") }
</div> </div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<span> <span>
{_t("You can use the custom server options to sign into other Matrix " + { _t("You can use the custom server options to sign into other Matrix " +
"servers by specifying a different Home server URL.")} "servers by specifying a different Home server URL.") }
<br/> <br />
{_t("This allows you to use this app with an existing Matrix account on " + { _t("This allows you to use this app with an existing Matrix account on " +
"a different home server.")} "a different home server.") }
<br/> <br />
<br/> <br />
{_t("You can also set a custom identity server but this will typically prevent " + { _t("You can also set a custom identity server but this will typically prevent " +
"interaction with users based on email address.")} "interaction with users based on email address.") }
</span> </span>
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished} autoFocus={true}> <button onClick={this.props.onFinished} autoFocus={true}>
{_t("Dismiss")} { _t("Dismiss") }
</button> </button>
</div> </div>
</div> </div>
); );
} },
}); });

View file

@ -129,8 +129,8 @@ export const PasswordAuthEntry = React.createClass({
return ( return (
<div> <div>
<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>
<form onSubmit={this._onSubmit}> <form onSubmit={this._onSubmit}>
<input <input
ref="passwordField" ref="passwordField"
@ -139,11 +139,11 @@ export const PasswordAuthEntry = React.createClass({
type="password" type="password"
/> />
<div className="mx_button_row"> <div className="mx_button_row">
{submitButtonOrSpinner} { submitButtonOrSpinner }
</div> </div>
</form> </form>
<div className="error"> <div className="error">
{this.props.errorText} { this.props.errorText }
</div> </div>
</div> </div>
); );
@ -178,14 +178,14 @@ export const RecaptchaAuthEntry = React.createClass({
} }
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm"); const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
var sitePublicKey = this.props.stageParams.public_key; const sitePublicKey = this.props.stageParams.public_key;
return ( return (
<div> <div>
<CaptchaForm sitePublicKey={sitePublicKey} <CaptchaForm sitePublicKey={sitePublicKey}
onCaptchaResponse={this._onCaptchaResponse} onCaptchaResponse={this._onCaptchaResponse}
/> />
<div className="error"> <div className="error">
{this.props.errorText} { this.props.errorText }
</div> </div>
</div> </div>
); );
@ -256,8 +256,8 @@ export const EmailIdentityAuthEntry = React.createClass({
} else { } else {
return ( return (
<div> <div>
<p>{_t("An email has been sent to")} <i>{this.props.inputs.emailAddress}</i></p> <p>{ _t("An email has been sent to") } <i>{ this.props.inputs.emailAddress }</i></p>
<p>{_t("Please check your email to continue registration.")}</p> <p>{ _t("Please check your email to continue registration.") }</p>
</div> </div>
); );
} }
@ -333,12 +333,12 @@ export const MsisdnAuthEntry = React.createClass({
}); });
this.props.matrixClient.submitMsisdnToken( this.props.matrixClient.submitMsisdnToken(
this._sid, this.props.clientSecret, this.state.token this._sid, this.props.clientSecret, this.state.token,
).then((result) => { ).then((result) => {
if (result.success) { if (result.success) {
const idServerParsedUrl = url.parse( const idServerParsedUrl = url.parse(
this.props.matrixClient.getIdentityServerUrl(), this.props.matrixClient.getIdentityServerUrl(),
) );
this.props.submitAuthDict({ this.props.submitAuthDict({
type: MsisdnAuthEntry.LOGIN_TYPE, type: MsisdnAuthEntry.LOGIN_TYPE,
threepid_creds: { threepid_creds: {
@ -370,8 +370,8 @@ export const MsisdnAuthEntry = React.createClass({
}); });
return ( return (
<div> <div>
<p>{_t("A text message has been sent to")} +<i>{this._msisdn}</i></p> <p>{ _t("A text message has been sent to") } +<i>{ this._msisdn }</i></p>
<p>{_t("Please enter the code it contains:")}</p> <p>{ _t("Please enter the code it contains:") }</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper"> <div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}> <form onSubmit={this._onFormSubmit}>
<input type="text" <input type="text"
@ -386,7 +386,7 @@ export const MsisdnAuthEntry = React.createClass({
/> />
</form> </form>
<div className="error"> <div className="error">
{this.state.errorText} { this.state.errorText }
</div> </div>
</div> </div>
</div> </div>
@ -421,9 +421,9 @@ export const FallbackAuthEntry = React.createClass({
}, },
_onShowFallbackClick: function() { _onShowFallbackClick: function() {
var url = this.props.matrixClient.getFallbackAuthUrl( const url = this.props.matrixClient.getFallbackAuthUrl(
this.props.loginType, this.props.loginType,
this.props.authSessionId this.props.authSessionId,
); );
this._popupWindow = window.open(url); this._popupWindow = window.open(url);
}, },
@ -440,9 +440,9 @@ export const FallbackAuthEntry = React.createClass({
render: function() { render: function() {
return ( return (
<div> <div>
<a onClick={this._onShowFallbackClick}>{_t("Start authentication")}</a> <a onClick={this._onShowFallbackClick}>{ _t("Start authentication") }</a>
<div className="error"> <div className="error">
{this.props.errorText} { this.props.errorText }
</div> </div>
</div> </div>
); );
@ -457,7 +457,7 @@ const AuthEntryComponents = [
]; ];
export function getEntryComponentForLoginType(loginType) { export function getEntryComponentForLoginType(loginType) {
for (var c of AuthEntryComponents) { for (const c of AuthEntryComponents) {
if (c.LOGIN_TYPE == loginType) { if (c.LOGIN_TYPE == loginType) {
return c; return c;
} }

View file

@ -16,7 +16,7 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'LoginHeader', displayName: 'LoginHeader',
@ -27,5 +27,5 @@ module.exports = React.createClass({
Matrix Matrix
</div> </div>
); );
} },
}); });

View file

@ -94,7 +94,7 @@ class PasswordLogin extends React.Component {
onLoginTypeChange(loginType) { onLoginTypeChange(loginType) {
this.setState({ this.setState({
loginType: loginType, loginType: loginType,
username: "" // Reset because email and username use the same state username: "", // Reset because email and username use the same state
}); });
} }
@ -116,11 +116,17 @@ class PasswordLogin extends React.Component {
this.props.onPasswordChanged(ev.target.value); this.props.onPasswordChanged(ev.target.value);
} }
renderLoginField(loginType) { renderLoginField(loginType, disabled) {
const classes = {
mx_Login_field: true,
mx_Login_field_disabled: disabled,
};
switch(loginType) { switch(loginType) {
case PasswordLogin.LOGIN_FIELD_EMAIL: case PasswordLogin.LOGIN_FIELD_EMAIL:
classes.mx_Login_email = true;
return <input return <input
className="mx_Login_field mx_Login_email" className={classNames(classes)}
key="email_input" key="email_input"
type="text" type="text"
name="username" // make it a little easier for browser's remember-password name="username" // make it a little easier for browser's remember-password
@ -128,10 +134,12 @@ class PasswordLogin extends React.Component {
placeholder="joe@example.com" placeholder="joe@example.com"
value={this.state.username} value={this.state.username}
autoFocus autoFocus
disabled={disabled}
/>; />;
case PasswordLogin.LOGIN_FIELD_MXID: case PasswordLogin.LOGIN_FIELD_MXID:
classes.mx_Login_username = true;
return <input return <input
className="mx_Login_field mx_Login_username" className={classNames(classes)}
key="username_input" key="username_input"
type="text" type="text"
name="username" // make it a little easier for browser's remember-password name="username" // make it a little easier for browser's remember-password
@ -139,9 +147,12 @@ class PasswordLogin extends React.Component {
placeholder={_t('User name')} placeholder={_t('User name')}
value={this.state.username} value={this.state.username}
autoFocus autoFocus
disabled={disabled}
/>; />;
case PasswordLogin.LOGIN_FIELD_PHONE: case PasswordLogin.LOGIN_FIELD_PHONE:
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
classes.mx_Login_phoneNumberField = true;
classes.mx_Login_field_has_prefix = true;
return <div className="mx_Login_phoneSection"> return <div className="mx_Login_phoneSection">
<CountryDropdown <CountryDropdown
className="mx_Login_phoneCountry mx_Login_field_prefix" className="mx_Login_phoneCountry mx_Login_field_prefix"
@ -150,9 +161,10 @@ class PasswordLogin extends React.Component {
value={this.state.phoneCountry} value={this.state.phoneCountry}
isSmall={true} isSmall={true}
showPrefix={true} showPrefix={true}
disabled={disabled}
/> />
<input <input
className="mx_Login_phoneNumberField mx_Login_field mx_Login_field_has_prefix" className={classNames(classes)}
ref="phoneNumber" ref="phoneNumber"
key="phone_input" key="phone_input"
type="text" type="text"
@ -161,13 +173,14 @@ class PasswordLogin extends React.Component {
placeholder={_t("Mobile phone number")} placeholder={_t("Mobile phone number")}
value={this.state.phoneNumber} value={this.state.phoneNumber}
autoFocus autoFocus
disabled={disabled}
/> />
</div>; </div>;
} }
} }
render() { render() {
var forgotPasswordJsx; let forgotPasswordJsx;
if (this.props.onForgotPasswordClick) { if (this.props.onForgotPasswordClick) {
forgotPasswordJsx = ( forgotPasswordJsx = (
@ -177,14 +190,25 @@ class PasswordLogin extends React.Component {
); );
} }
let matrixIdText = '';
if (this.props.hsUrl) {
try {
const parsedHsUrl = new URL(this.props.hsUrl);
matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname});
} catch (e) {
// pass
}
}
const pwFieldClass = classNames({ const pwFieldClass = classNames({
mx_Login_field: true, mx_Login_field: true,
mx_Login_field_disabled: matrixIdText === '',
error: this.props.loginIncorrect, error: this.props.loginIncorrect,
}); });
const Dropdown = sdk.getComponent('elements.Dropdown'); const Dropdown = sdk.getComponent('elements.Dropdown');
const loginField = this.renderLoginField(this.state.loginType); const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
return ( return (
<div> <div>
@ -194,20 +218,23 @@ class PasswordLogin extends React.Component {
<Dropdown <Dropdown
className="mx_Login_type_dropdown" className="mx_Login_type_dropdown"
value={this.state.loginType} value={this.state.loginType}
disabled={matrixIdText === ''}
onOptionChange={this.onLoginTypeChange}> onOptionChange={this.onLoginTypeChange}>
<span key={PasswordLogin.LOGIN_FIELD_MXID}>{ _t('my Matrix ID') }</span> <span key={PasswordLogin.LOGIN_FIELD_MXID}>{ matrixIdText }</span>
<span key={PasswordLogin.LOGIN_FIELD_EMAIL}>{ _t('Email address') }</span> <span key={PasswordLogin.LOGIN_FIELD_EMAIL}>{ _t('Email address') }</span>
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span> <span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
</Dropdown> </Dropdown>
</div> </div>
{loginField} { loginField }
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password" <input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
name="password" name="password"
value={this.state.password} onChange={this.onPasswordChanged} value={this.state.password} onChange={this.onPasswordChanged}
placeholder={ _t('Password') } /> placeholder={_t('Password')}
disabled={matrixIdText === ''}
/>
<br /> <br />
{forgotPasswordJsx} { forgotPasswordJsx }
<input className="mx_Login_submit" type="submit" value={ _t('Sign in') } /> <input className="mx_Login_submit" type="submit" value={_t('Sign in')} disabled={matrixIdText === ''} />
</form> </form>
</div> </div>
); );

View file

@ -64,7 +64,7 @@ module.exports = React.createClass({
minPasswordLength: 6, minPasswordLength: 6,
onError: function(e) { onError: function(e) {
console.error(e); console.error(e);
} },
}; };
}, },
@ -91,16 +91,16 @@ module.exports = React.createClass({
this.validateField(FIELD_PHONE_NUMBER); this.validateField(FIELD_PHONE_NUMBER);
this.validateField(FIELD_EMAIL); this.validateField(FIELD_EMAIL);
var self = this; const self = this;
if (this.allFieldsValid()) { if (this.allFieldsValid()) {
if (this.refs.email.value == '') { if (this.refs.email.value == '') {
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, { Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
title: _t("Warning!"), title: _t("Warning!"),
description: description:
<div> <div>
{_t("If you don't specify an email address, you won't be able to reset your password. " + { _t("If you don't specify an email address, you won't be able to reset your password. " +
"Are you sure?")} "Are you sure?") }
</div>, </div>,
button: _t("Continue"), button: _t("Continue"),
onFinished: function(confirmed) { onFinished: function(confirmed) {
@ -116,8 +116,8 @@ module.exports = React.createClass({
}, },
_doSubmit: function(ev) { _doSubmit: function(ev) {
let email = this.refs.email.value.trim(); const email = this.refs.email.value.trim();
var promise = this.props.onRegisterClick({ const promise = this.props.onRegisterClick({
username: this.refs.username.value.trim(), username: this.refs.username.value.trim(),
password: this.refs.password.value.trim(), password: this.refs.password.value.trim(),
email: email, email: email,
@ -138,8 +138,8 @@ module.exports = React.createClass({
* they were validated. * they were validated.
*/ */
allFieldsValid: function() { allFieldsValid: function() {
var keys = Object.keys(this.state.fieldValid); const keys = Object.keys(this.state.fieldValid);
for (var i = 0; i < keys.length; ++i) { for (let i = 0; i < keys.length; ++i) {
if (this.state.fieldValid[keys[i]] == false) { if (this.state.fieldValid[keys[i]] == false) {
return false; return false;
} }
@ -152,8 +152,8 @@ module.exports = React.createClass({
}, },
validateField: function(field_id) { validateField: function(field_id) {
var pwd1 = this.refs.password.value.trim(); const pwd1 = this.refs.password.value.trim();
var pwd2 = this.refs.passwordConfirm.value.trim(); const pwd2 = this.refs.passwordConfirm.value.trim();
switch (field_id) { switch (field_id) {
case FIELD_EMAIL: case FIELD_EMAIL:
@ -162,7 +162,7 @@ module.exports = React.createClass({
const matchingTeam = this.props.teamsConfig.teams.find( const matchingTeam = this.props.teamsConfig.teams.find(
(team) => { (team) => {
return email.split('@').pop() === team.domain; return email.split('@').pop() === team.domain;
} },
) || null; ) || null;
this.setState({ this.setState({
selectedTeam: matchingTeam, selectedTeam: matchingTeam,
@ -191,13 +191,13 @@ module.exports = React.createClass({
this.markFieldValid( this.markFieldValid(
field_id, field_id,
false, false,
"RegistrationForm.ERR_USERNAME_INVALID" "RegistrationForm.ERR_USERNAME_INVALID",
); );
} else if (username == '') { } else if (username == '') {
this.markFieldValid( this.markFieldValid(
field_id, field_id,
false, false,
"RegistrationForm.ERR_USERNAME_BLANK" "RegistrationForm.ERR_USERNAME_BLANK",
); );
} else { } else {
this.markFieldValid(field_id, true); this.markFieldValid(field_id, true);
@ -208,13 +208,13 @@ module.exports = React.createClass({
this.markFieldValid( this.markFieldValid(
field_id, field_id,
false, false,
"RegistrationForm.ERR_PASSWORD_MISSING" "RegistrationForm.ERR_PASSWORD_MISSING",
); );
} else if (pwd1.length < this.props.minPasswordLength) { } else if (pwd1.length < this.props.minPasswordLength) {
this.markFieldValid( this.markFieldValid(
field_id, field_id,
false, false,
"RegistrationForm.ERR_PASSWORD_LENGTH" "RegistrationForm.ERR_PASSWORD_LENGTH",
); );
} else { } else {
this.markFieldValid(field_id, true); this.markFieldValid(field_id, true);
@ -223,14 +223,14 @@ module.exports = React.createClass({
case FIELD_PASSWORD_CONFIRM: case FIELD_PASSWORD_CONFIRM:
this.markFieldValid( this.markFieldValid(
field_id, pwd1 == pwd2, field_id, pwd1 == pwd2,
"RegistrationForm.ERR_PASSWORD_MISMATCH" "RegistrationForm.ERR_PASSWORD_MISMATCH",
); );
break; break;
} }
}, },
markFieldValid: function(field_id, val, error_code) { markFieldValid: function(field_id, val, error_code) {
var fieldValid = this.state.fieldValid; const fieldValid = this.state.fieldValid;
fieldValid[field_id] = val; fieldValid[field_id] = val;
this.setState({fieldValid: fieldValid}); this.setState({fieldValid: fieldValid});
if (!val) { if (!val) {
@ -271,7 +271,7 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var self = this; const self = this;
const emailSection = ( const emailSection = (
<div> <div>
@ -280,7 +280,7 @@ module.exports = React.createClass({
defaultValue={this.props.defaultEmail} defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')} className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}} onBlur={function() {self.validateField(FIELD_EMAIL);}}
value={self.state.email}/> value={self.state.email} />
</div> </div>
); );
let belowEmailSection; let belowEmailSection;
@ -291,7 +291,7 @@ module.exports = React.createClass({
Sorry, but your university is not registered with us just yet.&nbsp; Sorry, but your university is not registered with us just yet.&nbsp;
Email us on&nbsp; Email us on&nbsp;
<a href={"mailto:" + this.props.teamsConfig.supportEmail}> <a href={"mailto:" + this.props.teamsConfig.supportEmail}>
{this.props.teamsConfig.supportEmail} { this.props.teamsConfig.supportEmail }
</a>&nbsp; </a>&nbsp;
to get your university signed up. Or continue to register with Riot to enjoy our open source platform. to get your university signed up. Or continue to register with Riot to enjoy our open source platform.
</p> </p>
@ -299,7 +299,7 @@ module.exports = React.createClass({
} else if (this.state.selectedTeam) { } else if (this.state.selectedTeam) {
belowEmailSection = ( belowEmailSection = (
<p className="mx_Login_support"> <p className="mx_Login_support">
{_t("You are registering with %(SelectedTeamName)s", {SelectedTeamName: this.state.selectedTeam.name})} { _t("You are registering with %(SelectedTeamName)s", {SelectedTeamName: this.state.selectedTeam.name}) }
</p> </p>
); );
} }
@ -321,7 +321,7 @@ module.exports = React.createClass({
FIELD_PHONE_NUMBER, FIELD_PHONE_NUMBER,
'mx_Login_phoneNumberField', 'mx_Login_phoneNumberField',
'mx_Login_field', 'mx_Login_field',
'mx_Login_field_has_prefix' 'mx_Login_field_has_prefix',
)} )}
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}} onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
value={self.state.phoneNumber} value={self.state.phoneNumber}
@ -333,16 +333,16 @@ module.exports = React.createClass({
<input className="mx_Login_submit" type="submit" value={_t("Register")} /> <input className="mx_Login_submit" type="submit" value={_t("Register")} />
); );
let placeholderUserName = _t("User name"); const placeholderUserName = _t("User name");
return ( return (
<div> <div>
<form onSubmit={this.onSubmit}> <form onSubmit={this.onSubmit}>
{emailSection} { emailSection }
{belowEmailSection} { belowEmailSection }
{phoneSection} { phoneSection }
<input type="text" ref="username" <input type="text" ref="username"
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername} placeholder={placeholderUserName} defaultValue={this.props.defaultUsername}
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')} className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_USERNAME);}} /> onBlur={function() {self.validateField(FIELD_USERNAME);}} />
<br /> <br />
@ -357,9 +357,9 @@ module.exports = React.createClass({
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM);}} onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM);}}
defaultValue={this.props.defaultPassword} /> defaultValue={this.props.defaultPassword} />
<br /> <br />
{registerButton} { registerButton }
</form> </form>
</div> </div>
); );
} },
}); });

View file

@ -16,9 +16,9 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); const React = require('react');
var Modal = require('../../../Modal'); const Modal = require('../../../Modal');
var sdk = require('../../../index'); const sdk = require('../../../index');
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
/** /**
@ -45,7 +45,7 @@ module.exports = React.createClass({
customIsUrl: React.PropTypes.string, customIsUrl: React.PropTypes.string,
withToggleButton: React.PropTypes.bool, withToggleButton: React.PropTypes.bool,
delayTimeMs: React.PropTypes.number // time to wait before invoking onChanged delayTimeMs: React.PropTypes.number, // time to wait before invoking onChanged
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -54,7 +54,7 @@ module.exports = React.createClass({
customHsUrl: "", customHsUrl: "",
customIsUrl: "", customIsUrl: "",
withToggleButton: false, withToggleButton: false,
delayTimeMs: 0 delayTimeMs: 0,
}; };
}, },
@ -65,18 +65,18 @@ module.exports = React.createClass({
// if withToggleButton is false, then show the config all the time given we have no way otherwise of making it visible // if withToggleButton is false, then show the config all the time given we have no way otherwise of making it visible
configVisible: !this.props.withToggleButton || configVisible: !this.props.withToggleButton ||
(this.props.customHsUrl !== this.props.defaultHsUrl) || (this.props.customHsUrl !== this.props.defaultHsUrl) ||
(this.props.customIsUrl !== this.props.defaultIsUrl) (this.props.customIsUrl !== this.props.defaultIsUrl),
}; };
}, },
onHomeserverChanged: function(ev) { onHomeserverChanged: function(ev) {
this.setState({hs_url: ev.target.value}, function() { this.setState({hs_url: ev.target.value}, function() {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
var hsUrl = this.state.hs_url.trim().replace(/\/$/, ""); let hsUrl = this.state.hs_url.trim().replace(/\/$/, "");
if (hsUrl === "") hsUrl = this.props.defaultHsUrl; if (hsUrl === "") hsUrl = this.props.defaultHsUrl;
this.props.onServerConfigChange({ this.props.onServerConfigChange({
hsUrl : this.state.hs_url, hsUrl: this.state.hs_url,
isUrl : this.state.is_url, isUrl: this.state.is_url,
}); });
}); });
}); });
@ -85,11 +85,11 @@ module.exports = React.createClass({
onIdentityServerChanged: function(ev) { onIdentityServerChanged: function(ev) {
this.setState({is_url: ev.target.value}, function() { this.setState({is_url: ev.target.value}, function() {
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() { this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
var isUrl = this.state.is_url.trim().replace(/\/$/, ""); let isUrl = this.state.is_url.trim().replace(/\/$/, "");
if (isUrl === "") isUrl = this.props.defaultIsUrl; if (isUrl === "") isUrl = this.props.defaultIsUrl;
this.props.onServerConfigChange({ this.props.onServerConfigChange({
hsUrl : this.state.hs_url, hsUrl: this.state.hs_url,
isUrl : this.state.is_url, isUrl: this.state.is_url,
}); });
}); });
}); });
@ -104,32 +104,31 @@ module.exports = React.createClass({
onServerConfigVisibleChange: function(visible, ev) { onServerConfigVisibleChange: function(visible, ev) {
this.setState({ this.setState({
configVisible: visible configVisible: visible,
}); });
if (!visible) { if (!visible) {
this.props.onServerConfigChange({ this.props.onServerConfigChange({
hsUrl : this.props.defaultHsUrl, hsUrl: this.props.defaultHsUrl,
isUrl : this.props.defaultIsUrl, isUrl: this.props.defaultIsUrl,
}); });
} } else {
else {
this.props.onServerConfigChange({ this.props.onServerConfigChange({
hsUrl : this.state.hs_url, hsUrl: this.state.hs_url,
isUrl : this.state.is_url, isUrl: this.state.is_url,
}); });
} }
}, },
showHelpPopup: function() { showHelpPopup: function() {
var CustomServerDialog = sdk.getComponent('login.CustomServerDialog'); const CustomServerDialog = sdk.getComponent('login.CustomServerDialog');
Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog); Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog);
}, },
render: function() { render: function() {
var serverConfigStyle = {}; const serverConfigStyle = {};
serverConfigStyle.display = this.state.configVisible ? 'block' : 'none'; serverConfigStyle.display = this.state.configVisible ? 'block' : 'none';
var toggleButton; let toggleButton;
if (this.props.withToggleButton) { if (this.props.withToggleButton) {
toggleButton = ( toggleButton = (
<div className="mx_ServerConfig_selector"> <div className="mx_ServerConfig_selector">
@ -137,14 +136,14 @@ module.exports = React.createClass({
checked={!this.state.configVisible} checked={!this.state.configVisible}
onChange={this.onServerConfigVisibleChange.bind(this, false)} /> onChange={this.onServerConfigVisibleChange.bind(this, false)} />
<label className="mx_Login_label" htmlFor="basic"> <label className="mx_Login_label" htmlFor="basic">
{_t("Default server")} { _t("Default server") }
</label> </label>
&nbsp;&nbsp; &nbsp;&nbsp;
<input className="mx_Login_radio" id="advanced" name="configVisible" type="radio" <input className="mx_Login_radio" id="advanced" name="configVisible" type="radio"
checked={this.state.configVisible} checked={this.state.configVisible}
onChange={this.onServerConfigVisibleChange.bind(this, true)} /> onChange={this.onServerConfigVisibleChange.bind(this, true)} />
<label className="mx_Login_label" htmlFor="advanced"> <label className="mx_Login_label" htmlFor="advanced">
{_t("Custom server")} { _t("Custom server") }
</label> </label>
</div> </div>
); );
@ -152,11 +151,11 @@ module.exports = React.createClass({
return ( return (
<div> <div>
{toggleButton} { toggleButton }
<div style={serverConfigStyle}> <div style={serverConfigStyle}>
<div className="mx_ServerConfig"> <div className="mx_ServerConfig">
<label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl"> <label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl">
{_t("Home server URL")} { _t("Home server URL") }
</label> </label>
<input className="mx_Login_field" id="hsurl" type="text" <input className="mx_Login_field" id="hsurl" type="text"
placeholder={this.props.defaultHsUrl} placeholder={this.props.defaultHsUrl}
@ -164,7 +163,7 @@ module.exports = React.createClass({
value={this.state.hs_url} value={this.state.hs_url}
onChange={this.onHomeserverChanged} /> onChange={this.onHomeserverChanged} />
<label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl"> <label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl">
{_t("Identity server URL")} { _t("Identity server URL") }
</label> </label>
<input className="mx_Login_field" id="isurl" type="text" <input className="mx_Login_field" id="isurl" type="text"
placeholder={this.props.defaultIsUrl} placeholder={this.props.defaultIsUrl}
@ -172,11 +171,11 @@ module.exports = React.createClass({
value={this.state.is_url} value={this.state.is_url}
onChange={this.onIdentityServerChanged} /> onChange={this.onIdentityServerChanged} />
<a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}> <a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
{_t("What does this mean?")} { _t("What does this mean?") }
</a> </a>
</div> </div>
</div> </div>
</div> </div>
); );
} },
}); });

View file

@ -35,7 +35,7 @@ export default class MAudioBody extends React.Component {
} }
onPlayToggle() { onPlayToggle() {
this.setState({ this.setState({
playing: !this.state.playing playing: !this.state.playing,
}); });
} }
@ -49,9 +49,9 @@ export default class MAudioBody extends React.Component {
} }
componentDidMount() { componentDidMount() {
var content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) { if (content.file !== undefined && this.state.decryptedUrl === null) {
var decryptedBlob; let decryptedBlob;
decryptFile(content.file).then(function(blob) { decryptFile(content.file).then(function(blob) {
decryptedBlob = blob; decryptedBlob = blob;
return readBlobAsDataUri(decryptedBlob); return readBlobAsDataUri(decryptedBlob);
@ -70,14 +70,13 @@ export default class MAudioBody extends React.Component {
} }
render() { render() {
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
if (this.state.error !== null) { if (this.state.error !== null) {
return ( return (
<span className="mx_MAudioBody" ref="body"> <span className="mx_MAudioBody" ref="body">
<img src="img/warning.svg" width="16" height="16"/> <img src="img/warning.svg" width="16" height="16" />
{_t("Error decrypting audio")} { _t("Error decrypting audio") }
</span> </span>
); );
} }
@ -89,7 +88,7 @@ export default class MAudioBody extends React.Component {
// Not sure how tall the audio player is so not sure how tall it should actually be. // Not sure how tall the audio player is so not sure how tall it should actually be.
return ( return (
<span className="mx_MAudioBody"> <span className="mx_MAudioBody">
<img src="img/spinner.gif" alt={content.body} width="16" height="16"/> <img src="img/spinner.gif" alt={content.body} width="16" height="16" />
</span> </span>
); );
} }

View file

@ -28,10 +28,10 @@ import Modal from '../../../Modal';
// A cached tinted copy of "img/download.svg" // A cached tinted copy of "img/download.svg"
var tintedDownloadImageURL; let tintedDownloadImageURL;
// Track a list of mounted MFileBody instances so that we can update // Track a list of mounted MFileBody instances so that we can update
// the "img/download.svg" when the tint changes. // the "img/download.svg" when the tint changes.
var nextMountId = 0; let nextMountId = 0;
const mounts = {}; const mounts = {};
/** /**
@ -169,11 +169,11 @@ function computedStyle(element) {
return ""; return "";
} }
const style = window.getComputedStyle(element, null); const style = window.getComputedStyle(element, null);
var cssText = style.cssText; let cssText = style.cssText;
if (cssText == "") { if (cssText == "") {
// Firefox doesn't implement ".cssText" for computed styles. // Firefox doesn't implement ".cssText" for computed styles.
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687 // https://bugzilla.mozilla.org/show_bug.cgi?id=137687
for (var i = 0; i < style.length; i++) { for (let i = 0; i < style.length; i++) {
cssText += style[i] + ":"; cssText += style[i] + ":";
cssText += style.getPropertyValue(style[i]) + ";"; cssText += style.getPropertyValue(style[i]) + ";";
} }
@ -202,7 +202,7 @@ module.exports = React.createClass({
* @return {string} the human readable link text for the attachment. * @return {string} the human readable link text for the attachment.
*/ */
presentableTextForFile: function(content) { presentableTextForFile: function(content) {
var linkText = _t("Attachment"); let linkText = _t("Attachment");
if (content.body && content.body.length > 0) { if (content.body && content.body.length > 0) {
// The content body should be the name of the file including a // The content body should be the name of the file including a
// file extension. // file extension.
@ -270,7 +270,7 @@ module.exports = React.createClass({
// Need to decrypt the attachment // Need to decrypt the attachment
// Wait for the user to click on the link before downloading // Wait for the user to click on the link before downloading
// and decrypting the attachment. // and decrypting the attachment.
var decrypting = false; let decrypting = false;
const decrypt = () => { const decrypt = () => {
if (decrypting) { if (decrypting) {
return false; return false;
@ -328,14 +328,14 @@ module.exports = React.createClass({
<span className="mx_MFileBody"> <span className="mx_MFileBody">
<div className="mx_MImageBody_download"> <div className="mx_MImageBody_download">
<div style={{display: "none"}}> <div style={{display: "none"}}>
{/* { /*
* Add dummy copy of the "a" tag * Add dummy copy of the "a" tag
* We'll use it to learn how the download link * We'll use it to learn how the download link
* would have been styled if it was rendered inline. * would have been styled if it was rendered inline.
*/} */ }
<a ref="dummyLink"/> <a ref="dummyLink" />
</div> </div>
<iframe src={renderer_url} onLoad={onIframeLoad} ref="iframe"/> <iframe src={renderer_url} onLoad={onIframeLoad} ref="iframe" />
</div> </div>
</span> </span>
); );
@ -356,13 +356,12 @@ module.exports = React.createClass({
</div> </div>
</span> </span>
); );
} } else {
else {
return ( return (
<span className="mx_MFileBody"> <span className="mx_MFileBody">
<div className="mx_MImageBody_download"> <div className="mx_MImageBody_download">
<a href={contentUrl} download={fileName} target="_blank" rel="noopener"> <a href={contentUrl} download={fileName} target="_blank" rel="noopener">
<img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/> <img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage" />
{ _t("Download %(text)s", { text: text }) } { _t("Download %(text)s", { text: text }) }
</a> </a>
</div> </div>
@ -370,7 +369,7 @@ module.exports = React.createClass({
); );
} }
} else { } else {
var extra = text ? (': ' + text) : ''; const extra = text ? (': ' + text) : '';
return <span className="mx_MFileBody"> return <span className="mx_MFileBody">
{ _t("Invalid file%(extra)s", { extra: extra }) } { _t("Invalid file%(extra)s", { extra: extra }) }
</span>; </span>;

View file

@ -191,8 +191,8 @@ module.exports = React.createClass({
if (this.state.error !== null) { if (this.state.error !== null) {
return ( return (
<span className="mx_MImageBody" ref="body"> <span className="mx_MImageBody" ref="body">
<img src="img/warning.svg" width="16" height="16"/> <img src="img/warning.svg" width="16" height="16" />
{_t("Error decrypting image")} { _t("Error decrypting image") }
</span> </span>
); );
} }
@ -210,7 +210,7 @@ module.exports = React.createClass({
}}> }}>
<img src="img/spinner.gif" alt={content.body} width="32" height="32" style={{ <img src="img/spinner.gif" alt={content.body} width="32" height="32" style={{
"margin": "auto", "margin": "auto",
}}/> }} />
</div> </div>
</span> </span>
); );
@ -227,7 +227,7 @@ module.exports = React.createClass({
if (thumbUrl) { if (thumbUrl) {
return ( return (
<span className="mx_MImageBody" ref="body"> <span className="mx_MImageBody" ref="body">
<a href={contentUrl} onClick={ this.onClick }> <a href={contentUrl} onClick={this.onClick}>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image" <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body} alt={content.body}
onMouseEnter={this.onImageEnter} onMouseEnter={this.onImageEnter}
@ -239,13 +239,13 @@ module.exports = React.createClass({
} else if (content.body) { } else if (content.body) {
return ( return (
<span className="mx_MImageBody"> <span className="mx_MImageBody">
{_t("Image '%(Body)s' cannot be displayed.", {Body: content.body})} { _t("Image '%(Body)s' cannot be displayed.", {Body: content.body}) }
</span> </span>
); );
} else { } else {
return ( return (
<span className="mx_MImageBody"> <span className="mx_MImageBody">
{_t("This image cannot be displayed.")} { _t("This image cannot be displayed.") }
</span> </span>
); );
} }

View file

@ -54,13 +54,12 @@ module.exports = React.createClass({
// no scaling needs to be applied // no scaling needs to be applied
return 1; return 1;
} }
var widthMulti = thumbWidth / fullWidth; const widthMulti = thumbWidth / fullWidth;
var heightMulti = thumbHeight / fullHeight; const heightMulti = thumbHeight / fullHeight;
if (widthMulti < heightMulti) { if (widthMulti < heightMulti) {
// width is the dominant dimension so scaling will be fixed on that // width is the dominant dimension so scaling will be fixed on that
return widthMulti; return widthMulti;
} } else {
else {
// height is the dominant dimension so scaling will be fixed on that // height is the dominant dimension so scaling will be fixed on that
return heightMulti; return heightMulti;
} }
@ -89,15 +88,15 @@ module.exports = React.createClass({
componentDidMount: function() { componentDidMount: function() {
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) { if (content.file !== undefined && this.state.decryptedUrl === null) {
var thumbnailPromise = Promise.resolve(null); let thumbnailPromise = Promise.resolve(null);
if (content.info.thumbnail_file) { if (content.info.thumbnail_file) {
thumbnailPromise = decryptFile( thumbnailPromise = decryptFile(
content.info.thumbnail_file content.info.thumbnail_file,
).then(function(blob) { ).then(function(blob) {
return readBlobAsDataUri(blob); return readBlobAsDataUri(blob);
}); });
} }
var decryptedBlob; let decryptedBlob;
thumbnailPromise.then((thumbnailUrl) => { thumbnailPromise.then((thumbnailUrl) => {
return decryptFile(content.file).then(function(blob) { return decryptFile(content.file).then(function(blob) {
decryptedBlob = blob; decryptedBlob = blob;
@ -126,8 +125,8 @@ module.exports = React.createClass({
if (this.state.error !== null) { if (this.state.error !== null) {
return ( return (
<span className="mx_MVideoBody" ref="body"> <span className="mx_MVideoBody" ref="body">
<img src="img/warning.svg" width="16" height="16"/> <img src="img/warning.svg" width="16" height="16" />
{_t("Error decrypting video")} { _t("Error decrypting video") }
</span> </span>
); );
} }
@ -144,7 +143,7 @@ module.exports = React.createClass({
"justify-items": "center", "justify-items": "center",
"width": "100%", "width": "100%",
}}> }}>
<img src="img/spinner.gif" alt={content.body} width="16" height="16"/> <img src="img/spinner.gif" alt={content.body} width="16" height="16" />
</div> </div>
</span> </span>
); );

Some files were not shown because too many files have changed in this diff Show more