Merge branch 'develop' into matthew/settings
This commit is contained in:
commit
f30b7eec2f
91 changed files with 7929 additions and 2382 deletions
|
@ -13,11 +13,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
};
|
|
||||||
|
|
13
package.json
13
package.json
|
@ -13,6 +13,7 @@
|
||||||
"reskindex": "./reskindex.js"
|
"reskindex": "./reskindex.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"reskindex": "reskindex -h header",
|
||||||
"build": "babel src -d lib --source-maps",
|
"build": "babel src -d lib --source-maps",
|
||||||
"start": "babel src -w -d lib --source-maps",
|
"start": "babel src -w -d lib --source-maps",
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
|
@ -23,19 +24,25 @@
|
||||||
"filesize": "^3.1.2",
|
"filesize": "^3.1.2",
|
||||||
"flux": "^2.0.3",
|
"flux": "^2.0.3",
|
||||||
"glob": "^5.0.14",
|
"glob": "^5.0.14",
|
||||||
|
"highlight.js": "^8.9.1",
|
||||||
"linkifyjs": "^2.0.0-beta.4",
|
"linkifyjs": "^2.0.0-beta.4",
|
||||||
"matrix-js-sdk": "^0.3.0",
|
"marked": "^0.3.5",
|
||||||
|
"matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"q": "^1.4.1",
|
"q": "^1.4.1",
|
||||||
"react": "^0.14.2",
|
"react": "^0.14.2",
|
||||||
"react-dom": "^0.14.2"
|
"react-dom": "^0.14.2",
|
||||||
|
"react-gemini-scrollbar": "^2.0.1",
|
||||||
|
"sanitize-html": "^1.11.1",
|
||||||
|
"velocity-animate": "^1.2.3"
|
||||||
},
|
},
|
||||||
"//deps": "The loader packages are here because webpack in a project that depends on us needs them in this package's node_modules folder",
|
"//deps": "The loader packages are here because webpack in a project that depends on us needs them in this package's node_modules folder",
|
||||||
"//depsbuglink": "https://github.com/webpack/webpack/issues/1472",
|
"//depsbuglink": "https://github.com/webpack/webpack/issues/1472",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel": "^5.8.23",
|
"babel": "^5.8.23",
|
||||||
"rimraf": "^2.4.3",
|
|
||||||
"json-loader": "^0.5.3",
|
"json-loader": "^0.5.3",
|
||||||
|
"require-json": "0.0.1",
|
||||||
|
"rimraf": "^2.4.3",
|
||||||
"source-map-loader": "^0.1.5"
|
"source-map-loader": "^0.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
63
reskindex.js
63
reskindex.js
|
@ -8,40 +8,13 @@ var args = require('optimist').argv;
|
||||||
|
|
||||||
var header = args.h || args.header;
|
var header = args.h || args.header;
|
||||||
|
|
||||||
if (args._.length == 0) {
|
var componentsDir = path.join('src', 'components');
|
||||||
console.log("No skin given");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
var skin = args._[0];
|
var componentIndex = path.join('src', 'component-index.js');
|
||||||
|
|
||||||
try {
|
var packageJson = JSON.parse(fs.readFileSync('./package.json'));
|
||||||
fs.accessSync(path.join('src', 'skins', skin), fs.F_OK);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Skin "+skin+" not found: "+e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
var skinfoFile = path.join('src', 'skins', skin, 'skinfo.json');
|
var strm = fs.createWriteStream(componentIndex);
|
||||||
|
|
||||||
try {
|
|
||||||
fs.accessSync(skinfoFile, fs.F_OK);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Skin "+skin+" has no skinfo.json");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
fs.accessSync(path.join('src', 'skins', skin, 'views'), fs.F_OK);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Skin "+skin+" has no views directory");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
var skindex = path.join('src', 'skins', skin, 'skindex.js');
|
|
||||||
var viewsDir = path.join('src', 'skins', skin, 'views');
|
|
||||||
|
|
||||||
var strm = fs.createWriteStream(skindex);
|
|
||||||
|
|
||||||
if (header) {
|
if (header) {
|
||||||
strm.write(fs.readFileSync(header));
|
strm.write(fs.readFileSync(header));
|
||||||
|
@ -55,29 +28,21 @@ strm.write(" * so you'd just be trying to swim upstream like a salmon.\n");
|
||||||
strm.write(" * You are not a salmon.\n");
|
strm.write(" * You are not a salmon.\n");
|
||||||
strm.write(" */\n\n");
|
strm.write(" */\n\n");
|
||||||
|
|
||||||
var mySkinfo = JSON.parse(fs.readFileSync(skinfoFile, "utf8"));
|
if (packageJson['matrix-react-parent']) {
|
||||||
|
strm.write("module.exports.components = require('"+packageJson['matrix-react-parent']+"/lib/component-index').components;\n\n");
|
||||||
|
} else {
|
||||||
|
strm.write("module.exports.components = {};\n");
|
||||||
|
}
|
||||||
|
|
||||||
strm.write("var skin = {};\n");
|
var files = glob.sync('**/*.js', {cwd: componentsDir});
|
||||||
strm.write('\n');
|
|
||||||
|
|
||||||
var files = glob.sync('**/*.js', {cwd: viewsDir});
|
|
||||||
for (var i = 0; i < files.length; ++i) {
|
for (var i = 0; i < files.length; ++i) {
|
||||||
var file = files[i].replace('.js', '');
|
var file = files[i].replace('.js', '');
|
||||||
var module = (file.replace(/\//g, '.'));
|
|
||||||
|
|
||||||
strm.write("skin['"+module+"'] = require('./views/"+file+"');\n");
|
var moduleName = (file.replace(/\//g, '.'));
|
||||||
|
|
||||||
|
strm.write("module.exports.components['"+moduleName+"'] = require('./components/"+file+"');");
|
||||||
|
strm.write('\n');
|
||||||
strm.uncork();
|
strm.uncork();
|
||||||
}
|
}
|
||||||
|
|
||||||
strm.write("\n");
|
|
||||||
|
|
||||||
if (mySkinfo.baseSkin) {
|
|
||||||
strm.write("module.exports = require('"+mySkinfo.baseSkin+"');");
|
|
||||||
strm.write("var extend = require('matrix-react-sdk/lib/extend');\n");
|
|
||||||
strm.write("extend(module.exports, skin);\n");
|
|
||||||
} else {
|
|
||||||
strm.write("module.exports = skin;");
|
|
||||||
}
|
|
||||||
|
|
||||||
strm.end();
|
strm.end();
|
||||||
|
|
||||||
|
|
49
src/Avatar.js
Normal file
49
src/Avatar.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
avatarUrlForMember: function(member, width, height, resizeMethod) {
|
||||||
|
var url = member.getAvatarUrl(
|
||||||
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
resizeMethod,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
if (!url) {
|
||||||
|
// member can be null here currently since on invites, the JS SDK
|
||||||
|
// does not have enough info to build a RoomMember object for
|
||||||
|
// the inviter.
|
||||||
|
url = this.defaultAvatarUrlForString(member ? member.userId : '');
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultAvatarUrlForString: function(s) {
|
||||||
|
var images = [ '76cfa6', '50e2c2', 'f4c371' ];
|
||||||
|
var total = 0;
|
||||||
|
for (var i = 0; i < s.length; ++i) {
|
||||||
|
total += s.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return 'img/' + images[total % images.length] + '.png';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -56,12 +56,12 @@ var Modal = require('./Modal');
|
||||||
var sdk = require('./index');
|
var sdk = require('./index');
|
||||||
var Matrix = require("matrix-js-sdk");
|
var Matrix = require("matrix-js-sdk");
|
||||||
var dis = require("./dispatcher");
|
var dis = require("./dispatcher");
|
||||||
var Modulator = require("./Modulator");
|
|
||||||
|
|
||||||
global.mxCalls = {
|
global.mxCalls = {
|
||||||
//room_id: MatrixCall
|
//room_id: MatrixCall
|
||||||
};
|
};
|
||||||
var calls = global.mxCalls;
|
var calls = global.mxCalls;
|
||||||
|
var ConferenceHandler = null;
|
||||||
|
|
||||||
function play(audioId) {
|
function play(audioId) {
|
||||||
// TODO: Attach an invisible element for this instead
|
// TODO: Attach an invisible element for this instead
|
||||||
|
@ -115,7 +115,7 @@ function _setCallListeners(call) {
|
||||||
_setCallState(call, call.roomId, "busy");
|
_setCallState(call, call.roomId, "busy");
|
||||||
pause("ringbackAudio");
|
pause("ringbackAudio");
|
||||||
play("busyAudio");
|
play("busyAudio");
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Call Timeout",
|
title: "Call Timeout",
|
||||||
description: "The remote side failed to pick up."
|
description: "The remote side failed to pick up."
|
||||||
|
@ -173,7 +173,7 @@ function _onAction(payload) {
|
||||||
console.error("Unknown conf call type: %s", payload.type);
|
console.error("Unknown conf call type: %s", payload.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'place_call':
|
case 'place_call':
|
||||||
|
@ -202,7 +202,7 @@ function _onAction(payload) {
|
||||||
|
|
||||||
var members = room.getJoinedMembers();
|
var members = room.getJoinedMembers();
|
||||||
if (members.length <= 1) {
|
if (members.length <= 1) {
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
description: "You cannot place a call with yourself."
|
description: "You cannot place a call with yourself."
|
||||||
});
|
});
|
||||||
|
@ -227,7 +227,7 @@ function _onAction(payload) {
|
||||||
break;
|
break;
|
||||||
case 'place_conference_call':
|
case 'place_conference_call':
|
||||||
console.log("Place conference call in %s", payload.room_id);
|
console.log("Place conference call in %s", payload.room_id);
|
||||||
if (!Modulator.hasConferenceHandler()) {
|
if (!ConferenceHandler) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
description: "Conference calls are not supported in this client"
|
description: "Conference calls are not supported in this client"
|
||||||
});
|
});
|
||||||
|
@ -239,7 +239,6 @@ function _onAction(payload) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var ConferenceHandler = Modulator.getConferenceHandler();
|
|
||||||
ConferenceHandler.createNewMatrixCall(
|
ConferenceHandler.createNewMatrixCall(
|
||||||
MatrixClientPeg.get(), payload.room_id
|
MatrixClientPeg.get(), payload.room_id
|
||||||
).done(function(call) {
|
).done(function(call) {
|
||||||
|
@ -295,8 +294,7 @@ var callHandler = {
|
||||||
var call = module.exports.getCall(roomId);
|
var call = module.exports.getCall(roomId);
|
||||||
if (call) return call;
|
if (call) return call;
|
||||||
|
|
||||||
if (Modulator.hasConferenceHandler()) {
|
if (ConferenceHandler) {
|
||||||
var ConferenceHandler = Modulator.getConferenceHandler();
|
|
||||||
call = ConferenceHandler.getConferenceCallForRoom(roomId);
|
call = ConferenceHandler.getConferenceCallForRoom(roomId);
|
||||||
}
|
}
|
||||||
if (call) return call;
|
if (call) return call;
|
||||||
|
@ -317,6 +315,10 @@ var callHandler = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
setConferenceHandler: function(confHandler) {
|
||||||
|
ConferenceHandler = confHandler;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Only things in here which actually need to be global are the
|
// Only things in here which actually need to be global are the
|
||||||
|
|
|
@ -18,6 +18,10 @@ limitations under the License.
|
||||||
|
|
||||||
var q = require('q');
|
var q = require('q');
|
||||||
var extend = require('./extend');
|
var extend = require('./extend');
|
||||||
|
var dis = require('./dispatcher');
|
||||||
|
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
|
var sdk = require('./index');
|
||||||
|
var Modal = require('./Modal');
|
||||||
|
|
||||||
function infoForImageFile(imageFile) {
|
function infoForImageFile(imageFile) {
|
||||||
var deferred = q.defer();
|
var deferred = q.defer();
|
||||||
|
@ -48,39 +52,108 @@ function infoForImageFile(imageFile) {
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendContentToRoom(file, roomId, matrixClient) {
|
class ContentMessages {
|
||||||
var content = {
|
constructor() {
|
||||||
body: file.name,
|
this.inprogress = [];
|
||||||
info: {
|
this.nextId = 0;
|
||||||
size: file.size,
|
}
|
||||||
|
|
||||||
|
sendContentToRoom(file, roomId, matrixClient) {
|
||||||
|
var content = {
|
||||||
|
body: file.name,
|
||||||
|
info: {
|
||||||
|
size: file.size,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// if we have a mime type for the file, add it to the message metadata
|
||||||
|
if (file.type) {
|
||||||
|
content.info.mimetype = file.type;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// if we have a mime type for the file, add it to the message metadata
|
var def = q.defer();
|
||||||
if (file.type) {
|
if (file.type.indexOf('image/') == 0) {
|
||||||
content.info.mimetype = file.type;
|
content.msgtype = 'm.image';
|
||||||
}
|
infoForImageFile(file).then(function(imageInfo) {
|
||||||
|
extend(content.info, imageInfo);
|
||||||
var def = q.defer();
|
def.resolve();
|
||||||
if (file.type.indexOf('image/') == 0) {
|
});
|
||||||
content.msgtype = 'm.image';
|
} else {
|
||||||
infoForImageFile(file).then(function(imageInfo) {
|
content.msgtype = 'm.file';
|
||||||
extend(content.info, imageInfo);
|
|
||||||
def.resolve();
|
def.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
var upload = {
|
||||||
|
fileName: file.name,
|
||||||
|
roomId: roomId,
|
||||||
|
total: 0,
|
||||||
|
loaded: 0
|
||||||
|
};
|
||||||
|
this.inprogress.push(upload);
|
||||||
|
dis.dispatch({action: 'upload_started'});
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
return def.promise.then(function() {
|
||||||
|
upload.promise = matrixClient.uploadContent(file);
|
||||||
|
return upload.promise;
|
||||||
|
}).progress(function(ev) {
|
||||||
|
if (ev) {
|
||||||
|
upload.total = ev.total;
|
||||||
|
upload.loaded = ev.loaded;
|
||||||
|
dis.dispatch({action: 'upload_progress', upload: upload});
|
||||||
|
}
|
||||||
|
}).then(function(url) {
|
||||||
|
dis.dispatch({action: 'upload_finished', upload: upload});
|
||||||
|
content.url = url;
|
||||||
|
return matrixClient.sendMessage(roomId, content);
|
||||||
|
}, function(err) {
|
||||||
|
dis.dispatch({action: 'upload_failed', upload: upload});
|
||||||
|
if (!upload.canceled) {
|
||||||
|
var desc = "The file '"+upload.fileName+"' failed to upload.";
|
||||||
|
if (err.http_status == 413) {
|
||||||
|
desc = "The file '"+upload.fileName+"' exceeds this home server's size limit for uploads";
|
||||||
|
}
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Upload Failed",
|
||||||
|
description: desc
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).finally(function() {
|
||||||
|
var inprogressKeys = Object.keys(self.inprogress);
|
||||||
|
for (var i = 0; i < self.inprogress.length; ++i) {
|
||||||
|
var k = inprogressKeys[i];
|
||||||
|
if (self.inprogress[k].promise === upload.promise) {
|
||||||
|
self.inprogress.splice(k, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
content.msgtype = 'm.file';
|
|
||||||
def.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return def.promise.then(function() {
|
getCurrentUploads() {
|
||||||
return matrixClient.uploadContent(file);
|
return this.inprogress;
|
||||||
}).then(function(url) {
|
}
|
||||||
content.url = url;
|
|
||||||
return matrixClient.sendMessage(roomId, content);
|
cancelUpload(promise) {
|
||||||
});
|
var inprogressKeys = Object.keys(this.inprogress);
|
||||||
|
var upload;
|
||||||
|
for (var i = 0; i < this.inprogress.length; ++i) {
|
||||||
|
var k = inprogressKeys[i];
|
||||||
|
if (this.inprogress[k].promise === promise) {
|
||||||
|
upload = this.inprogress[k];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (upload) {
|
||||||
|
upload.canceled = true;
|
||||||
|
MatrixClientPeg.get().cancelUpload(upload.promise);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
if (global.mx_ContentMessage === undefined) {
|
||||||
sendContentToRoom: sendContentToRoom
|
global.mx_ContentMessage = new ContentMessages();
|
||||||
};
|
}
|
||||||
|
|
||||||
|
module.exports = global.mx_ContentMessage;
|
||||||
|
|
82
src/ContextualMenu.js
Normal file
82
src/ContextualMenu.js
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ReactDOM = require('react-dom');
|
||||||
|
|
||||||
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
|
// pass in a custom control as the actual body.
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ContextualMenuContainerId: "mx_ContextualMenu_Container",
|
||||||
|
|
||||||
|
getOrCreateContainer: function() {
|
||||||
|
var container = document.getElementById(this.ContextualMenuContainerId);
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement("div");
|
||||||
|
container.id = this.ContextualMenuContainerId;
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
},
|
||||||
|
|
||||||
|
createMenu: function (Element, props) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var closeMenu = function() {
|
||||||
|
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
|
||||||
|
|
||||||
|
if (props && props.onFinished) props.onFinished.apply(null, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
var position = {
|
||||||
|
top: props.top - 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
var chevron = null;
|
||||||
|
if (props.left) {
|
||||||
|
chevron = <img className="mx_ContextualMenu_chevron_left" src="img/chevron-left.png" width="9" height="16" />
|
||||||
|
position.left = props.left + 8;
|
||||||
|
} else {
|
||||||
|
chevron = <img className="mx_ContextualMenu_chevron_right" src="img/chevron-right.png" width="9" height="16" />
|
||||||
|
position.right = props.right + 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
var className = 'mx_ContextualMenu_wrapper';
|
||||||
|
|
||||||
|
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||||
|
// property set here so you can't close the menu from a button click!
|
||||||
|
var menu = (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="mx_ContextualMenu" style={position}>
|
||||||
|
{chevron}
|
||||||
|
<Element {...props} onFinished={closeMenu}/>
|
||||||
|
</div>
|
||||||
|
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
ReactDOM.render(menu, this.getOrCreateContainer());
|
||||||
|
|
||||||
|
return {close: closeMenu};
|
||||||
|
},
|
||||||
|
};
|
45
src/DateUtils.js
Normal file
45
src/DateUtils.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
|
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
formatDate: function(date) {
|
||||||
|
// date.toLocaleTimeString is completely system dependent.
|
||||||
|
// just go 24h for now
|
||||||
|
function pad(n) {
|
||||||
|
return (n < 10 ? '0' : '') + n;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = new Date();
|
||||||
|
if (date.toDateString() === now.toDateString()) {
|
||||||
|
return pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
|
}
|
||||||
|
else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||||
|
return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
|
}
|
||||||
|
else if (now.getFullYear() === date.getFullYear()) {
|
||||||
|
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + (date.getDay()+1) + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + (date.getDay()+1) + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
149
src/HtmlUtils.js
Normal file
149
src/HtmlUtils.js
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var sanitizeHtml = require('sanitize-html');
|
||||||
|
var highlight = require('highlight.js');
|
||||||
|
|
||||||
|
var sanitizeHtmlParams = {
|
||||||
|
allowedTags: [
|
||||||
|
'font', // custom to matrix. deliberately no h1/h2 to stop people shouting.
|
||||||
|
'del', // for markdown
|
||||||
|
'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||||
|
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
|
||||||
|
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre'
|
||||||
|
],
|
||||||
|
allowedAttributes: {
|
||||||
|
// custom ones first:
|
||||||
|
font: [ 'color' ], // custom to matrix
|
||||||
|
a: [ 'href', 'name', 'target' ], // remote target: custom to matrix
|
||||||
|
// We don't currently allow img itself by default, but this
|
||||||
|
// would make sense if we did
|
||||||
|
img: [ 'src' ],
|
||||||
|
},
|
||||||
|
// Lots of these won't come up by default because we don't allow them
|
||||||
|
selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ],
|
||||||
|
// URL schemes we permit
|
||||||
|
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
|
||||||
|
allowedSchemesByTag: {},
|
||||||
|
|
||||||
|
transformTags: { // custom to matrix
|
||||||
|
// add blank targets to all hyperlinks
|
||||||
|
'a': sanitizeHtml.simpleTransform('a', { target: '_blank'} )
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
_applyHighlights: function(safeSnippet, highlights, html, k) {
|
||||||
|
var lastOffset = 0;
|
||||||
|
var offset;
|
||||||
|
var nodes = [];
|
||||||
|
|
||||||
|
// XXX: when highlighting HTML, synapse performs the search on the plaintext body,
|
||||||
|
// but we're attempting to apply the highlights here to the HTML body. This is
|
||||||
|
// never going to end well - we really should be hooking into the sanitzer HTML
|
||||||
|
// parser to only attempt to highlight text nodes to avoid corrupting tags.
|
||||||
|
// If and when this happens, we'll probably have to split his method in two between
|
||||||
|
// HTML and plain-text highlighting.
|
||||||
|
|
||||||
|
var safeHighlight = html ? sanitizeHtml(highlights[0], sanitizeHtmlParams) : highlights[0];
|
||||||
|
while ((offset = safeSnippet.indexOf(safeHighlight, lastOffset)) >= 0) {
|
||||||
|
// handle preamble
|
||||||
|
if (offset > lastOffset) {
|
||||||
|
nodes = nodes.concat(this._applySubHighlightsInRange(safeSnippet, lastOffset, offset, highlights, html, k));
|
||||||
|
k += nodes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do highlight
|
||||||
|
if (html) {
|
||||||
|
nodes.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeHighlight }} className="mx_MessageTile_searchHighlight" />);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nodes.push(<span key={ k++ } className="mx_MessageTile_searchHighlight">{ safeHighlight }</span>);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastOffset = offset + safeHighlight.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle postamble
|
||||||
|
if (lastOffset != safeSnippet.length) {
|
||||||
|
nodes = nodes.concat(this._applySubHighlightsInRange(safeSnippet, lastOffset, undefined, highlights, html, k));
|
||||||
|
k += nodes.length;
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
},
|
||||||
|
|
||||||
|
_applySubHighlightsInRange: function(safeSnippet, lastOffset, offset, highlights, html, k) {
|
||||||
|
var nodes = [];
|
||||||
|
if (highlights[1]) {
|
||||||
|
// recurse into this range to check for the next set of highlight matches
|
||||||
|
var subnodes = this._applyHighlights( safeSnippet.substring(lastOffset, offset), highlights.slice(1), html, k );
|
||||||
|
nodes = nodes.concat(subnodes);
|
||||||
|
k += subnodes.length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// no more highlights to be found, just return the unhighlighted string
|
||||||
|
if (html) {
|
||||||
|
nodes.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeSnippet.substring(lastOffset, offset) }} />);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nodes.push(<span key={ k++ }>{ safeSnippet.substring(lastOffset, offset) }</span>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
},
|
||||||
|
|
||||||
|
bodyToHtml: function(content, highlights) {
|
||||||
|
var originalBody = content.body;
|
||||||
|
var body;
|
||||||
|
var k = 0;
|
||||||
|
|
||||||
|
if (highlights && highlights.length > 0) {
|
||||||
|
var bodyList = [];
|
||||||
|
|
||||||
|
if (content.format === "org.matrix.custom.html") {
|
||||||
|
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
||||||
|
bodyList = this._applyHighlights(safeBody, highlights, true, k);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bodyList = this._applyHighlights(originalBody, highlights, true, k);
|
||||||
|
}
|
||||||
|
body = bodyList;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (content.format === "org.matrix.custom.html") {
|
||||||
|
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
||||||
|
body = <span className="markdown-body" dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
body = originalBody;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
},
|
||||||
|
|
||||||
|
highlightDom: function(element) {
|
||||||
|
var blocks = element.getElementsByTagName("code");
|
||||||
|
for (var i = 0; i < blocks.length; i++) {
|
||||||
|
highlight.highlightBlock(blocks[i]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
111
src/Modulator.js
111
src/Modulator.js
|
@ -1,111 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The modulator stores 'modules': classes that provide
|
|
||||||
* functionality and are not React UI components.
|
|
||||||
* Modules go into named slots, eg. a conference calling
|
|
||||||
* module goes into the 'conference' slot. If two modules
|
|
||||||
* that use the same slot are loaded, this is considered
|
|
||||||
* to be an error.
|
|
||||||
*
|
|
||||||
* There are some module slots that the react SDK knows
|
|
||||||
* about natively: these have explicit getters.
|
|
||||||
*
|
|
||||||
* A module must define:
|
|
||||||
* - 'slot' (string): The name of the slot it goes into
|
|
||||||
* and may define:
|
|
||||||
* - 'start' (function): Called on module load
|
|
||||||
* - 'stop' (function): Called on module unload
|
|
||||||
*/
|
|
||||||
class Modulator {
|
|
||||||
constructor() {
|
|
||||||
this.modules = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
getModule(name) {
|
|
||||||
var m = this.getModuleOrNull(name);
|
|
||||||
if (m === null) {
|
|
||||||
throw new Error("No such module: "+name);
|
|
||||||
}
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
getModuleOrNull(name) {
|
|
||||||
if (this.modules == {}) {
|
|
||||||
throw new Error(
|
|
||||||
"Attempted to get a module before a skin has been loaded."+
|
|
||||||
"This is probably because a component has called "+
|
|
||||||
"getModule at the root level."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var module = this.modules[name];
|
|
||||||
if (module) {
|
|
||||||
return module;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasModule(name) {
|
|
||||||
var m = this.getModuleOrNull(name);
|
|
||||||
return m !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadModule(moduleObject) {
|
|
||||||
if (!moduleObject.slot) {
|
|
||||||
throw new Error(
|
|
||||||
"Attempted to load something that is not a module "+
|
|
||||||
"(does not have a slot name)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.modules[moduleObject.slot] !== undefined) {
|
|
||||||
throw new Error(
|
|
||||||
"Cannot load module: slot '"+moduleObject.slot+"' is occupied!"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.modules[moduleObject.slot] = moduleObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
var keys = Object.keys(this.modules);
|
|
||||||
for (var i = 0; i < keys.length; ++i) {
|
|
||||||
var k = keys[i];
|
|
||||||
var m = this.modules[k];
|
|
||||||
|
|
||||||
if (m.stop) m.stop();
|
|
||||||
}
|
|
||||||
this.modules = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ***********
|
|
||||||
// known slots
|
|
||||||
// ***********
|
|
||||||
|
|
||||||
getConferenceHandler() {
|
|
||||||
return this.getModule('conference');
|
|
||||||
}
|
|
||||||
|
|
||||||
hasConferenceHandler() {
|
|
||||||
return this.hasModule('conference');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define one Modulator globally (see Skinner.js)
|
|
||||||
if (global.mxModulator === undefined) {
|
|
||||||
global.mxModulator = new Modulator();
|
|
||||||
}
|
|
||||||
module.exports = global.mxModulator;
|
|
||||||
|
|
|
@ -16,8 +16,10 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
var dis = require("../../dispatcher");
|
var TextForEvent = require('./TextForEvent');
|
||||||
|
var Avatar = require('./Avatar');
|
||||||
|
var dis = require("./dispatcher");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Dispatches:
|
* Dispatches:
|
||||||
|
@ -27,16 +29,85 @@ var dis = require("../../dispatcher");
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = {
|
var Notifier = {
|
||||||
|
|
||||||
|
notificationMessageForEvent: function(ev) {
|
||||||
|
return TextForEvent.textForEvent(ev);
|
||||||
|
},
|
||||||
|
|
||||||
|
displayNotification: function(ev, room) {
|
||||||
|
if (!global.Notification || global.Notification.permission != 'granted') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (global.document.hasFocus()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = this.notificationMessageForEvent(ev);
|
||||||
|
if (!msg) return;
|
||||||
|
|
||||||
|
var title;
|
||||||
|
if (!ev.sender || room.name == ev.sender.name) {
|
||||||
|
title = room.name;
|
||||||
|
// notificationMessageForEvent includes sender,
|
||||||
|
// but we already have the sender here
|
||||||
|
if (ev.getContent().body) msg = ev.getContent().body;
|
||||||
|
} else if (ev.getType() == 'm.room.member') {
|
||||||
|
// context is all in the message here, we don't need
|
||||||
|
// to display sender info
|
||||||
|
title = room.name;
|
||||||
|
} else if (ev.sender) {
|
||||||
|
title = ev.sender.name + " (" + room.name + ")";
|
||||||
|
// notificationMessageForEvent includes sender,
|
||||||
|
// but we've just out sender in the title
|
||||||
|
if (ev.getContent().body) msg = ev.getContent().body;
|
||||||
|
}
|
||||||
|
|
||||||
|
var avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
|
||||||
|
ev.sender, 40, 40, 'crop'
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
var notification = new global.Notification(
|
||||||
|
title,
|
||||||
|
{
|
||||||
|
"body": msg,
|
||||||
|
"icon": avatarUrl,
|
||||||
|
"tag": "matrixreactsdk"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
notification.onclick = function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: room.roomId
|
||||||
|
});
|
||||||
|
global.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*var audioClip;
|
||||||
|
|
||||||
|
if (audioNotification) {
|
||||||
|
audioClip = playAudio(audioNotification);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
global.setTimeout(function() {
|
||||||
|
notification.close();
|
||||||
|
}, 5 * 1000);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
start: function() {
|
start: function() {
|
||||||
this.boundOnRoomTimeline = this.onRoomTimeline.bind(this);
|
this.boundOnRoomTimeline = this.onRoomTimeline.bind(this);
|
||||||
|
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
||||||
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
|
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
|
||||||
this.state = { 'toolbarHidden' : false };
|
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
|
||||||
|
this.toolbarHidden = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
stop: function() {
|
stop: function() {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
|
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
|
||||||
|
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -96,7 +167,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
setToolbarHidden: function(hidden) {
|
setToolbarHidden: function(hidden) {
|
||||||
this.state.toolbarHidden = hidden;
|
this.toolbarHidden = hidden;
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "notifier_enabled",
|
action: "notifier_enabled",
|
||||||
value: this.isEnabled()
|
value: this.isEnabled()
|
||||||
|
@ -104,11 +175,18 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
isToolbarHidden: function() {
|
isToolbarHidden: function() {
|
||||||
return this.state.toolbarHidden;
|
return this.toolbarHidden;
|
||||||
|
},
|
||||||
|
|
||||||
|
onSyncStateChange: function(state) {
|
||||||
|
if (state === "PREPARED" || state === "SYNCING") {
|
||||||
|
this.isPrepared = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
||||||
if (toStartOfTimeline) return;
|
if (toStartOfTimeline) return;
|
||||||
|
if (!this.isPrepared) return; // don't alert for any messages initially
|
||||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
|
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return;
|
||||||
|
|
||||||
if (!this.isEnabled()) {
|
if (!this.isEnabled()) {
|
||||||
|
@ -122,3 +200,8 @@ module.exports = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!global.mxNotifier) {
|
||||||
|
global.mxNotifier = Notifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = global.mxNotifier;
|
|
@ -15,58 +15,54 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
|
var 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
|
var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
|
||||||
var PRESENCE_STATES = ["online", "offline", "unavailable"];
|
var PRESENCE_STATES = ["online", "offline", "unavailable"];
|
||||||
|
|
||||||
// The current presence state
|
class Presence {
|
||||||
var state, timer;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start listening the user activity to evaluate his presence state.
|
* Start listening the user activity to evaluate his presence state.
|
||||||
* Any state change will be sent to the Home Server.
|
* Any state change will be sent to the Home Server.
|
||||||
*/
|
*/
|
||||||
start: function() {
|
start() {
|
||||||
var self = this;
|
|
||||||
this.running = true;
|
this.running = true;
|
||||||
if (undefined === state) {
|
if (undefined === this.state) {
|
||||||
// The user is online if they move the mouse or press a key
|
|
||||||
document.onmousemove = function() { self._resetTimer(); };
|
|
||||||
document.onkeypress = function() { self._resetTimer(); };
|
|
||||||
this._resetTimer();
|
this._resetTimer();
|
||||||
|
this.dispatcherRef = dis.register(this._onUserActivity.bind(this));
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop tracking user activity
|
* Stop tracking user activity
|
||||||
*/
|
*/
|
||||||
stop: function() {
|
stop() {
|
||||||
this.running = false;
|
this.running = false;
|
||||||
if (timer) {
|
if (this.timer) {
|
||||||
clearTimeout(timer);
|
clearInterval(this.timer);
|
||||||
timer = undefined;
|
this.timer = undefined;
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
state = undefined;
|
this.state = undefined;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current presence state.
|
* Get the current presence state.
|
||||||
* @returns {string} the presence state (see PRESENCE enum)
|
* @returns {string} the presence state (see PRESENCE enum)
|
||||||
*/
|
*/
|
||||||
getState: function() {
|
getState() {
|
||||||
return state;
|
return this.state;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the presence state.
|
* Set the presence state.
|
||||||
* If the state has changed, the Home Server will be notified.
|
* If the state has changed, the Home Server will be notified.
|
||||||
* @param {string} newState the new presence state (see PRESENCE enum)
|
* @param {string} newState the new presence state (see PRESENCE enum)
|
||||||
*/
|
*/
|
||||||
setState: function(newState) {
|
setState(newState) {
|
||||||
if (newState === state) {
|
if (newState === this.state) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (PRESENCE_STATES.indexOf(newState) === -1) {
|
if (PRESENCE_STATES.indexOf(newState) === -1) {
|
||||||
|
@ -75,33 +71,42 @@ module.exports = {
|
||||||
if (!this.running) {
|
if (!this.running) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state = newState;
|
var old_state = this.state;
|
||||||
MatrixClientPeg.get().setPresence(state).done(function() {
|
this.state = newState;
|
||||||
|
var self = this;
|
||||||
|
MatrixClientPeg.get().setPresence(this.state).done(function() {
|
||||||
console.log("Presence: %s", newState);
|
console.log("Presence: %s", newState);
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Failed to set presence: %s", err);
|
console.error("Failed to set presence: %s", err);
|
||||||
|
self.state = old_state;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
|
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_onUnavailableTimerFire: function() {
|
_onUnavailableTimerFire() {
|
||||||
this.setState("unavailable");
|
this.setState("unavailable");
|
||||||
},
|
}
|
||||||
|
|
||||||
|
_onUserActivity() {
|
||||||
|
this._resetTimer();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback called when the user made an action on the page
|
* Callback called when the user made an action on the page
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_resetTimer: function() {
|
_resetTimer() {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.setState("online");
|
this.setState("online");
|
||||||
// Re-arm the timer
|
// Re-arm the timer
|
||||||
clearTimeout(timer);
|
clearTimeout(this.timer);
|
||||||
timer = setTimeout(function() {
|
this.timer = setTimeout(function() {
|
||||||
self._onUnavailableTimerFire();
|
self._onUnavailableTimerFire();
|
||||||
}, UNAVAILABLE_TIME_MS);
|
}, UNAVAILABLE_TIME_MS);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
module.exports = new Presence();
|
||||||
|
|
33
src/Resend.js
Normal file
33
src/Resend.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
|
var dis = require('./dispatcher');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
resend: function(event) {
|
||||||
|
MatrixClientPeg.get().resendEvent(
|
||||||
|
event, MatrixClientPeg.get().getRoom(event.getRoomId())
|
||||||
|
).done(function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'message_sent',
|
||||||
|
event: event
|
||||||
|
});
|
||||||
|
}, function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'message_send_failed',
|
||||||
|
event: event
|
||||||
|
});
|
||||||
|
});
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'message_resend_started',
|
||||||
|
event: event
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFromQueue: function(event) {
|
||||||
|
MatrixClientPeg.get().getScheduler().removeEventFromQueue(event);
|
||||||
|
var room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||||
|
if (!room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
room.removeEvents([event.getId()]);
|
||||||
|
}
|
||||||
|
};
|
346
src/Signup.js
Normal file
346
src/Signup.js
Normal file
|
@ -0,0 +1,346 @@
|
||||||
|
"use strict";
|
||||||
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
|
var SignupStages = require("./SignupStages");
|
||||||
|
var dis = require("./dispatcher");
|
||||||
|
var q = require("q");
|
||||||
|
|
||||||
|
const EMAIL_STAGE_TYPE = "m.login.email.identity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base class for common functionality between Registration and Login e.g.
|
||||||
|
* storage of HS/IS URLs.
|
||||||
|
*/
|
||||||
|
class Signup {
|
||||||
|
constructor(hsUrl, isUrl) {
|
||||||
|
this._hsUrl = hsUrl;
|
||||||
|
this._isUrl = isUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHomeserverUrl() {
|
||||||
|
return this._hsUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdentityServerUrl() {
|
||||||
|
return this._isUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHomeserverUrl(hsUrl) {
|
||||||
|
this._hsUrl = hsUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIdentityServerUrl(isUrl) {
|
||||||
|
this._isUrl = isUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registration logic class
|
||||||
|
*/
|
||||||
|
class Register extends Signup {
|
||||||
|
constructor(hsUrl, isUrl) {
|
||||||
|
super(hsUrl, isUrl);
|
||||||
|
this.setStep("START");
|
||||||
|
this.data = null; // from the server
|
||||||
|
// random other stuff (e.g. query params, NOT params from the server)
|
||||||
|
this.params = {};
|
||||||
|
this.credentials = null;
|
||||||
|
this.activeStage = null;
|
||||||
|
this.registrationPromise = null;
|
||||||
|
// These values MUST be undefined else we'll send "username: null" which
|
||||||
|
// will error on Synapse rather than having the key absent.
|
||||||
|
this.username = undefined; // desired
|
||||||
|
this.email = undefined; // desired
|
||||||
|
this.password = undefined; // desired
|
||||||
|
}
|
||||||
|
|
||||||
|
setClientSecret(secret) {
|
||||||
|
this.params.clientSecret = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionId(sessionId) {
|
||||||
|
this.params.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRegistrationUrl(regUrl) {
|
||||||
|
this.params.registrationUrl = regUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIdSid(idSid) {
|
||||||
|
this.params.idSid = idSid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStep() {
|
||||||
|
return this._step;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCredentials() {
|
||||||
|
return this.credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
getServerData() {
|
||||||
|
return this.data || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
getPromise() {
|
||||||
|
return this.registrationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStep(step) {
|
||||||
|
this._step = 'Register.' + step;
|
||||||
|
// TODO:
|
||||||
|
// It's a shame this is going to the global dispatcher, we only really
|
||||||
|
// want things which have an instance of this class to be able to add
|
||||||
|
// listeners...
|
||||||
|
console.log("Dispatching 'registration_step_update' for step %s", this._step);
|
||||||
|
dis.dispatch({
|
||||||
|
action: "registration_step_update"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
register(formVals) {
|
||||||
|
var {username, password, email} = formVals;
|
||||||
|
this.email = email;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
|
||||||
|
// feels a bit wrong to be clobbering the global client for something we
|
||||||
|
// don't even know if it'll work, but we'll leave this here for now to
|
||||||
|
// not complicate matters further. It would be nicer to isolate this
|
||||||
|
// logic entirely from the rest of the app though.
|
||||||
|
MatrixClientPeg.replaceUsingUrls(
|
||||||
|
this._hsUrl,
|
||||||
|
this._isUrl
|
||||||
|
);
|
||||||
|
return this._tryRegister();
|
||||||
|
}
|
||||||
|
|
||||||
|
_tryRegister(authDict) {
|
||||||
|
var self = this;
|
||||||
|
return MatrixClientPeg.get().register(
|
||||||
|
this.username, this.password, this.params.sessionId, authDict
|
||||||
|
).then(function(result) {
|
||||||
|
self.credentials = result;
|
||||||
|
self.setStep("COMPLETE");
|
||||||
|
return result; // contains the credentials
|
||||||
|
}, function(error) {
|
||||||
|
if (error.httpStatus === 401 && error.data && error.data.flows) {
|
||||||
|
self.data = error.data || {};
|
||||||
|
var flow = self.chooseFlow(error.data.flows);
|
||||||
|
|
||||||
|
if (flow) {
|
||||||
|
console.log("Active flow => %s", JSON.stringify(flow));
|
||||||
|
var flowStage = self.firstUncompletedStage(flow);
|
||||||
|
return self.startStage(flowStage);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error("Unable to register - missing email address?");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (error.errcode === 'M_USER_IN_USE') {
|
||||||
|
throw new Error("Username in use");
|
||||||
|
} else if (error.httpStatus == 401) {
|
||||||
|
throw new Error("Authorisation failed!");
|
||||||
|
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
|
||||||
|
throw new Error(`Registration failed! (${error.httpStatus})`);
|
||||||
|
} else if (error.httpStatus >= 500 && error.httpStatus < 600) {
|
||||||
|
throw new Error(
|
||||||
|
`Server error during registration! (${error.httpStatus})`
|
||||||
|
);
|
||||||
|
} else if (error.name == "M_MISSING_PARAM") {
|
||||||
|
// The HS hasn't remembered the login params from
|
||||||
|
// the first try when the login email was sent.
|
||||||
|
throw new Error(
|
||||||
|
"This home server does not support resuming registration."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUncompletedStage(flow) {
|
||||||
|
for (var i = 0; i < flow.stages.length; ++i) {
|
||||||
|
if (!this.hasCompletedStage(flow.stages[i])) {
|
||||||
|
return flow.stages[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCompletedStage(stageType) {
|
||||||
|
var completed = (this.data || {}).completed || [];
|
||||||
|
return completed.indexOf(stageType) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
startStage(stageName) {
|
||||||
|
var self = this;
|
||||||
|
this.setStep(`STEP_${stageName}`);
|
||||||
|
var StageClass = SignupStages[stageName];
|
||||||
|
if (!StageClass) {
|
||||||
|
// no idea how to handle this!
|
||||||
|
throw new Error("Unknown stage: " + stageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stage = new StageClass(MatrixClientPeg.get(), this);
|
||||||
|
this.activeStage = stage;
|
||||||
|
return stage.complete().then(function(request) {
|
||||||
|
if (request.auth) {
|
||||||
|
console.log("Stage %s is returning an auth dict", stageName);
|
||||||
|
return self._tryRegister(request.auth);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// never resolve the promise chain. This is for things like email auth
|
||||||
|
// which display a "check your email" message and relies on the
|
||||||
|
// link in the email to actually register you.
|
||||||
|
console.log("Waiting for external action.");
|
||||||
|
return q.defer().promise;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseFlow(flows) {
|
||||||
|
// If the user gave us an email then we want to pick an email
|
||||||
|
// flow we can do, else any other flow.
|
||||||
|
var emailFlow = null;
|
||||||
|
var otherFlow = null;
|
||||||
|
flows.forEach(function(flow) {
|
||||||
|
var flowHasEmail = false;
|
||||||
|
for (var stageI = 0; stageI < flow.stages.length; ++stageI) {
|
||||||
|
var stage = flow.stages[stageI];
|
||||||
|
|
||||||
|
if (!SignupStages[stage]) {
|
||||||
|
// we can't do this flow, don't have a Stage impl.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stage === EMAIL_STAGE_TYPE) {
|
||||||
|
flowHasEmail = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flowHasEmail) {
|
||||||
|
emailFlow = flow;
|
||||||
|
} else {
|
||||||
|
otherFlow = flow;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.email || this.hasCompletedStage(EMAIL_STAGE_TYPE)) {
|
||||||
|
// we've been given an email or we've already done an email part
|
||||||
|
return emailFlow;
|
||||||
|
} else {
|
||||||
|
return otherFlow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recheckState() {
|
||||||
|
// feels a bit wrong to be clobbering the global client for something we
|
||||||
|
// don't even know if it'll work, but we'll leave this here for now to
|
||||||
|
// not complicate matters further. It would be nicer to isolate this
|
||||||
|
// logic entirely from the rest of the app though.
|
||||||
|
MatrixClientPeg.replaceUsingUrls(
|
||||||
|
this._hsUrl,
|
||||||
|
this._isUrl
|
||||||
|
);
|
||||||
|
// We've been given a bunch of data from a previous register step,
|
||||||
|
// this only happens for email auth currently. It's kinda ming we need
|
||||||
|
// to know this though. A better solution would be to ask the stages if
|
||||||
|
// they are ready to do something rather than accepting that we know about
|
||||||
|
// email auth and its internals.
|
||||||
|
this.params.hasEmailInfo = (
|
||||||
|
this.params.clientSecret && this.params.sessionId && this.params.idSid
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.params.hasEmailInfo) {
|
||||||
|
this.registrationPromise = this.startStage(EMAIL_STAGE_TYPE);
|
||||||
|
}
|
||||||
|
return this.registrationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
tellStage(stageName, data) {
|
||||||
|
if (this.activeStage && this.activeStage.type === stageName) {
|
||||||
|
console.log("Telling stage %s about something..", stageName);
|
||||||
|
this.activeStage.onReceiveData(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Login extends Signup {
|
||||||
|
constructor(hsUrl, isUrl) {
|
||||||
|
super(hsUrl, isUrl);
|
||||||
|
this._currentFlowIndex = 0;
|
||||||
|
this._flows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getFlows() {
|
||||||
|
var self = this;
|
||||||
|
// feels a bit wrong to be clobbering the global client for something we
|
||||||
|
// don't even know if it'll work, but we'll leave this here for now to
|
||||||
|
// not complicate matters further. It would be nicer to isolate this
|
||||||
|
// logic entirely from the rest of the app though.
|
||||||
|
MatrixClientPeg.replaceUsingUrls(
|
||||||
|
this._hsUrl,
|
||||||
|
this._isUrl
|
||||||
|
);
|
||||||
|
return MatrixClientPeg.get().loginFlows().then(function(result) {
|
||||||
|
self._flows = result.flows;
|
||||||
|
self._currentFlowIndex = 0;
|
||||||
|
// technically the UI should display options for all flows for the
|
||||||
|
// user to then choose one, so return all the flows here.
|
||||||
|
return self._flows;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseFlow(flowIndex) {
|
||||||
|
this._currentFlowIndex = flowIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentFlowStep() {
|
||||||
|
// technically the flow can have multiple steps, but no one does this
|
||||||
|
// for login so we can ignore it.
|
||||||
|
var flowStep = this._flows[this._currentFlowIndex];
|
||||||
|
return flowStep ? flowStep.type : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
loginViaPassword(username, pass) {
|
||||||
|
var self = this;
|
||||||
|
var isEmail = username.indexOf("@") > 0;
|
||||||
|
var loginParams = {
|
||||||
|
password: pass
|
||||||
|
};
|
||||||
|
if (isEmail) {
|
||||||
|
loginParams.medium = 'email';
|
||||||
|
loginParams.address = username;
|
||||||
|
} else {
|
||||||
|
loginParams.user = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MatrixClientPeg.get().login('m.login.password', loginParams).then(function(data) {
|
||||||
|
return q({
|
||||||
|
homeserverUrl: self._hsUrl,
|
||||||
|
identityServerUrl: self._isUrl,
|
||||||
|
userId: data.user_id,
|
||||||
|
accessToken: data.access_token
|
||||||
|
});
|
||||||
|
}, function(error) {
|
||||||
|
if (error.httpStatus == 400 && loginParams.medium) {
|
||||||
|
error.friendlyText = (
|
||||||
|
'This Home Server does not support login using email address.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (error.httpStatus === 403) {
|
||||||
|
error.friendlyText = (
|
||||||
|
'Incorrect username and/or password.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
error.friendlyText = (
|
||||||
|
'There was a problem logging in. (HTTP ' + error.httpStatus + ")"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.Register = Register;
|
||||||
|
module.exports.Login = Login;
|
196
src/SignupStages.js
Normal file
196
src/SignupStages.js
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
"use strict";
|
||||||
|
var q = require("q");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface class which login types should abide by.
|
||||||
|
*/
|
||||||
|
class Stage {
|
||||||
|
constructor(type, matrixClient, signupInstance) {
|
||||||
|
this.type = type;
|
||||||
|
this.client = matrixClient;
|
||||||
|
this.signupInstance = signupInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
complete() {
|
||||||
|
// Return a promise which is:
|
||||||
|
// RESOLVED => With an Object which has an 'auth' key which is the auth dict
|
||||||
|
// to submit.
|
||||||
|
// REJECTED => With an Error if there was a problem with this stage.
|
||||||
|
// Has a "message" string and an "isFatal" flag.
|
||||||
|
return q.reject("NOT IMPLEMENTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
onReceiveData() {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Stage.TYPE = "NOT IMPLEMENTED";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This stage requires no auth.
|
||||||
|
*/
|
||||||
|
class DummyStage extends Stage {
|
||||||
|
constructor(matrixClient, signupInstance) {
|
||||||
|
super(DummyStage.TYPE, matrixClient, signupInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
complete() {
|
||||||
|
return q({
|
||||||
|
auth: {
|
||||||
|
type: DummyStage.TYPE
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DummyStage.TYPE = "m.login.dummy";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This stage uses Google's Recaptcha to do auth.
|
||||||
|
*/
|
||||||
|
class RecaptchaStage extends Stage {
|
||||||
|
constructor(matrixClient, signupInstance) {
|
||||||
|
super(RecaptchaStage.TYPE, matrixClient, signupInstance);
|
||||||
|
this.defer = q.defer(); // resolved with the captcha response
|
||||||
|
this.publicKey = null; // from the HS
|
||||||
|
this.divId = null; // from the UI component
|
||||||
|
}
|
||||||
|
|
||||||
|
// called when the UI component has loaded the recaptcha <div> so we can
|
||||||
|
// render to it.
|
||||||
|
onReceiveData(data) {
|
||||||
|
if (!data || !data.divId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.divId = data.divId;
|
||||||
|
this._attemptRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
complete() {
|
||||||
|
var publicKey;
|
||||||
|
var serverParams = this.signupInstance.getServerData().params;
|
||||||
|
if (serverParams && serverParams["m.login.recaptcha"]) {
|
||||||
|
publicKey = serverParams["m.login.recaptcha"].public_key;
|
||||||
|
}
|
||||||
|
if (!publicKey) {
|
||||||
|
return q.reject({
|
||||||
|
message: "This server has not supplied enough information for Recaptcha " +
|
||||||
|
"authentication",
|
||||||
|
isFatal: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
this._attemptRender();
|
||||||
|
return this.defer.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
_attemptRender() {
|
||||||
|
if (!global.grecaptcha) {
|
||||||
|
console.error("grecaptcha not loaded!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.publicKey) {
|
||||||
|
console.error("No public key for recaptcha!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.divId) {
|
||||||
|
console.error("No div ID specified!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Rendering to %s", this.divId);
|
||||||
|
var self = this;
|
||||||
|
global.grecaptcha.render(this.divId, {
|
||||||
|
sitekey: this.publicKey,
|
||||||
|
callback: function(response) {
|
||||||
|
console.log("Received captcha response");
|
||||||
|
self.defer.resolve({
|
||||||
|
auth: {
|
||||||
|
type: 'm.login.recaptcha',
|
||||||
|
response: response
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RecaptchaStage.TYPE = "m.login.recaptcha";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This state uses the IS to verify email addresses.
|
||||||
|
*/
|
||||||
|
class EmailIdentityStage extends Stage {
|
||||||
|
constructor(matrixClient, signupInstance) {
|
||||||
|
super(EmailIdentityStage.TYPE, matrixClient, signupInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
_completeVerify() {
|
||||||
|
// pull out the host of the IS URL by creating an anchor element
|
||||||
|
var isLocation = document.createElement('a');
|
||||||
|
isLocation.href = this.signupInstance.getIdentityServerUrl();
|
||||||
|
|
||||||
|
return q({
|
||||||
|
auth: {
|
||||||
|
type: 'm.login.email.identity',
|
||||||
|
threepid_creds: {
|
||||||
|
sid: this.signupInstance.params.idSid,
|
||||||
|
client_secret: this.signupInstance.params.clientSecret,
|
||||||
|
id_server: isLocation.host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete the email stage.
|
||||||
|
*
|
||||||
|
* This is called twice under different circumstances:
|
||||||
|
* 1) When requesting an email token from the IS
|
||||||
|
* 2) When validating query parameters received from the link in the email
|
||||||
|
*/
|
||||||
|
complete() {
|
||||||
|
// TODO: The Registration class shouldn't really know this info.
|
||||||
|
if (this.signupInstance.params.hasEmailInfo) {
|
||||||
|
return this._completeVerify();
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientSecret = this.client.generateClientSecret();
|
||||||
|
var nextLink = this.signupInstance.params.registrationUrl +
|
||||||
|
'?client_secret=' +
|
||||||
|
encodeURIComponent(clientSecret) +
|
||||||
|
"&hs_url=" +
|
||||||
|
encodeURIComponent(this.signupInstance.getHomeserverUrl()) +
|
||||||
|
"&is_url=" +
|
||||||
|
encodeURIComponent(this.signupInstance.getIdentityServerUrl()) +
|
||||||
|
"&session_id=" +
|
||||||
|
encodeURIComponent(this.signupInstance.getServerData().session);
|
||||||
|
|
||||||
|
return this.client.requestEmailToken(
|
||||||
|
this.signupInstance.email,
|
||||||
|
clientSecret,
|
||||||
|
1, // TODO: Multiple send attempts?
|
||||||
|
nextLink
|
||||||
|
).then(function(response) {
|
||||||
|
return {}; // don't want to make a request
|
||||||
|
}, function(error) {
|
||||||
|
console.error(error);
|
||||||
|
var e = {
|
||||||
|
isFatal: true
|
||||||
|
};
|
||||||
|
if (error.errcode == 'THREEPID_IN_USE') {
|
||||||
|
e.message = "Email in use";
|
||||||
|
} else {
|
||||||
|
e.message = 'Unable to contact the given identity server';
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EmailIdentityStage.TYPE = "m.login.email.identity";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
[DummyStage.TYPE]: DummyStage,
|
||||||
|
[RecaptchaStage.TYPE]: RecaptchaStage,
|
||||||
|
[EmailIdentityStage.TYPE]: EmailIdentityStage
|
||||||
|
};
|
|
@ -32,6 +32,12 @@ class Skinner {
|
||||||
if (comp) {
|
if (comp) {
|
||||||
return comp;
|
return comp;
|
||||||
}
|
}
|
||||||
|
// XXX: Temporarily also try 'views.' as we're currently
|
||||||
|
// leaving the 'views.' off views.
|
||||||
|
var comp = this.components['views.'+name];
|
||||||
|
if (comp) {
|
||||||
|
return comp;
|
||||||
|
}
|
||||||
throw new Error("No such component: "+name);
|
throw new Error("No such component: "+name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +48,24 @@ class Skinner {
|
||||||
"If you want to change the active skin, call resetSkin first"
|
"If you want to change the active skin, call resetSkin first"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.components = skinObject;
|
this.components = {};
|
||||||
|
var compKeys = Object.keys(skinObject.components);
|
||||||
|
for (var i = 0; i < compKeys.length; ++i) {
|
||||||
|
var comp = skinObject.components[compKeys[i]];
|
||||||
|
this.addComponent(compKeys[i], comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addComponent(name, comp) {
|
||||||
|
var slot = name;
|
||||||
|
if (comp.replaces !== undefined) {
|
||||||
|
if (comp.replaces.indexOf('.') > -1) {
|
||||||
|
slot = comp.replaces;
|
||||||
|
} else {
|
||||||
|
slot = name.substr(0, name.lastIndexOf('.') + 1) + comp.replaces.split('.').pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.components[slot] = comp;
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
|
|
@ -14,20 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var dis = require("../../dispatcher");
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
logOut: function() {
|
/**
|
||||||
dis.dispatch({action: 'logout'});
|
* Returns true iff this event arriving in a room should affect the room's
|
||||||
if (this.props.onFinished) {
|
* count of unread messages
|
||||||
this.props.onFinished();
|
*/
|
||||||
}
|
eventTriggersUnreadCount: function(ev) {
|
||||||
},
|
if (ev.getType() == "m.room.member") {
|
||||||
|
return false;
|
||||||
cancelPrompt: function() {
|
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
||||||
if (this.props.onFinished) {
|
return false;
|
||||||
this.props.onFinished();
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
68
src/UserActivity.js
Normal file
68
src/UserActivity.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var dis = require("./dispatcher");
|
||||||
|
|
||||||
|
var MIN_DISPATCH_INTERVAL = 1 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class watches for user activity (moving the mouse or pressing a key)
|
||||||
|
* and dispatches the user_activity action at times when the user is interacting
|
||||||
|
* with the app (but at a much lower frequency than mouse move events)
|
||||||
|
*/
|
||||||
|
class UserActivity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start listening to user activity
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
document.onmousemove = this._onUserActivity.bind(this);
|
||||||
|
document.onkeypress = this._onUserActivity.bind(this);
|
||||||
|
this.lastActivityAtTs = new Date().getTime();
|
||||||
|
this.lastDispatchAtTs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop tracking user activity
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
document.onmousemove = undefined;
|
||||||
|
document.onkeypress = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onUserActivity(event) {
|
||||||
|
if (event.screenX) {
|
||||||
|
if (event.screenX === this.lastScreenX &&
|
||||||
|
event.screenY === this.lastScreenY)
|
||||||
|
{
|
||||||
|
// mouse hasn't actually moved
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastScreenX = event.screenX;
|
||||||
|
this.lastScreenY = event.screenY;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastActivityAtTs = (new Date).getTime();
|
||||||
|
if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL) {
|
||||||
|
this.lastDispatchAtTs = this.lastActivityAtTs;
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'user_activity'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new UserActivity();
|
113
src/Velociraptor.js
Normal file
113
src/Velociraptor.js
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
var React = require('react');
|
||||||
|
var ReactDom = require('react-dom');
|
||||||
|
var Velocity = require('velocity-animate');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Velociraptor contains components and animates transitions with velocity.
|
||||||
|
* It will only pick up direct changes to properties ('left', currently), and so
|
||||||
|
* will not work for animating positional changes where the position is implicit
|
||||||
|
* from DOM order. This makes it a lot simpler and lighter: if you need fully
|
||||||
|
* automatic positional animation, look at react-shuffle or similar libraries.
|
||||||
|
*/
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'Velociraptor',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
children: React.PropTypes.array,
|
||||||
|
transition: React.PropTypes.object,
|
||||||
|
container: React.PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this.children = {};
|
||||||
|
this.nodes = {};
|
||||||
|
var self = this;
|
||||||
|
React.Children.map(this.props.children, function(c) {
|
||||||
|
self.children[c.key] = c;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
var self = this;
|
||||||
|
var oldChildren = this.children;
|
||||||
|
this.children = {};
|
||||||
|
React.Children.map(nextProps.children, function(c) {
|
||||||
|
if (oldChildren[c.key]) {
|
||||||
|
var old = oldChildren[c.key];
|
||||||
|
var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
|
||||||
|
|
||||||
|
if (oldNode.style.left != c.props.style.left) {
|
||||||
|
Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
|
||||||
|
// special case visibility because it's nonsensical to animate an invisible element
|
||||||
|
// so we always hidden->visible pre-transition and visible->hidden after
|
||||||
|
if (oldNode.style.visibility == 'visible' && c.props.style.visibility == 'hidden') {
|
||||||
|
oldNode.style.visibility = c.props.style.visibility;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') {
|
||||||
|
oldNode.style.visibility = c.props.style.visibility;
|
||||||
|
}
|
||||||
|
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
||||||
|
}
|
||||||
|
self.children[c.key] = old;
|
||||||
|
} else {
|
||||||
|
// new element. If it has a startStyle, use that as the style and go through
|
||||||
|
// the enter animations
|
||||||
|
var newProps = {
|
||||||
|
ref: self.collectNode.bind(self, c.key)
|
||||||
|
};
|
||||||
|
if (c.props.startStyle && Object.keys(c.props.startStyle).length) {
|
||||||
|
var startStyle = c.props.startStyle;
|
||||||
|
if (Array.isArray(startStyle)) {
|
||||||
|
startStyle = startStyle[0];
|
||||||
|
}
|
||||||
|
newProps._restingStyle = c.props.style;
|
||||||
|
newProps.style = startStyle;
|
||||||
|
//console.log("mounted@startstyle0: "+JSON.stringify(startStyle));
|
||||||
|
// apply the enter animations once it's mounted
|
||||||
|
}
|
||||||
|
self.children[c.key] = React.cloneElement(c, newProps);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
collectNode: function(k, node) {
|
||||||
|
if (
|
||||||
|
this.nodes[k] === undefined &&
|
||||||
|
node.props.startStyle &&
|
||||||
|
Object.keys(node.props.startStyle).length
|
||||||
|
) {
|
||||||
|
var domNode = ReactDom.findDOMNode(node);
|
||||||
|
var startStyles = node.props.startStyle;
|
||||||
|
var transitionOpts = node.props.enterTransitionOpts;
|
||||||
|
if (!Array.isArray(startStyles)) {
|
||||||
|
startStyles = [ startStyles ];
|
||||||
|
transitionOpts = [ transitionOpts ];
|
||||||
|
}
|
||||||
|
// start from startStyle 1: 0 is the one we gave it
|
||||||
|
// to start with, so now we animate 1 etc.
|
||||||
|
for (var i = 1; i < startStyles.length; ++i) {
|
||||||
|
Velocity(domNode, startStyles[i], transitionOpts[i-1]);
|
||||||
|
//console.log("start: "+JSON.stringify(startStyles[i]));
|
||||||
|
}
|
||||||
|
// and then we animate to the resting state
|
||||||
|
Velocity(domNode, node.props._restingStyle, transitionOpts[i-1]);
|
||||||
|
//console.log("enter: "+JSON.stringify(node.props._restingStyle));
|
||||||
|
}
|
||||||
|
this.nodes[k] = node;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var self = this;
|
||||||
|
var childList = Object.keys(this.children).map(function(k) {
|
||||||
|
return React.cloneElement(self.children[k], {
|
||||||
|
ref: self.collectNode.bind(self, self.children[k].key)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{childList}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
15
src/VelocityBounce.js
Normal file
15
src/VelocityBounce.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
var Velocity = require('velocity-animate');
|
||||||
|
|
||||||
|
// courtesy of https://github.com/julianshapiro/velocity/issues/283
|
||||||
|
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
|
||||||
|
function bounce( p ) {
|
||||||
|
var pow2,
|
||||||
|
bounce = 4;
|
||||||
|
|
||||||
|
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {}
|
||||||
|
return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
Velocity.Easings.easeOutBounce = function(p) {
|
||||||
|
return 1 - bounce(1 - p);
|
||||||
|
}
|
75
src/component-index.js
Normal file
75
src/component-index.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* THIS FILE IS AUTO-GENERATED
|
||||||
|
* You can edit it you like, but your changes will be overwritten,
|
||||||
|
* so you'd just be trying to swim upstream like a salmon.
|
||||||
|
* You are not a salmon.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports.components = {};
|
||||||
|
module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom');
|
||||||
|
module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
|
||||||
|
module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
|
||||||
|
module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
|
||||||
|
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
|
||||||
|
module.exports.components['structures.RoomView'] = require('./components/structures/RoomView');
|
||||||
|
module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar');
|
||||||
|
module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings');
|
||||||
|
module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar');
|
||||||
|
module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar');
|
||||||
|
module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton');
|
||||||
|
module.exports.components['views.create_room.Presets'] = require('./components/views/create_room/Presets');
|
||||||
|
module.exports.components['views.create_room.RoomAlias'] = require('./components/views/create_room/RoomAlias');
|
||||||
|
module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog');
|
||||||
|
module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt');
|
||||||
|
module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog');
|
||||||
|
module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText');
|
||||||
|
module.exports.components['views.elements.ProgressBar'] = require('./components/views/elements/ProgressBar');
|
||||||
|
module.exports.components['views.elements.UserSelector'] = require('./components/views/elements/UserSelector');
|
||||||
|
module.exports.components['views.login.CaptchaForm'] = require('./components/views/login/CaptchaForm');
|
||||||
|
module.exports.components['views.login.CasLogin'] = require('./components/views/login/CasLogin');
|
||||||
|
module.exports.components['views.login.CustomServerDialog'] = require('./components/views/login/CustomServerDialog');
|
||||||
|
module.exports.components['views.login.LoginFooter'] = require('./components/views/login/LoginFooter');
|
||||||
|
module.exports.components['views.login.LoginHeader'] = require('./components/views/login/LoginHeader');
|
||||||
|
module.exports.components['views.login.PasswordLogin'] = require('./components/views/login/PasswordLogin');
|
||||||
|
module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm');
|
||||||
|
module.exports.components['views.login.ServerConfig'] = require('./components/views/login/ServerConfig');
|
||||||
|
module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
|
||||||
|
module.exports.components['views.messages.MFileBody'] = require('./components/views/messages/MFileBody');
|
||||||
|
module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody');
|
||||||
|
module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody');
|
||||||
|
module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody');
|
||||||
|
module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
|
||||||
|
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
|
||||||
|
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile');
|
||||||
|
module.exports.components['views.rooms.MemberInfo'] = require('./components/views/rooms/MemberInfo');
|
||||||
|
module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList');
|
||||||
|
module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile');
|
||||||
|
module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer');
|
||||||
|
module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader');
|
||||||
|
module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList');
|
||||||
|
module.exports.components['views.rooms.RoomSettings'] = require('./components/views/rooms/RoomSettings');
|
||||||
|
module.exports.components['views.rooms.RoomTile'] = require('./components/views/rooms/RoomTile');
|
||||||
|
module.exports.components['views.settings.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar');
|
||||||
|
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
|
||||||
|
module.exports.components['views.settings.ChangePassword'] = require('./components/views/settings/ChangePassword');
|
||||||
|
module.exports.components['views.settings.EnableNotificationsButton'] = require('./components/views/settings/EnableNotificationsButton');
|
||||||
|
module.exports.components['views.voip.CallView'] = require('./components/views/voip/CallView');
|
||||||
|
module.exports.components['views.voip.IncomingCallBox'] = require('./components/views/voip/IncomingCallBox');
|
||||||
|
module.exports.components['views.voip.VideoFeed'] = require('./components/views/voip/VideoFeed');
|
||||||
|
module.exports.components['views.voip.VideoView'] = require('./components/views/voip/VideoView');
|
290
src/components/structures/CreateRoom.js
Normal file
290
src/components/structures/CreateRoom.js
Normal file
|
@ -0,0 +1,290 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require("react");
|
||||||
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
|
var PresetValues = {
|
||||||
|
PrivateChat: "private_chat",
|
||||||
|
PublicChat: "public_chat",
|
||||||
|
Custom: "custom",
|
||||||
|
};
|
||||||
|
var q = require('q');
|
||||||
|
var encryption = require("../../encryption");
|
||||||
|
var sdk = require('../../index');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'CreateRoom',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
onRoomCreated: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
phases: {
|
||||||
|
CONFIG: "CONFIG", // We're waiting for user to configure and hit create.
|
||||||
|
CREATING: "CREATING", // We're sending the request.
|
||||||
|
CREATED: "CREATED", // We successfully created the room.
|
||||||
|
ERROR: "ERROR", // There was an error while trying to create room.
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
onRoomCreated: function() {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
phase: this.phases.CONFIG,
|
||||||
|
error_string: "",
|
||||||
|
is_private: true,
|
||||||
|
share_history: false,
|
||||||
|
default_preset: PresetValues.PrivateChat,
|
||||||
|
topic: '',
|
||||||
|
room_name: '',
|
||||||
|
invited_users: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onCreateRoom: function() {
|
||||||
|
var options = {};
|
||||||
|
|
||||||
|
if (this.state.room_name) {
|
||||||
|
options.name = this.state.room_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.topic) {
|
||||||
|
options.topic = this.state.topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.preset) {
|
||||||
|
if (this.state.preset != PresetValues.Custom) {
|
||||||
|
options.preset = this.state.preset;
|
||||||
|
} else {
|
||||||
|
options.initial_state = [
|
||||||
|
{
|
||||||
|
type: "m.room.join_rules",
|
||||||
|
content: {
|
||||||
|
"join_rule": this.state.is_private ? "invite" : "public"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "m.room.history_visibility",
|
||||||
|
content: {
|
||||||
|
"history_visibility": this.state.share_history ? "shared" : "invited"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options.invite = this.state.invited_users;
|
||||||
|
|
||||||
|
var alias = this.getAliasLocalpart();
|
||||||
|
if (alias) {
|
||||||
|
options.room_alias_name = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
if (!cli) {
|
||||||
|
// TODO: Error.
|
||||||
|
console.error("Cannot create room: No matrix client.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deferred = cli.createRoom(options);
|
||||||
|
|
||||||
|
var response;
|
||||||
|
|
||||||
|
if (this.state.encrypt) {
|
||||||
|
deferred = deferred.then(function(res) {
|
||||||
|
response = res;
|
||||||
|
return encryption.enableEncryption(
|
||||||
|
cli, response.room_id, options.invite
|
||||||
|
);
|
||||||
|
}).then(function() {
|
||||||
|
return q(response) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
phase: this.phases.CREATING,
|
||||||
|
});
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
deferred.then(function (resp) {
|
||||||
|
self.setState({
|
||||||
|
phase: self.phases.CREATED,
|
||||||
|
});
|
||||||
|
self.props.onRoomCreated(resp.room_id);
|
||||||
|
}, function(err) {
|
||||||
|
self.setState({
|
||||||
|
phase: self.phases.ERROR,
|
||||||
|
error_string: err.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getPreset: function() {
|
||||||
|
return this.refs.presets.getPreset();
|
||||||
|
},
|
||||||
|
|
||||||
|
getName: function() {
|
||||||
|
return this.refs.name_textbox.getName();
|
||||||
|
},
|
||||||
|
|
||||||
|
getTopic: function() {
|
||||||
|
return this.refs.topic.getTopic();
|
||||||
|
},
|
||||||
|
|
||||||
|
getAliasLocalpart: function() {
|
||||||
|
return this.refs.alias.getAliasLocalpart();
|
||||||
|
},
|
||||||
|
|
||||||
|
getInvitedUsers: function() {
|
||||||
|
return this.refs.user_selector.getUserIds();
|
||||||
|
},
|
||||||
|
|
||||||
|
onPresetChanged: function(preset) {
|
||||||
|
switch (preset) {
|
||||||
|
case PresetValues.PrivateChat:
|
||||||
|
this.setState({
|
||||||
|
preset: preset,
|
||||||
|
is_private: true,
|
||||||
|
share_history: false,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case PresetValues.PublicChat:
|
||||||
|
this.setState({
|
||||||
|
preset: preset,
|
||||||
|
is_private: false,
|
||||||
|
share_history: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case PresetValues.Custom:
|
||||||
|
this.setState({
|
||||||
|
preset: preset,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onPrivateChanged: function(ev) {
|
||||||
|
this.setState({
|
||||||
|
preset: PresetValues.Custom,
|
||||||
|
is_private: ev.target.checked,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareHistoryChanged: function(ev) {
|
||||||
|
this.setState({
|
||||||
|
preset: PresetValues.Custom,
|
||||||
|
share_history: ev.target.checked,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onTopicChange: function(ev) {
|
||||||
|
this.setState({
|
||||||
|
topic: ev.target.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onNameChange: function(ev) {
|
||||||
|
this.setState({
|
||||||
|
room_name: ev.target.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onInviteChanged: function(invited_users) {
|
||||||
|
this.setState({
|
||||||
|
invited_users: invited_users,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onAliasChanged: function(alias) {
|
||||||
|
this.setState({
|
||||||
|
alias: alias
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onEncryptChanged: function(ev) {
|
||||||
|
this.setState({
|
||||||
|
encrypt: ev.target.checked,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var curr_phase = this.state.phase;
|
||||||
|
if (curr_phase == this.phases.CREATING) {
|
||||||
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
return (
|
||||||
|
<Loader/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var error_box = "";
|
||||||
|
if (curr_phase == this.phases.ERROR) {
|
||||||
|
error_box = (
|
||||||
|
<div className="mx_Error">
|
||||||
|
An error occured: {this.state.error_string}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
|
||||||
|
var RoomAlias = sdk.getComponent("create_room.RoomAlias");
|
||||||
|
var Presets = sdk.getComponent("create_room.Presets");
|
||||||
|
var UserSelector = sdk.getComponent("elements.UserSelector");
|
||||||
|
var RoomHeader = sdk.getComponent("rooms.RoomHeader");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_CreateRoom">
|
||||||
|
<RoomHeader simpleHeader="Create room" />
|
||||||
|
<div className="mx_CreateRoom_body">
|
||||||
|
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder="Name"/> <br />
|
||||||
|
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder="Topic"/> <br />
|
||||||
|
<RoomAlias ref="alias" alias={this.state.alias} onChange={this.onAliasChanged}/> <br />
|
||||||
|
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
|
||||||
|
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged}/>
|
||||||
|
Make this room private
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged}/>
|
||||||
|
Share message history with new users
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="mx_CreateRoom_encrypt">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged}/>
|
||||||
|
Encrypt room
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CreateRoomButton onCreateRoom={this.onCreateRoom} /> <br />
|
||||||
|
</div>
|
||||||
|
{error_box}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -13,19 +13,37 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
var React = require('react');
|
||||||
|
var Matrix = require("matrix-js-sdk");
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
|
var Notifier = require("../../Notifier");
|
||||||
|
var ContextualMenu = require("../../ContextualMenu");
|
||||||
var RoomListSorter = require("../../RoomListSorter");
|
var RoomListSorter = require("../../RoomListSorter");
|
||||||
|
var UserActivity = require("../../UserActivity");
|
||||||
var Presence = require("../../Presence");
|
var Presence = require("../../Presence");
|
||||||
var dis = require("../../dispatcher");
|
var dis = require("../../dispatcher");
|
||||||
|
|
||||||
|
var Login = require("./login/Login");
|
||||||
|
var Registration = require("./login/Registration");
|
||||||
|
var PostRegistration = require("./login/PostRegistration");
|
||||||
|
|
||||||
|
var Modal = require("../../Modal");
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
var MatrixTools = require('../../MatrixTools');
|
var MatrixTools = require('../../MatrixTools');
|
||||||
var linkifyMatrix = require("../../linkify-matrix");
|
var linkifyMatrix = require("../../linkify-matrix");
|
||||||
|
|
||||||
var url = require('url');
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MatrixChat',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
config: React.PropTypes.object.isRequired,
|
||||||
|
ConferenceHandler: React.PropTypes.any,
|
||||||
|
onNewScreen: React.PropTypes.func,
|
||||||
|
registrationUrl: React.PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
PageTypes: {
|
PageTypes: {
|
||||||
RoomView: "room_view",
|
RoomView: "room_view",
|
||||||
UserSettings: "user_settings",
|
UserSettings: "user_settings",
|
||||||
|
@ -43,6 +61,7 @@ module.exports = {
|
||||||
collapse_lhs: false,
|
collapse_lhs: false,
|
||||||
collapse_rhs: false,
|
collapse_rhs: false,
|
||||||
ready: false,
|
ready: false,
|
||||||
|
width: 10000
|
||||||
};
|
};
|
||||||
if (s.logged_in) {
|
if (s.logged_in) {
|
||||||
if (MatrixClientPeg.get().getRooms().length) {
|
if (MatrixClientPeg.get().getRooms().length) {
|
||||||
|
@ -62,6 +81,9 @@ module.exports = {
|
||||||
this.startMatrixClient();
|
this.startMatrixClient();
|
||||||
}
|
}
|
||||||
this.focusComposer = false;
|
this.focusComposer = false;
|
||||||
|
// scrollStateMap is a map from room id to the scroll state returned by
|
||||||
|
// RoomView.getScrollState()
|
||||||
|
this.scrollStateMap = {};
|
||||||
document.addEventListener("keydown", this.onKeyDown);
|
document.addEventListener("keydown", this.onKeyDown);
|
||||||
window.addEventListener("focus", this.onFocus);
|
window.addEventListener("focus", this.onFocus);
|
||||||
if (this.state.logged_in) {
|
if (this.state.logged_in) {
|
||||||
|
@ -78,12 +100,16 @@ module.exports = {
|
||||||
if (this.onUserClick) {
|
if (this.onUserClick) {
|
||||||
linkifyMatrix.onUserClick = this.onUserClick;
|
linkifyMatrix.onUserClick = this.onUserClick;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', this.handleResize);
|
||||||
|
this.handleResize();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
document.removeEventListener("keydown", this.onKeyDown);
|
document.removeEventListener("keydown", this.onKeyDown);
|
||||||
window.removeEventListener("focus", this.onFocus);
|
window.removeEventListener("focus", this.onFocus);
|
||||||
|
window.removeEventListener('resize', this.handleResize);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
|
@ -95,7 +121,6 @@ module.exports = {
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
var roomIndexDelta = 1;
|
var roomIndexDelta = 1;
|
||||||
var Notifier = sdk.getComponent('organisms.Notifier');
|
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
|
@ -104,6 +129,7 @@ module.exports = {
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
}
|
}
|
||||||
Notifier.stop();
|
Notifier.stop();
|
||||||
|
UserActivity.stop();
|
||||||
Presence.stop();
|
Presence.stop();
|
||||||
MatrixClientPeg.get().stopClient();
|
MatrixClientPeg.get().stopClient();
|
||||||
MatrixClientPeg.get().removeAllListeners();
|
MatrixClientPeg.get().removeAllListeners();
|
||||||
|
@ -142,6 +168,11 @@ module.exports = {
|
||||||
});
|
});
|
||||||
this.notifyNewScreen('login');
|
this.notifyNewScreen('login');
|
||||||
break;
|
break;
|
||||||
|
case 'start_post_registration':
|
||||||
|
this.setState({ // don't clobber logged_in status
|
||||||
|
screen: 'post_registration'
|
||||||
|
});
|
||||||
|
break;
|
||||||
case 'token_login':
|
case 'token_login':
|
||||||
if (this.state.logged_in) return;
|
if (this.state.logged_in) return;
|
||||||
|
|
||||||
|
@ -174,28 +205,38 @@ module.exports = {
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'view_room':
|
case 'leave_room':
|
||||||
this.focusComposer = true;
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
var newState = {
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
currentRoom: payload.room_id,
|
|
||||||
page_type: this.PageTypes.RoomView,
|
var roomId = payload.room_id;
|
||||||
};
|
Modal.createDialog(QuestionDialog, {
|
||||||
if (this.sdkReady) {
|
title: "Leave room",
|
||||||
// if the SDK is not ready yet, remember what room
|
description: "Are you sure you want to leave the room?",
|
||||||
// we're supposed to be on but don't notify about
|
onFinished: function(should_leave) {
|
||||||
// the new screen yet (we won't be showing it yet)
|
if (should_leave) {
|
||||||
// The normal case where this happens is navigating
|
var d = MatrixClientPeg.get().leave(roomId);
|
||||||
// to the room in the URL bar on page load.
|
|
||||||
var presentedId = payload.room_id;
|
// FIXME: controller shouldn't be loading a view :(
|
||||||
var room = MatrixClientPeg.get().getRoom(payload.room_id);
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
if (room) {
|
var modal = Modal.createDialog(Loader);
|
||||||
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
|
|
||||||
if (theAlias) presentedId = theAlias;
|
d.then(function() {
|
||||||
|
modal.close();
|
||||||
|
dis.dispatch({action: 'view_next_room'});
|
||||||
|
}, function(err) {
|
||||||
|
modal.close();
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Failed to leave room",
|
||||||
|
description: err.toString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.notifyNewScreen('room/'+presentedId);
|
});
|
||||||
newState.ready = true;
|
break;
|
||||||
}
|
case 'view_room':
|
||||||
this.setState(newState);
|
this._viewRoom(payload.room_id);
|
||||||
break;
|
break;
|
||||||
case 'view_prev_room':
|
case 'view_prev_room':
|
||||||
roomIndexDelta = -1;
|
roomIndexDelta = -1;
|
||||||
|
@ -212,11 +253,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
|
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
|
||||||
if (roomIndex < 0) roomIndex = allRooms.length - 1;
|
if (roomIndex < 0) roomIndex = allRooms.length - 1;
|
||||||
this.focusComposer = true;
|
this._viewRoom(allRooms[roomIndex].roomId);
|
||||||
this.setState({
|
|
||||||
currentRoom: allRooms[roomIndex].roomId
|
|
||||||
});
|
|
||||||
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
|
|
||||||
break;
|
break;
|
||||||
case 'view_indexed_room':
|
case 'view_indexed_room':
|
||||||
var allRooms = RoomListSorter.mostRecentActivityFirst(
|
var allRooms = RoomListSorter.mostRecentActivityFirst(
|
||||||
|
@ -224,11 +261,7 @@ module.exports = {
|
||||||
);
|
);
|
||||||
var roomIndex = payload.roomIndex;
|
var roomIndex = payload.roomIndex;
|
||||||
if (allRooms[roomIndex]) {
|
if (allRooms[roomIndex]) {
|
||||||
this.focusComposer = true;
|
this._viewRoom(allRooms[roomIndex].roomId);
|
||||||
this.setState({
|
|
||||||
currentRoom: allRooms[roomIndex].roomId
|
|
||||||
});
|
|
||||||
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'view_room_alias':
|
case 'view_room_alias':
|
||||||
|
@ -252,21 +285,15 @@ module.exports = {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'view_user_settings':
|
case 'view_user_settings':
|
||||||
this.setState({
|
this._setPage(this.PageTypes.UserSettings);
|
||||||
page_type: this.PageTypes.UserSettings,
|
|
||||||
});
|
|
||||||
this.notifyNewScreen('settings');
|
this.notifyNewScreen('settings');
|
||||||
break;
|
break;
|
||||||
case 'view_create_room':
|
case 'view_create_room':
|
||||||
this.setState({
|
this._setPage(this.PageTypes.CreateRoom);
|
||||||
page_type: this.PageTypes.CreateRoom,
|
|
||||||
});
|
|
||||||
this.notifyNewScreen('new');
|
this.notifyNewScreen('new');
|
||||||
break;
|
break;
|
||||||
case 'view_room_directory':
|
case 'view_room_directory':
|
||||||
this.setState({
|
this._setPage(this.PageTypes.RoomDirectory);
|
||||||
page_type: this.PageTypes.RoomDirectory,
|
|
||||||
});
|
|
||||||
this.notifyNewScreen('directory');
|
this.notifyNewScreen('directory');
|
||||||
break;
|
break;
|
||||||
case 'notifier_enabled':
|
case 'notifier_enabled':
|
||||||
|
@ -295,7 +322,64 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoggedIn: function() {
|
_setPage: function(pageType) {
|
||||||
|
// record the scroll state if we're in a room view.
|
||||||
|
this._updateScrollMap();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
page_type: pageType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_viewRoom: function(roomId) {
|
||||||
|
// before we switch room, record the scroll state of the current room
|
||||||
|
this._updateScrollMap();
|
||||||
|
|
||||||
|
this.focusComposer = true;
|
||||||
|
var newState = {
|
||||||
|
currentRoom: roomId,
|
||||||
|
page_type: this.PageTypes.RoomView,
|
||||||
|
};
|
||||||
|
if (this.sdkReady) {
|
||||||
|
// if the SDK is not ready yet, remember what room
|
||||||
|
// we're supposed to be on but don't notify about
|
||||||
|
// the new screen yet (we won't be showing it yet)
|
||||||
|
// The normal case where this happens is navigating
|
||||||
|
// to the room in the URL bar on page load.
|
||||||
|
var presentedId = roomId;
|
||||||
|
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
if (room) {
|
||||||
|
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
|
||||||
|
if (theAlias) presentedId = theAlias;
|
||||||
|
}
|
||||||
|
this.notifyNewScreen('room/'+presentedId);
|
||||||
|
newState.ready = true;
|
||||||
|
}
|
||||||
|
this.setState(newState);
|
||||||
|
if (this.scrollStateMap[roomId]) {
|
||||||
|
var scrollState = this.scrollStateMap[roomId];
|
||||||
|
this.refs.roomView.restoreScrollState(scrollState);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// update scrollStateMap according to the current scroll state of the
|
||||||
|
// room view.
|
||||||
|
_updateScrollMap: function() {
|
||||||
|
if (!this.refs.roomView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var roomview = this.refs.roomView;
|
||||||
|
var state = roomview.getScrollState();
|
||||||
|
this.scrollStateMap[roomview.props.roomId] = state;
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoggedIn: function(credentials) {
|
||||||
|
console.log("onLoggedIn => %s", credentials.userId);
|
||||||
|
MatrixClientPeg.replaceUsingAccessToken(
|
||||||
|
credentials.homeserverUrl, credentials.identityServerUrl,
|
||||||
|
credentials.userId, credentials.accessToken
|
||||||
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
screen: undefined,
|
screen: undefined,
|
||||||
logged_in: true
|
logged_in: true
|
||||||
|
@ -305,11 +389,14 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
startMatrixClient: function() {
|
startMatrixClient: function() {
|
||||||
var Notifier = sdk.getComponent('organisms.Notifier');
|
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
var self = this;
|
var self = this;
|
||||||
cli.on('sync', function(state) {
|
cli.on('sync', function(state, prevState) {
|
||||||
if (self.sdkReady || state !== "PREPARED") { return; }
|
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("MatrixClient sync state => %s", state);
|
||||||
|
if (state !== "PREPARED") { return; }
|
||||||
self.sdkReady = true;
|
self.sdkReady = true;
|
||||||
|
|
||||||
if (self.starting_room_alias) {
|
if (self.starting_room_alias) {
|
||||||
|
@ -354,8 +441,11 @@ module.exports = {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Notifier.start();
|
Notifier.start();
|
||||||
|
UserActivity.start();
|
||||||
Presence.start();
|
Presence.start();
|
||||||
cli.startClient();
|
cli.startClient({
|
||||||
|
pendingEventOrdering: "end"
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyDown: function(ev) {
|
onKeyDown: function(ev) {
|
||||||
|
@ -420,6 +510,10 @@ module.exports = {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room_directory',
|
action: 'view_room_directory',
|
||||||
});
|
});
|
||||||
|
} else if (screen == 'post_registration') {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'start_post_registration',
|
||||||
|
});
|
||||||
} else if (screen.indexOf('room/') == 0) {
|
} else if (screen.indexOf('room/') == 0) {
|
||||||
var roomString = screen.split('/')[1];
|
var roomString = screen.split('/')[1];
|
||||||
if (roomString[0] == '#') {
|
if (roomString[0] == '#') {
|
||||||
|
@ -441,11 +535,191 @@ module.exports = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
console.error("Unknown screen : %s", screen);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
notifyNewScreen: function(screen) {
|
notifyNewScreen: function(screen) {
|
||||||
if (this.props.onNewScreen) {
|
if (this.props.onNewScreen) {
|
||||||
this.props.onNewScreen(screen);
|
this.props.onNewScreen(screen);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onAliasClick: function(event, alias) {
|
||||||
|
event.preventDefault();
|
||||||
|
dis.dispatch({action: 'view_room_alias', room_alias: alias});
|
||||||
|
},
|
||||||
|
|
||||||
|
onUserClick: function(event, userId) {
|
||||||
|
event.preventDefault();
|
||||||
|
var MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||||
|
var member = new Matrix.RoomMember(null, userId);
|
||||||
|
ContextualMenu.createMenu(MemberInfo, {
|
||||||
|
member: member,
|
||||||
|
right: window.innerWidth - event.pageX,
|
||||||
|
top: event.pageY
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onLogoutClick: function(event) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'logout'
|
||||||
|
});
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleResize: function(e) {
|
||||||
|
var hideLhsThreshold = 1000;
|
||||||
|
var showLhsThreshold = 1000;
|
||||||
|
var hideRhsThreshold = 820;
|
||||||
|
var showRhsThreshold = 820;
|
||||||
|
|
||||||
|
if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
|
||||||
|
dis.dispatch({ action: 'hide_left_panel' });
|
||||||
|
}
|
||||||
|
if (this.state.width <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
|
||||||
|
dis.dispatch({ action: 'show_left_panel' });
|
||||||
|
}
|
||||||
|
if (this.state.width > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
|
||||||
|
dis.dispatch({ action: 'hide_right_panel' });
|
||||||
|
}
|
||||||
|
if (this.state.width <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
|
||||||
|
dis.dispatch({ action: 'show_right_panel' });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({width: window.innerWidth});
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomCreated: function(room_id) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_room",
|
||||||
|
room_id: room_id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onRegisterClick: function() {
|
||||||
|
this.showScreen("register");
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoginClick: function() {
|
||||||
|
this.showScreen("login");
|
||||||
|
},
|
||||||
|
|
||||||
|
onRegistered: function(credentials) {
|
||||||
|
this.onLoggedIn(credentials);
|
||||||
|
// do post-registration stuff
|
||||||
|
this.showScreen("post_registration");
|
||||||
|
},
|
||||||
|
|
||||||
|
onFinishPostRegistration: function() {
|
||||||
|
// Don't confuse this with "PageType" which is the middle window to show
|
||||||
|
this.setState({
|
||||||
|
screen: undefined
|
||||||
|
});
|
||||||
|
this.showScreen("settings");
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||||
|
var RoomView = sdk.getComponent('structures.RoomView');
|
||||||
|
var RightPanel = sdk.getComponent('structures.RightPanel');
|
||||||
|
var UserSettings = sdk.getComponent('structures.UserSettings');
|
||||||
|
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||||
|
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||||
|
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||||
|
|
||||||
|
// needs to be before normal PageTypes as you are logged in technically
|
||||||
|
if (this.state.screen == 'post_registration') {
|
||||||
|
return (
|
||||||
|
<PostRegistration
|
||||||
|
onComplete={this.onFinishPostRegistration} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (this.state.logged_in && this.state.ready) {
|
||||||
|
var page_element;
|
||||||
|
var right_panel = "";
|
||||||
|
|
||||||
|
switch (this.state.page_type) {
|
||||||
|
case this.PageTypes.RoomView:
|
||||||
|
page_element = (
|
||||||
|
<RoomView
|
||||||
|
ref="roomView"
|
||||||
|
roomId={this.state.currentRoom}
|
||||||
|
key={this.state.currentRoom}
|
||||||
|
ConferenceHandler={this.props.ConferenceHandler} />
|
||||||
|
);
|
||||||
|
right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} />
|
||||||
|
break;
|
||||||
|
case this.PageTypes.UserSettings:
|
||||||
|
page_element = <UserSettings />
|
||||||
|
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
|
||||||
|
break;
|
||||||
|
case this.PageTypes.CreateRoom:
|
||||||
|
page_element = <CreateRoom onRoomCreated={this.onRoomCreated}/>
|
||||||
|
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
|
||||||
|
break;
|
||||||
|
case this.PageTypes.RoomDirectory:
|
||||||
|
page_element = <RoomDirectory />
|
||||||
|
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fix duplication here and do conditionals like we do above
|
||||||
|
if (Notifier.supportsDesktopNotifications() && !Notifier.isEnabled() && !Notifier.isToolbarHidden()) {
|
||||||
|
return (
|
||||||
|
<div className="mx_MatrixChat_wrapper">
|
||||||
|
<MatrixToolbar />
|
||||||
|
<div className="mx_MatrixChat mx_MatrixChat_toolbarShowing">
|
||||||
|
<LeftPanel selectedRoom={this.state.currentRoom} collapsed={this.state.collapse_lhs} />
|
||||||
|
<main className="mx_MatrixChat_middlePanel">
|
||||||
|
{page_element}
|
||||||
|
</main>
|
||||||
|
{right_panel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<div className="mx_MatrixChat">
|
||||||
|
<LeftPanel selectedRoom={this.state.currentRoom} collapsed={this.state.collapse_lhs} />
|
||||||
|
<main className="mx_MatrixChat_middlePanel">
|
||||||
|
{page_element}
|
||||||
|
</main>
|
||||||
|
{right_panel}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (this.state.logged_in) {
|
||||||
|
var Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
return (
|
||||||
|
<div className="mx_MatrixChat_splash">
|
||||||
|
<Spinner />
|
||||||
|
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>Logout</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (this.state.screen == 'register') {
|
||||||
|
return (
|
||||||
|
<Registration
|
||||||
|
clientSecret={this.state.register_client_secret}
|
||||||
|
sessionId={this.state.register_session_id}
|
||||||
|
idSid={this.state.register_id_sid}
|
||||||
|
hsUrl={this.props.config.default_hs_url}
|
||||||
|
isUrl={this.props.config.default_is_url}
|
||||||
|
registrationUrl={this.props.registrationUrl}
|
||||||
|
onLoggedIn={this.onRegistered}
|
||||||
|
onLoginClick={this.onLoginClick} />
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Login
|
||||||
|
onLoggedIn={this.onLoggedIn}
|
||||||
|
onRegisterClick={this.onRegisterClick}
|
||||||
|
homeserverUrl={this.props.config.default_hs_url}
|
||||||
|
identityServerUrl={this.props.config.default_is_url} />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
1343
src/components/structures/RoomView.js
Normal file
1343
src/components/structures/RoomView.js
Normal file
File diff suppressed because it is too large
Load diff
93
src/components/structures/UploadBar.js
Normal file
93
src/components/structures/UploadBar.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ContentMessages = require('../../ContentMessages');
|
||||||
|
var dis = require('../../dispatcher');
|
||||||
|
var filesize = require('filesize');
|
||||||
|
|
||||||
|
module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
|
propTypes: {
|
||||||
|
room: React.PropTypes.object
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
dis.register(this.onAction);
|
||||||
|
this.mounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this.mounted = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
onAction: function(payload) {
|
||||||
|
switch (payload.action) {
|
||||||
|
case 'upload_progress':
|
||||||
|
case 'upload_finished':
|
||||||
|
case 'upload_failed':
|
||||||
|
if (this.mounted) this.forceUpdate();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var uploads = ContentMessages.getCurrentUploads();
|
||||||
|
if (uploads.length == 0) {
|
||||||
|
return <div />
|
||||||
|
}
|
||||||
|
|
||||||
|
var upload;
|
||||||
|
for (var i = 0; i < uploads.length; ++i) {
|
||||||
|
if (uploads[i].roomId == this.props.room.roomId) {
|
||||||
|
upload = uploads[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!upload) {
|
||||||
|
upload = uploads[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
var innerProgressStyle = {
|
||||||
|
width: ((upload.loaded / (upload.total || 1)) * 100) + '%'
|
||||||
|
};
|
||||||
|
var uploadedSize = filesize(upload.loaded);
|
||||||
|
var totalSize = filesize(upload.total);
|
||||||
|
if (uploadedSize.replace(/^.* /,'') === totalSize.replace(/^.* /,'')) {
|
||||||
|
uploadedSize = uploadedSize.replace(/ .*/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
var others;
|
||||||
|
if (uploads.length > 1) {
|
||||||
|
others = 'and '+(uploads.length - 1) + ' other' + (uploads.length > 2 ? 's' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_UploadBar">
|
||||||
|
<div className="mx_UploadBar_uploadProgressOuter">
|
||||||
|
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
|
||||||
|
</div>
|
||||||
|
<img className="mx_UploadBar_uploadIcon" src="img/fileicon.png" width="17" height="22"/>
|
||||||
|
<img className="mx_UploadBar_uploadCancel" src="img/cancel.svg" width="18" height="18"
|
||||||
|
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
|
||||||
|
/>
|
||||||
|
<div className="mx_UploadBar_uploadBytes">
|
||||||
|
{ uploadedSize } / { totalSize }
|
||||||
|
</div>
|
||||||
|
<div className="mx_UploadBar_uploadFilename">Uploading {upload.fileName}{others}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
162
src/components/structures/UserSettings.js
Normal file
162
src/components/structures/UserSettings.js
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
var React = require('react');
|
||||||
|
var sdk = require('../../index');
|
||||||
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
|
var Modal = require('../../Modal');
|
||||||
|
var q = require('q');
|
||||||
|
var version = require('../../../package.json').version;
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'UserSettings',
|
||||||
|
Phases: {
|
||||||
|
Loading: "loading",
|
||||||
|
Display: "display",
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
avatarUrl: null,
|
||||||
|
threePids: [],
|
||||||
|
clientVersion: version,
|
||||||
|
phase: this.Phases.Loading,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
var self = this;
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
var profile_d = cli.getProfileInfo(cli.credentials.userId);
|
||||||
|
var threepid_d = cli.getThreePids();
|
||||||
|
|
||||||
|
q.all([profile_d, threepid_d]).then(
|
||||||
|
function(resps) {
|
||||||
|
self.setState({
|
||||||
|
avatarUrl: resps[0].avatar_url,
|
||||||
|
threepids: resps[1].threepids,
|
||||||
|
phase: self.Phases.Display,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(err) { console.err(err); }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
editAvatar: function() {
|
||||||
|
var url = MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl);
|
||||||
|
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||||
|
var avatarDialog = (
|
||||||
|
<div>
|
||||||
|
<ChangeAvatar initialAvatarUrl={url} />
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button onClick={this.onAvatarDialogCancel}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
this.avatarDialog = Modal.createDialogWithElement(avatarDialog);
|
||||||
|
},
|
||||||
|
|
||||||
|
addEmail: function() {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
editDisplayName: function() {
|
||||||
|
this.refs.displayname.edit();
|
||||||
|
},
|
||||||
|
|
||||||
|
changePassword: function() {
|
||||||
|
var ChangePassword = sdk.getComponent('settings.ChangePassword');
|
||||||
|
Modal.createDialog(ChangePassword);
|
||||||
|
},
|
||||||
|
|
||||||
|
onLogoutClicked: function(ev) {
|
||||||
|
var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt');
|
||||||
|
this.logoutModal = Modal.createDialog(LogoutPrompt, {onCancel: this.onLogoutPromptCancel});
|
||||||
|
},
|
||||||
|
|
||||||
|
onLogoutPromptCancel: function() {
|
||||||
|
this.logoutModal.closeDialog();
|
||||||
|
},
|
||||||
|
|
||||||
|
onAvatarDialogCancel: function() {
|
||||||
|
this.avatarDialog.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
if (this.state.phase === this.Phases.Loading) {
|
||||||
|
return <Loader />
|
||||||
|
}
|
||||||
|
else if (this.state.phase === this.Phases.Display) {
|
||||||
|
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||||
|
var EnableNotificationsButton = sdk.getComponent('settings.EnableNotificationsButton');
|
||||||
|
return (
|
||||||
|
<div className="mx_UserSettings">
|
||||||
|
<div className="mx_UserSettings_User">
|
||||||
|
<h1>User Settings</h1>
|
||||||
|
<hr/>
|
||||||
|
<div className="mx_UserSettings_User_Inner">
|
||||||
|
<div className="mx_UserSettings_Avatar">
|
||||||
|
<div className="mx_UserSettings_Avatar_Text">
|
||||||
|
Profile Photo
|
||||||
|
</div>
|
||||||
|
<div className="mx_UserSettings_Avatar_Edit" onClick={this.editAvatar}>
|
||||||
|
Edit
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_UserSettings_DisplayName">
|
||||||
|
<ChangeDisplayName ref="displayname" />
|
||||||
|
<div className="mx_UserSettings_DisplayName_Edit" onClick={this.editDisplayName}>
|
||||||
|
Edit
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_UserSettings_3pids">
|
||||||
|
{this.state.threepids.map(function(val) {
|
||||||
|
return <div key={val.address}>{val.address}</div>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_UserSettings_Add3pid" onClick={this.addEmail}>
|
||||||
|
Add email
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_UserSettings_Global">
|
||||||
|
<h1>Global Settings</h1>
|
||||||
|
<hr/>
|
||||||
|
<div className="mx_UserSettings_Global_Inner">
|
||||||
|
<div className="mx_UserSettings_ChangePassword" onClick={this.changePassword}>
|
||||||
|
Change Password
|
||||||
|
</div>
|
||||||
|
<div className="mx_UserSettings_ClientVersion">
|
||||||
|
Version {this.state.clientVersion}
|
||||||
|
</div>
|
||||||
|
<div className="mx_UserSettings_EnableNotifications">
|
||||||
|
<EnableNotificationsButton />
|
||||||
|
</div>
|
||||||
|
<div className="mx_UserSettings_Logout">
|
||||||
|
<button onClick={this.onLogoutClicked}>Sign Out</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
194
src/components/structures/login/Login.js
Normal file
194
src/components/structures/login/Login.js
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ReactDOM = require('react-dom');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var Signup = require("../../../Signup");
|
||||||
|
var PasswordLogin = require("../../views/login/PasswordLogin");
|
||||||
|
var CasLogin = require("../../views/login/CasLogin");
|
||||||
|
var ServerConfig = require("../../views/login/ServerConfig");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wire component which glues together login UI components and Signup logic
|
||||||
|
*/
|
||||||
|
module.exports = React.createClass({displayName: 'Login',
|
||||||
|
propTypes: {
|
||||||
|
onLoggedIn: React.PropTypes.func.isRequired,
|
||||||
|
homeserverUrl: React.PropTypes.string,
|
||||||
|
identityServerUrl: React.PropTypes.string,
|
||||||
|
// login shouldn't know or care how registration is done.
|
||||||
|
onRegisterClick: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
homeserverUrl: 'https://matrix.org/',
|
||||||
|
identityServerUrl: 'https://matrix.org'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
busy: false,
|
||||||
|
errorText: null,
|
||||||
|
enteredHomeserverUrl: this.props.homeserverUrl,
|
||||||
|
enteredIdentityServerUrl: this.props.identityServerUrl
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._initLoginLogic();
|
||||||
|
},
|
||||||
|
|
||||||
|
onPasswordLogin: function(username, password) {
|
||||||
|
var self = this;
|
||||||
|
self.setState({
|
||||||
|
busy: true
|
||||||
|
});
|
||||||
|
|
||||||
|
this._loginLogic.loginViaPassword(username, password).then(function(data) {
|
||||||
|
self.props.onLoggedIn(data);
|
||||||
|
}, function(error) {
|
||||||
|
self._setErrorTextFromError(error);
|
||||||
|
}).finally(function() {
|
||||||
|
self.setState({
|
||||||
|
busy: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onHsUrlChanged: function(newHsUrl) {
|
||||||
|
this._initLoginLogic(newHsUrl);
|
||||||
|
},
|
||||||
|
|
||||||
|
onIsUrlChanged: function(newIsUrl) {
|
||||||
|
this._initLoginLogic(null, newIsUrl);
|
||||||
|
},
|
||||||
|
|
||||||
|
_initLoginLogic: function(hsUrl, isUrl) {
|
||||||
|
var self = this;
|
||||||
|
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
||||||
|
isUrl = isUrl || this.state.enteredIdentityServerUrl;
|
||||||
|
|
||||||
|
var loginLogic = new Signup.Login(hsUrl, isUrl);
|
||||||
|
this._loginLogic = loginLogic;
|
||||||
|
|
||||||
|
loginLogic.getFlows().then(function(flows) {
|
||||||
|
// old behaviour was to always use the first flow without presenting
|
||||||
|
// options. This works in most cases (we don't have a UI for multiple
|
||||||
|
// logins so let's skip that for now).
|
||||||
|
loginLogic.chooseFlow(0);
|
||||||
|
}, function(err) {
|
||||||
|
self._setErrorTextFromError(err);
|
||||||
|
}).finally(function() {
|
||||||
|
self.setState({
|
||||||
|
busy: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
enteredHomeserverUrl: hsUrl,
|
||||||
|
enteredIdentityServerUrl: isUrl,
|
||||||
|
busy: true,
|
||||||
|
errorText: null // reset err messages
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_getCurrentFlowStep: function() {
|
||||||
|
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null
|
||||||
|
},
|
||||||
|
|
||||||
|
_setErrorTextFromError: function(err) {
|
||||||
|
if (err.friendlyText) {
|
||||||
|
this.setState({
|
||||||
|
errorText: err.friendlyText
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var errCode = err.errcode;
|
||||||
|
if (!errCode && err.httpStatus) {
|
||||||
|
errCode = "HTTP " + err.httpStatus;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
errorText: (
|
||||||
|
"Error: Problem communicating with the given homeserver " +
|
||||||
|
(errCode ? "(" + errCode + ")" : "")
|
||||||
|
)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentForStep: function(step) {
|
||||||
|
switch (step) {
|
||||||
|
case 'm.login.password':
|
||||||
|
return (
|
||||||
|
<PasswordLogin onSubmit={this.onPasswordLogin} />
|
||||||
|
);
|
||||||
|
case 'm.login.cas':
|
||||||
|
return (
|
||||||
|
<CasLogin />
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
if (!step) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Sorry, this homeserver is using a login which is not
|
||||||
|
recognised ({step})
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
var LoginHeader = sdk.getComponent("login.LoginHeader");
|
||||||
|
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||||
|
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_Login">
|
||||||
|
<div className="mx_Login_box">
|
||||||
|
<LoginHeader />
|
||||||
|
<div>
|
||||||
|
<h2>Sign in</h2>
|
||||||
|
{ this.componentForStep(this._getCurrentFlowStep()) }
|
||||||
|
<ServerConfig ref="serverConfig"
|
||||||
|
withToggleButton={true}
|
||||||
|
defaultHsUrl={this.props.homeserverUrl}
|
||||||
|
defaultIsUrl={this.props.identityServerUrl}
|
||||||
|
onHsUrlChanged={this.onHsUrlChanged}
|
||||||
|
onIsUrlChanged={this.onIsUrlChanged}
|
||||||
|
delayTimeMs={1000}/>
|
||||||
|
<div className="mx_Login_error">
|
||||||
|
{ loader }
|
||||||
|
{ this.state.errorText }
|
||||||
|
</div>
|
||||||
|
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||||
|
Create a new account
|
||||||
|
</a>
|
||||||
|
<br/>
|
||||||
|
<LoginFooter />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
79
src/components/structures/login/PostRegistration.js
Normal file
79
src/components/structures/login/PostRegistration.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'PostRegistration',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
onComplete: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
avatarUrl: null,
|
||||||
|
errorString: null,
|
||||||
|
busy: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
|
||||||
|
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
|
||||||
|
// the URL to be passed to you (because it's also used for room avatars).
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
this.setState({busy: true});
|
||||||
|
var self = this;
|
||||||
|
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
|
||||||
|
self.setState({
|
||||||
|
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
|
||||||
|
busy: false
|
||||||
|
});
|
||||||
|
}, function(error) {
|
||||||
|
self.setState({
|
||||||
|
errorString: "Failed to fetch avatar URL",
|
||||||
|
busy: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||||
|
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||||
|
var LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||||
|
return (
|
||||||
|
<div className="mx_Login">
|
||||||
|
<div className="mx_Login_box">
|
||||||
|
<LoginHeader />
|
||||||
|
<div className="mx_Login_profile">
|
||||||
|
Set a display name:
|
||||||
|
<ChangeDisplayName />
|
||||||
|
Upload an avatar:
|
||||||
|
<ChangeAvatar
|
||||||
|
initialAvatarUrl={this.state.avatarUrl} />
|
||||||
|
<button onClick={this.props.onComplete}>Continue</button>
|
||||||
|
{this.state.errorString}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
247
src/components/structures/login/Registration.js
Normal file
247
src/components/structures/login/Registration.js
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var dis = require('../../../dispatcher');
|
||||||
|
var Signup = require("../../../Signup");
|
||||||
|
var ServerConfig = require("../../views/login/ServerConfig");
|
||||||
|
var RegistrationForm = require("../../views/login/RegistrationForm");
|
||||||
|
var CaptchaForm = require("../../views/login/CaptchaForm");
|
||||||
|
|
||||||
|
var MIN_PASSWORD_LENGTH = 6;
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'Registration',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
onLoggedIn: React.PropTypes.func.isRequired,
|
||||||
|
clientSecret: React.PropTypes.string,
|
||||||
|
sessionId: React.PropTypes.string,
|
||||||
|
registrationUrl: React.PropTypes.string,
|
||||||
|
idSid: React.PropTypes.string,
|
||||||
|
hsUrl: React.PropTypes.string,
|
||||||
|
isUrl: React.PropTypes.string,
|
||||||
|
// registration shouldn't know or care how login is done.
|
||||||
|
onLoginClick: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
busy: false,
|
||||||
|
errorText: null,
|
||||||
|
enteredHomeserverUrl: this.props.hsUrl,
|
||||||
|
enteredIdentityServerUrl: this.props.isUrl
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
// attach this to the instance rather than this.state since it isn't UI
|
||||||
|
this.registerLogic = new Signup.Register(
|
||||||
|
this.props.hsUrl, this.props.isUrl
|
||||||
|
);
|
||||||
|
this.registerLogic.setClientSecret(this.props.clientSecret);
|
||||||
|
this.registerLogic.setSessionId(this.props.sessionId);
|
||||||
|
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
|
||||||
|
this.registerLogic.setIdSid(this.props.idSid);
|
||||||
|
this.registerLogic.recheckState();
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
// may have already done an HTTP hit (e.g. redirect from an email) so
|
||||||
|
// check for any pending response
|
||||||
|
var promise = this.registerLogic.getPromise();
|
||||||
|
if (promise) {
|
||||||
|
this.onProcessingRegistration(promise);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onHsUrlChanged: function(newHsUrl) {
|
||||||
|
this.registerLogic.setHomeserverUrl(newHsUrl);
|
||||||
|
},
|
||||||
|
|
||||||
|
onIsUrlChanged: function(newIsUrl) {
|
||||||
|
this.registerLogic.setIdentityServerUrl(newIsUrl);
|
||||||
|
},
|
||||||
|
|
||||||
|
onAction: function(payload) {
|
||||||
|
if (payload.action !== "registration_step_update") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.forceUpdate(); // registration state has changed.
|
||||||
|
},
|
||||||
|
|
||||||
|
onFormSubmit: function(formVals) {
|
||||||
|
var self = this;
|
||||||
|
this.setState({
|
||||||
|
errorText: "",
|
||||||
|
busy: true
|
||||||
|
});
|
||||||
|
this.onProcessingRegistration(this.registerLogic.register(formVals));
|
||||||
|
},
|
||||||
|
|
||||||
|
// Promise is resolved when the registration process is FULLY COMPLETE
|
||||||
|
onProcessingRegistration: function(promise) {
|
||||||
|
var self = this;
|
||||||
|
promise.done(function(response) {
|
||||||
|
if (!response || !response.access_token) {
|
||||||
|
console.warn(
|
||||||
|
"FIXME: Register fulfilled without a final response, " +
|
||||||
|
"did you break the promise chain?"
|
||||||
|
);
|
||||||
|
// no matter, we'll grab it direct
|
||||||
|
response = self.registerLogic.getCredentials();
|
||||||
|
}
|
||||||
|
if (!response || !response.user_id || !response.access_token) {
|
||||||
|
console.error("Final response is missing keys.");
|
||||||
|
self.setState({
|
||||||
|
errorText: "There was a problem processing the response."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.props.onLoggedIn({
|
||||||
|
userId: response.user_id,
|
||||||
|
homeserverUrl: self.registerLogic.getHomeserverUrl(),
|
||||||
|
identityServerUrl: self.registerLogic.getIdentityServerUrl(),
|
||||||
|
accessToken: response.access_token
|
||||||
|
});
|
||||||
|
self.setState({
|
||||||
|
busy: false
|
||||||
|
});
|
||||||
|
}, function(err) {
|
||||||
|
if (err.message) {
|
||||||
|
self.setState({
|
||||||
|
errorText: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.setState({
|
||||||
|
busy: false
|
||||||
|
});
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onFormValidationFailed: function(errCode) {
|
||||||
|
var errMsg;
|
||||||
|
switch (errCode) {
|
||||||
|
case "RegistrationForm.ERR_PASSWORD_MISSING":
|
||||||
|
errMsg = "Missing password.";
|
||||||
|
break;
|
||||||
|
case "RegistrationForm.ERR_PASSWORD_MISMATCH":
|
||||||
|
errMsg = "Passwords don't match.";
|
||||||
|
break;
|
||||||
|
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
||||||
|
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("Unknown error code: %s", errCode);
|
||||||
|
errMsg = "An unknown error occurred.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
errorText: errMsg
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onCaptchaLoaded: function(divIdName) {
|
||||||
|
this.registerLogic.tellStage("m.login.recaptcha", {
|
||||||
|
divId: divIdName
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
busy: false // requires user input
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_getRegisterContentJsx: function() {
|
||||||
|
var currStep = this.registerLogic.getStep();
|
||||||
|
var registerStep;
|
||||||
|
switch (currStep) {
|
||||||
|
case "Register.COMPLETE":
|
||||||
|
break; // NOP
|
||||||
|
case "Register.START":
|
||||||
|
case "Register.STEP_m.login.dummy":
|
||||||
|
registerStep = (
|
||||||
|
<RegistrationForm
|
||||||
|
showEmail={true}
|
||||||
|
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||||
|
onError={this.onFormValidationFailed}
|
||||||
|
onRegisterClick={this.onFormSubmit} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "Register.STEP_m.login.email.identity":
|
||||||
|
registerStep = (
|
||||||
|
<div>
|
||||||
|
Please check your email to continue registration.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "Register.STEP_m.login.recaptcha":
|
||||||
|
registerStep = (
|
||||||
|
<CaptchaForm onCaptchaLoaded={this.onCaptchaLoaded} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("Unknown register state: %s", currStep);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var busySpinner;
|
||||||
|
if (this.state.busy) {
|
||||||
|
var Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
busySpinner = (
|
||||||
|
<Spinner />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Create an account</h2>
|
||||||
|
{registerStep}
|
||||||
|
<div className="mx_Login_error">{this.state.errorText}</div>
|
||||||
|
{busySpinner}
|
||||||
|
<ServerConfig ref="serverConfig"
|
||||||
|
withToggleButton={true}
|
||||||
|
defaultHsUrl={this.state.enteredHomeserverUrl}
|
||||||
|
defaultIsUrl={this.state.enteredIdentityServerUrl}
|
||||||
|
onHsUrlChanged={this.onHsUrlChanged}
|
||||||
|
onIsUrlChanged={this.onIsUrlChanged}
|
||||||
|
delayTimeMs={1000} />
|
||||||
|
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
|
||||||
|
I already have an account
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var LoginHeader = sdk.getComponent('login.LoginHeader');
|
||||||
|
return (
|
||||||
|
<div className="mx_Login">
|
||||||
|
<div className="mx_Login_box">
|
||||||
|
<LoginHeader />
|
||||||
|
{this._getRegisterContentJsx()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
112
src/components/views/avatars/MemberAvatar.js
Normal file
112
src/components/views/avatars/MemberAvatar.js
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var Avatar = require('../../../Avatar');
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MemberAvatar',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
member: React.PropTypes.object.isRequired,
|
||||||
|
width: React.PropTypes.number,
|
||||||
|
height: React.PropTypes.number,
|
||||||
|
resizeMethod: React.PropTypes.string,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
resizeMethod: 'crop'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
this.refreshUrl();
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultAvatarUrl: function(member, width, height, resizeMethod) {
|
||||||
|
return Avatar.defaultAvatarUrlForString(member.userId);
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: function(ev) {
|
||||||
|
// don't tightloop if the browser can't load a data url
|
||||||
|
if (ev.target.src == this.defaultAvatarUrl(this.props.member)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
imageUrl: this.defaultAvatarUrl(this.props.member)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_computeUrl: function() {
|
||||||
|
return Avatar.avatarUrlForMember(this.props.member,
|
||||||
|
this.props.width,
|
||||||
|
this.props.height,
|
||||||
|
this.props.resizeMethod);
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshUrl: function() {
|
||||||
|
var newUrl = this._computeUrl();
|
||||||
|
if (newUrl != this.currentUrl) {
|
||||||
|
this.currentUrl = newUrl;
|
||||||
|
this.setState({imageUrl: newUrl});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
imageUrl: this._computeUrl()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
///////////////
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
// XXX: recalculates default avatar url constantly
|
||||||
|
if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) {
|
||||||
|
var initial;
|
||||||
|
if (this.props.member.name[0])
|
||||||
|
initial = this.props.member.name[0].toUpperCase();
|
||||||
|
if (initial === '@' && this.props.member.name[1])
|
||||||
|
initial = this.props.member.name[1].toUpperCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="mx_MemberAvatar" {...this.props}>
|
||||||
|
<span className="mx_MemberAvatar_initial" aria-hidden="true"
|
||||||
|
style={{ fontSize: (this.props.width * 0.65) + "px",
|
||||||
|
width: this.props.width + "px",
|
||||||
|
lineHeight: this.props.height + "px" }}>{ initial }</span>
|
||||||
|
<img className="mx_MemberAvatar_image" src={this.state.imageUrl} title={this.props.member.name}
|
||||||
|
onError={this.onError} width={this.props.width} height={this.props.height} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl}
|
||||||
|
onError={this.onError}
|
||||||
|
width={this.props.width} height={this.props.height}
|
||||||
|
title={this.props.member.name}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -13,18 +13,13 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
var React = require('react');
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var Avatar = require('../../../Avatar');
|
||||||
|
|
||||||
'use strict';
|
module.exports = React.createClass({
|
||||||
|
displayName: 'RoomAvatar',
|
||||||
|
|
||||||
var MatrixClientPeg = require('../../MatrixClientPeg');
|
|
||||||
|
|
||||||
/*
|
|
||||||
* View class should provide:
|
|
||||||
* - getUrlList() returning an array of URLs to try for the room avatar
|
|
||||||
in order of preference from the most preferred at index 0. null entries
|
|
||||||
in the array will be skipped over.
|
|
||||||
*/
|
|
||||||
module.exports = {
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
width: 36,
|
width: 36,
|
||||||
|
@ -41,10 +36,26 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
this._update();
|
this.refreshImageUrl();
|
||||||
this.setState({
|
},
|
||||||
imageUrl: this._nextUrl()
|
|
||||||
});
|
refreshImageUrl: function(nextProps) {
|
||||||
|
// If the list has changed, we start from scratch and re-check, but
|
||||||
|
// don't do so unless the list has changed or we'd re-try fetching
|
||||||
|
// images each time we re-rendered
|
||||||
|
var newList = this.getUrlList();
|
||||||
|
var differs = false;
|
||||||
|
for (var i = 0; i < newList.length && i < this.urlList.length; ++i) {
|
||||||
|
if (this.urlList[i] != newList[i]) differs = true;
|
||||||
|
}
|
||||||
|
if (this.urlList.length != newList.length) differs = true;
|
||||||
|
|
||||||
|
if (differs) {
|
||||||
|
this._update();
|
||||||
|
this.setState({
|
||||||
|
imageUrl: this._nextUrl()
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_update: function() {
|
_update: function() {
|
||||||
|
@ -108,5 +119,53 @@ module.exports = {
|
||||||
this.setState({
|
this.setState({
|
||||||
imageUrl: this._nextUrl()
|
imageUrl: this._nextUrl()
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
////////////
|
||||||
|
|
||||||
|
|
||||||
|
getUrlList: function() {
|
||||||
|
return [
|
||||||
|
this.roomAvatarUrl(),
|
||||||
|
this.getOneToOneAvatar(),
|
||||||
|
this.getFallbackAvatar()
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
getFallbackAvatar: function() {
|
||||||
|
return Avatar.defaultAvatarUrlForString(this.props.room.roomId);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var style = {
|
||||||
|
width: this.props.width,
|
||||||
|
height: this.props.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
// XXX: recalculates fallback avatar constantly
|
||||||
|
if (this.state.imageUrl === this.getFallbackAvatar()) {
|
||||||
|
var initial;
|
||||||
|
if (this.props.room.name[0])
|
||||||
|
initial = this.props.room.name[0].toUpperCase();
|
||||||
|
if ((initial === '@' || initial === '#') && this.props.room.name[1])
|
||||||
|
initial = this.props.room.name[1].toUpperCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span className="mx_RoomAvatar_initial" aria-hidden="true"
|
||||||
|
style={{ fontSize: (this.props.width * 0.65) + "px",
|
||||||
|
width: this.props.width + "px",
|
||||||
|
lineHeight: this.props.height + "px" }}>{ initial }</span>
|
||||||
|
<img className="mx_RoomAvatar" src={this.state.imageUrl}
|
||||||
|
onError={this.onError} style={style} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return <img className="mx_RoomAvatar" src={this.state.imageUrl}
|
||||||
|
onError={this.onError} style={style} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
|
@ -18,7 +18,8 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
|
displayName: 'CreateRoomButton',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onCreateRoom: React.PropTypes.func,
|
onCreateRoom: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
@ -32,4 +33,10 @@ module.exports = {
|
||||||
onClick: function() {
|
onClick: function() {
|
||||||
this.props.onCreateRoom();
|
this.props.onCreateRoom();
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<button className="mx_CreateRoomButton" onClick={this.onClick}>Create Room</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -24,7 +24,8 @@ var Presets = {
|
||||||
Custom: "custom",
|
Custom: "custom",
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
|
displayName: 'CreateRoomPresets',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onChange: React.PropTypes.func,
|
onChange: React.PropTypes.func,
|
||||||
preset: React.PropTypes.string
|
preset: React.PropTypes.string
|
||||||
|
@ -37,4 +38,18 @@ module.exports = {
|
||||||
onChange: function() {},
|
onChange: function() {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
onValueChanged: function(ev) {
|
||||||
|
this.props.onChange(ev.target.value)
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<select className="mx_Presets" onChange={this.onValueChanged} value={this.props.preset}>
|
||||||
|
<option value={this.Presets.PrivateChat}>Private Chat</option>
|
||||||
|
<option value={this.Presets.PublicChat}>Public Chat</option>
|
||||||
|
<option value={this.Presets.Custom}>Custom</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
101
src/components/views/create_room/RoomAlias.js
Normal file
101
src/components/views/create_room/RoomAlias.js
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'RoomAlias',
|
||||||
|
propTypes: {
|
||||||
|
// Specifying a homeserver will make magical things happen when you,
|
||||||
|
// e.g. start typing in the room alias box.
|
||||||
|
homeserver: React.PropTypes.string,
|
||||||
|
alias: React.PropTypes.string,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
onChange: function() {},
|
||||||
|
alias: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getAliasLocalpart: function() {
|
||||||
|
var room_alias = this.props.alias;
|
||||||
|
|
||||||
|
if (room_alias && this.props.homeserver) {
|
||||||
|
var suffix = ":" + this.props.homeserver;
|
||||||
|
if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
|
||||||
|
room_alias = room_alias.slice(1, -suffix.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return room_alias;
|
||||||
|
},
|
||||||
|
|
||||||
|
onValueChanged: function(ev) {
|
||||||
|
this.props.onChange(ev.target.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
onFocus: function(ev) {
|
||||||
|
var target = ev.target;
|
||||||
|
var curr_val = ev.target.value;
|
||||||
|
|
||||||
|
if (this.props.homeserver) {
|
||||||
|
if (curr_val == "") {
|
||||||
|
setTimeout(function() {
|
||||||
|
target.value = "#:" + this.props.homeserver;
|
||||||
|
target.setSelectionRange(1, 1);
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
var suffix = ":" + this.props.homeserver;
|
||||||
|
setTimeout(function() {
|
||||||
|
target.setSelectionRange(
|
||||||
|
curr_val.startsWith("#") ? 1 : 0,
|
||||||
|
curr_val.endsWith(suffix) ? (target.value.length - suffix.length) : target.value.length
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onBlur: function(ev) {
|
||||||
|
var curr_val = ev.target.value;
|
||||||
|
|
||||||
|
if (this.props.homeserver) {
|
||||||
|
if (curr_val == "#:" + this.props.homeserver) {
|
||||||
|
ev.target.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curr_val != "") {
|
||||||
|
var new_val = ev.target.value;
|
||||||
|
var suffix = ":" + this.props.homeserver;
|
||||||
|
if (!curr_val.startsWith("#")) new_val = "#" + new_val;
|
||||||
|
if (!curr_val.endsWith(suffix)) new_val = new_val + suffix;
|
||||||
|
ev.target.value = new_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<input type="text" className="mx_RoomAlias" placeholder="Alias (optional)"
|
||||||
|
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
|
||||||
|
value={this.props.alias}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -14,9 +14,21 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Usage:
|
||||||
|
* Modal.createDialog(ErrorDialog, {
|
||||||
|
* title: "some text", (default: "Error")
|
||||||
|
* description: "some more text",
|
||||||
|
* button: "Button Text",
|
||||||
|
* onClose: someFunction,
|
||||||
|
* focus: true|false (default: true)
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
|
displayName: 'ErrorDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.string,
|
||||||
button: React.PropTypes.string,
|
button: React.PropTypes.string,
|
||||||
|
@ -32,4 +44,22 @@ module.exports = {
|
||||||
focus: true,
|
focus: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div className="mx_ErrorDialog">
|
||||||
|
<div className="mx_ErrorDialogTitle">
|
||||||
|
{this.props.title}
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
{this.props.description}
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button onClick={this.props.onFinished} autoFocus={this.props.focus}>
|
||||||
|
{this.props.button}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
48
src/components/views/dialogs/LogoutPrompt.js
Normal file
48
src/components/views/dialogs/LogoutPrompt.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
var React = require('react');
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'LogoutPrompt',
|
||||||
|
logOut: function() {
|
||||||
|
dis.dispatch({action: 'logout'});
|
||||||
|
if (this.props.onFinished) {
|
||||||
|
this.props.onFinished();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelPrompt: function() {
|
||||||
|
if (this.props.onFinished) {
|
||||||
|
this.props.onFinished();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
Sign out?
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button onClick={this.logOut}>Sign Out</button>
|
||||||
|
<button onClick={this.cancelPrompt}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -16,7 +16,8 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
|
displayName: 'QuestionDialog',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
title: React.PropTypes.string,
|
title: React.PropTypes.string,
|
||||||
description: React.PropTypes.string,
|
description: React.PropTypes.string,
|
||||||
|
@ -33,4 +34,34 @@ module.exports = {
|
||||||
focus: true,
|
focus: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
onOk: function() {
|
||||||
|
this.props.onFinished(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
onCancel: function() {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div className="mx_QuestionDialog">
|
||||||
|
<div className="mx_QuestionDialogTitle">
|
||||||
|
{this.props.title}
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
{this.props.description}
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button onClick={this.onOk} autoFocus={this.props.focus}>
|
||||||
|
{this.props.button}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onClick={this.onCancel}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -18,7 +18,8 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
|
displayName: 'EditableText',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onValueChanged: React.PropTypes.func,
|
onValueChanged: React.PropTypes.func,
|
||||||
initialValue: React.PropTypes.string,
|
initialValue: React.PropTypes.string,
|
||||||
|
@ -85,4 +86,54 @@ module.exports = {
|
||||||
onValueChanged: function(shouldSubmit) {
|
onValueChanged: function(shouldSubmit) {
|
||||||
this.props.onValueChanged(this.state.value, shouldSubmit);
|
this.props.onValueChanged(this.state.value, shouldSubmit);
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
onKeyUp: function(ev) {
|
||||||
|
if (ev.key == "Enter") {
|
||||||
|
this.onFinish(ev);
|
||||||
|
} else if (ev.key == "Escape") {
|
||||||
|
this.cancelEdit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onClickDiv: function() {
|
||||||
|
this.setState({
|
||||||
|
phase: this.Phases.Edit,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onFocus: function(ev) {
|
||||||
|
ev.target.setSelectionRange(0, ev.target.value.length);
|
||||||
|
},
|
||||||
|
|
||||||
|
onFinish: function(ev) {
|
||||||
|
if (ev.target.value) {
|
||||||
|
this.setValue(ev.target.value, ev.key === "Enter");
|
||||||
|
} else {
|
||||||
|
this.cancelEdit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var editable_el;
|
||||||
|
|
||||||
|
if (this.state.phase == this.Phases.Display) {
|
||||||
|
if (this.state.value) {
|
||||||
|
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.state.value}</div>;
|
||||||
|
} else {
|
||||||
|
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.props.label}</div>;
|
||||||
|
}
|
||||||
|
} else if (this.state.phase == this.Phases.Edit) {
|
||||||
|
editable_el = (
|
||||||
|
<div>
|
||||||
|
<input type="text" defaultValue={this.state.value} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onFinish} placeholder={this.props.placeHolder} autoFocus/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_EditableText">
|
||||||
|
{editable_el}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
38
src/components/views/elements/ProgressBar.js
Normal file
38
src/components/views/elements/ProgressBar.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'ProgressBar',
|
||||||
|
propTypes: {
|
||||||
|
value: React.PropTypes.number,
|
||||||
|
max: React.PropTypes.number
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
// Would use an HTML5 progress tag but if that doesn't animate if you
|
||||||
|
// use the HTML attributes rather than styles
|
||||||
|
var progressStyle = {
|
||||||
|
width: ((this.props.value / this.props.max) * 100)+"%"
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="mx_ProgressBar"><div className="mx_ProgressBar_fill" style={progressStyle}></div></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -18,7 +18,9 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
|
displayName: 'UserSelector',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onChange: React.PropTypes.func,
|
onChange: React.PropTypes.func,
|
||||||
selected_users: React.PropTypes.arrayOf(React.PropTypes.string),
|
selected_users: React.PropTypes.arrayOf(React.PropTypes.string),
|
||||||
|
@ -42,4 +44,26 @@ module.exports = {
|
||||||
return e != user_id;
|
return e != user_id;
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
onAddUserId: function() {
|
||||||
|
this.addUser(this.refs.user_id_input.value);
|
||||||
|
this.refs.user_id_input.value = "";
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var self = this;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ul className="mx_UserSelector_UserIdList" ref="list">
|
||||||
|
{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>
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<input type="text" ref="user_id_input" defaultValue="" className="mx_UserSelector_userIdInput" placeholder="ex. @bob:example.com"/>
|
||||||
|
<button onClick={this.onAddUserId} className="mx_UserSelector_AddUserId">
|
||||||
|
Add User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
67
src/components/views/login/CaptchaForm.js
Normal file
67
src/components/views/login/CaptchaForm.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var DIV_ID = 'mx_recaptcha';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pure UI component which displays a captcha form.
|
||||||
|
*/
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'CaptchaForm',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
onCaptchaLoaded: React.PropTypes.func.isRequired // called with div id name
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
onCaptchaLoaded: function() {
|
||||||
|
console.error("Unhandled onCaptchaLoaded");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
|
||||||
|
// so we do this instead.
|
||||||
|
var self = this;
|
||||||
|
if (this.refs.recaptchaContainer) {
|
||||||
|
console.log("Loading recaptcha script...");
|
||||||
|
var scriptTag = document.createElement('script');
|
||||||
|
window.mx_on_recaptcha_loaded = function() {
|
||||||
|
console.log("Loaded recaptcha script.");
|
||||||
|
self.props.onCaptchaLoaded(DIV_ID);
|
||||||
|
};
|
||||||
|
scriptTag.setAttribute(
|
||||||
|
'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit"
|
||||||
|
);
|
||||||
|
this.refs.recaptchaContainer.appendChild(scriptTag);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
// FIXME: Tight coupling with the div id and SignupStages.js
|
||||||
|
return (
|
||||||
|
<div ref="recaptchaContainer">
|
||||||
|
This Home Server would like to make sure you are not a robot
|
||||||
|
<div id={DIV_ID}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -16,10 +16,12 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
var React = require('react');
|
||||||
var url = require("url");
|
var url = require("url");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
|
displayName: 'CasLogin',
|
||||||
|
|
||||||
onCasClicked: function(ev) {
|
onCasClicked: function(ev) {
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
|
@ -30,4 +32,12 @@ module.exports = {
|
||||||
window.location.href = casUrl;
|
window.location.href = casUrl;
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={this.onCasClicked}>Sign in with CAS</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
50
src/components/views/login/CustomServerDialog.js
Normal file
50
src/components/views/login/CustomServerDialog.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var React = require("react");
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'CustomServerDialog',
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div className="mx_ErrorDialog">
|
||||||
|
<div className="mx_ErrorDialogTitle">
|
||||||
|
Custom Server Options
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
<span>
|
||||||
|
You can use the custom server options to log into other Matrix
|
||||||
|
servers by specifying a different Home server URL.
|
||||||
|
<br/>
|
||||||
|
This allows you to use this app with an existing Matrix account on
|
||||||
|
a different Home server.
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
You can also set a custom Identity server but this will affect
|
||||||
|
people's ability to find you if you use a server in a group other
|
||||||
|
than the main Matrix.org group.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button onClick={this.props.onFinished} autoFocus={true}>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -16,15 +16,16 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var linkify = require('linkifyjs');
|
var React = require('react');
|
||||||
var linkifyElement = require('linkifyjs/element');
|
|
||||||
var linkifyMatrix = require('../../linkify-matrix');
|
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
module.exports = React.createClass({
|
||||||
|
displayName: 'LoginFooter',
|
||||||
|
|
||||||
module.exports = {
|
render: function() {
|
||||||
componentDidMount: function() {
|
return (
|
||||||
linkifyElement(this.refs.content, linkifyMatrix.options);
|
<div className="mx_Login_links">
|
||||||
|
<a href="https://matrix.org">powered by Matrix</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
|
@ -14,16 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
propTypes: {
|
displayName: 'LoginHeader',
|
||||||
room: React.PropTypes.object.isRequired,
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
render: function() {
|
||||||
return {
|
return (
|
||||||
power_levels_changed: false
|
<div className="mx_Login_logo">
|
||||||
};
|
Matrix
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
});
|
65
src/components/views/login/PasswordLogin.js
Normal file
65
src/components/views/login/PasswordLogin.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ReactDOM = require('react-dom');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pure UI component which displays a username/password form.
|
||||||
|
*/
|
||||||
|
module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
|
propTypes: {
|
||||||
|
onSubmit: React.PropTypes.func.isRequired // fn(username, password)
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
username: "",
|
||||||
|
password: ""
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onSubmitForm: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.props.onSubmit(this.state.username, this.state.password);
|
||||||
|
},
|
||||||
|
|
||||||
|
onUsernameChanged: function(ev) {
|
||||||
|
this.setState({username: ev.target.value});
|
||||||
|
},
|
||||||
|
|
||||||
|
onPasswordChanged: function(ev) {
|
||||||
|
this.setState({password: ev.target.value});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={this.onSubmitForm}>
|
||||||
|
<input className="mx_Login_field" ref="user" type="text"
|
||||||
|
value={this.state.username} onChange={this.onUsernameChanged}
|
||||||
|
placeholder="Email or user name" autoFocus />
|
||||||
|
<br />
|
||||||
|
<input className="mx_Login_field" ref="pass" type="password"
|
||||||
|
value={this.state.password} onChange={this.onPasswordChanged}
|
||||||
|
placeholder="Password" />
|
||||||
|
<br />
|
||||||
|
<input className="mx_Login_submit" type="submit" value="Log in" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
126
src/components/views/login/RegistrationForm.js
Normal file
126
src/components/views/login/RegistrationForm.js
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pure UI component which displays a registration form.
|
||||||
|
*/
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'RegistrationForm',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
defaultEmail: React.PropTypes.string,
|
||||||
|
defaultUsername: React.PropTypes.string,
|
||||||
|
showEmail: React.PropTypes.bool,
|
||||||
|
minPasswordLength: React.PropTypes.number,
|
||||||
|
onError: React.PropTypes.func,
|
||||||
|
onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
showEmail: false,
|
||||||
|
minPasswordLength: 6,
|
||||||
|
onError: function(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
email: this.props.defaultEmail,
|
||||||
|
username: this.props.defaultUsername,
|
||||||
|
password: null,
|
||||||
|
passwordConfirm: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onSubmit: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
var pwd1 = this.refs.password.value.trim();
|
||||||
|
var pwd2 = this.refs.passwordConfirm.value.trim()
|
||||||
|
|
||||||
|
var errCode;
|
||||||
|
if (!pwd1 || !pwd2) {
|
||||||
|
errCode = "RegistrationForm.ERR_PASSWORD_MISSING";
|
||||||
|
}
|
||||||
|
else if (pwd1 !== pwd2) {
|
||||||
|
errCode = "RegistrationForm.ERR_PASSWORD_MISMATCH";
|
||||||
|
}
|
||||||
|
else if (pwd1.length < this.props.minPasswordLength) {
|
||||||
|
errCode = "RegistrationForm.ERR_PASSWORD_LENGTH";
|
||||||
|
}
|
||||||
|
if (errCode) {
|
||||||
|
this.props.onError(errCode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var promise = this.props.onRegisterClick({
|
||||||
|
username: this.refs.username.value.trim(),
|
||||||
|
password: pwd1,
|
||||||
|
email: this.refs.email.value.trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (promise) {
|
||||||
|
ev.target.disabled = true;
|
||||||
|
promise.finally(function() {
|
||||||
|
ev.target.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var emailSection, registerButton;
|
||||||
|
if (this.props.showEmail) {
|
||||||
|
emailSection = (
|
||||||
|
<input className="mx_Login_field" type="text" ref="email"
|
||||||
|
autoFocus={true} placeholder="Email address"
|
||||||
|
defaultValue={this.state.email} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.props.onRegisterClick) {
|
||||||
|
registerButton = (
|
||||||
|
<input className="mx_Login_submit" type="submit" value="Register" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={this.onSubmit}>
|
||||||
|
{emailSection}
|
||||||
|
<br />
|
||||||
|
<input className="mx_Login_field" type="text" ref="username"
|
||||||
|
placeholder="User name" defaultValue={this.state.username} />
|
||||||
|
<br />
|
||||||
|
<input className="mx_Login_field" type="password" ref="password"
|
||||||
|
placeholder="Password" defaultValue={this.state.password} />
|
||||||
|
<br />
|
||||||
|
<input className="mx_Login_field" type="password" ref="passwordConfirm"
|
||||||
|
placeholder="Confirm password"
|
||||||
|
defaultValue={this.state.passwordConfirm} />
|
||||||
|
<br />
|
||||||
|
{registerButton}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
145
src/components/views/login/ServerConfig.js
Normal file
145
src/components/views/login/ServerConfig.js
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var Modal = require('../../../Modal');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pure UI component which displays the HS and IS to use.
|
||||||
|
*/
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'ServerConfig',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
onHsUrlChanged: React.PropTypes.func,
|
||||||
|
onIsUrlChanged: React.PropTypes.func,
|
||||||
|
defaultHsUrl: React.PropTypes.string,
|
||||||
|
defaultIsUrl: React.PropTypes.string,
|
||||||
|
withToggleButton: React.PropTypes.bool,
|
||||||
|
delayTimeMs: React.PropTypes.number // time to wait before invoking onChanged
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
onHsUrlChanged: function() {},
|
||||||
|
onIsUrlChanged: function() {},
|
||||||
|
withToggleButton: false,
|
||||||
|
delayTimeMs: 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
hs_url: this.props.defaultHsUrl,
|
||||||
|
is_url: this.props.defaultIsUrl,
|
||||||
|
original_hs_url: this.props.defaultHsUrl,
|
||||||
|
original_is_url: this.props.defaultIsUrl,
|
||||||
|
// no toggle button = show, toggle button = hide
|
||||||
|
configVisible: !this.props.withToggleButton
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onHomeserverChanged: function(ev) {
|
||||||
|
this.setState({hs_url: ev.target.value}, function() {
|
||||||
|
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() {
|
||||||
|
this.props.onHsUrlChanged(this.state.hs_url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onIdentityServerChanged: function(ev) {
|
||||||
|
this.setState({is_url: ev.target.value}, function() {
|
||||||
|
this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() {
|
||||||
|
this.props.onIsUrlChanged(this.state.is_url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_waitThenInvoke: function(existingTimeoutId, fn) {
|
||||||
|
if (existingTimeoutId) {
|
||||||
|
clearTimeout(existingTimeoutId);
|
||||||
|
}
|
||||||
|
return setTimeout(fn.bind(this), this.props.delayTimeMs);
|
||||||
|
},
|
||||||
|
|
||||||
|
getHsUrl: function() {
|
||||||
|
return this.state.hs_url;
|
||||||
|
},
|
||||||
|
|
||||||
|
getIsUrl: function() {
|
||||||
|
return this.state.is_url;
|
||||||
|
},
|
||||||
|
|
||||||
|
onServerConfigVisibleChange: function(ev) {
|
||||||
|
this.setState({
|
||||||
|
configVisible: ev.target.checked
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showHelpPopup: function() {
|
||||||
|
var CustomServerDialog = sdk.getComponent('login.CustomServerDialog');
|
||||||
|
Modal.createDialog(CustomServerDialog);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var serverConfigStyle = {};
|
||||||
|
serverConfigStyle.display = this.state.configVisible ? 'block' : 'none';
|
||||||
|
|
||||||
|
var toggleButton;
|
||||||
|
if (this.props.withToggleButton) {
|
||||||
|
toggleButton = (
|
||||||
|
<div>
|
||||||
|
<input className="mx_Login_checkbox" id="advanced" type="checkbox"
|
||||||
|
checked={this.state.configVisible}
|
||||||
|
onChange={this.onServerConfigVisibleChange} />
|
||||||
|
<label className="mx_Login_label" htmlFor="advanced">
|
||||||
|
Use custom server options (advanced)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{toggleButton}
|
||||||
|
<div style={serverConfigStyle}>
|
||||||
|
<div className="mx_ServerConfig">
|
||||||
|
<label className="mx_Login_label mx_ServerConfig_hslabel" htmlFor="hsurl">
|
||||||
|
Home server URL
|
||||||
|
</label>
|
||||||
|
<input className="mx_Login_field" id="hsurl" type="text"
|
||||||
|
placeholder={this.state.original_hs_url}
|
||||||
|
value={this.state.hs_url}
|
||||||
|
onChange={this.onHomeserverChanged} />
|
||||||
|
<label className="mx_Login_label mx_ServerConfig_islabel" htmlFor="isurl">
|
||||||
|
Identity server URL
|
||||||
|
</label>
|
||||||
|
<input className="mx_Login_field" id="isurl" type="text"
|
||||||
|
placeholder={this.state.original_is_url}
|
||||||
|
value={this.state.is_url}
|
||||||
|
onChange={this.onIdentityServerChanged} />
|
||||||
|
<a className="mx_ServerConfig_help" href="#" onClick={this.showHelpPopup}>
|
||||||
|
What does this mean?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -16,9 +16,13 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
var filesize = require('filesize');
|
var filesize = require('filesize');
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MFileBody',
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
presentableTextForFile: function(content) {
|
presentableTextForFile: function(content) {
|
||||||
var linkText = 'Attachment';
|
var linkText = 'Attachment';
|
||||||
if (content.body && content.body.length > 0) {
|
if (content.body && content.body.length > 0) {
|
||||||
|
@ -39,6 +43,31 @@ module.exports = {
|
||||||
linkText += ' (' + additionals.join(', ') + ')';
|
linkText += ' (' + additionals.join(', ') + ')';
|
||||||
}
|
}
|
||||||
return linkText;
|
return linkText;
|
||||||
}
|
},
|
||||||
};
|
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var content = this.props.mxEvent.getContent();
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
var httpUrl = cli.mxcUrlToHttp(content.url);
|
||||||
|
var text = this.presentableTextForFile(content);
|
||||||
|
|
||||||
|
if (httpUrl) {
|
||||||
|
return (
|
||||||
|
<span className="mx_MFileTile">
|
||||||
|
<div className="mx_MImageTile_download">
|
||||||
|
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
||||||
|
<img src="img/download.png" width="10" height="12"/>
|
||||||
|
Download {text}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var extra = text ? ': '+text : '';
|
||||||
|
return <span className="mx_MFileTile">
|
||||||
|
Invalid file{extra}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
141
src/components/views/messages/MImageBody.js
Normal file
141
src/components/views/messages/MImageBody.js
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var filesize = require('filesize');
|
||||||
|
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var Modal = require('../../../Modal');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MImageBody',
|
||||||
|
|
||||||
|
thumbHeight: function(fullWidth, fullHeight, thumbWidth, thumbHeight) {
|
||||||
|
if (!fullWidth || !fullHeight) {
|
||||||
|
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
||||||
|
// log this because it's spammy
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
|
||||||
|
// no scaling needs to be applied
|
||||||
|
return fullHeight;
|
||||||
|
}
|
||||||
|
var widthMulti = thumbWidth / fullWidth;
|
||||||
|
var heightMulti = thumbHeight / fullHeight;
|
||||||
|
if (widthMulti < heightMulti) {
|
||||||
|
// width is the dominant dimension so scaling will be fixed on that
|
||||||
|
return Math.floor(widthMulti * fullHeight);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// height is the dominant dimension so scaling will be fixed on that
|
||||||
|
return Math.floor(heightMulti * fullHeight);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick: function onClick(ev) {
|
||||||
|
if (ev.button == 0 && !ev.metaKey) {
|
||||||
|
ev.preventDefault();
|
||||||
|
var content = this.props.mxEvent.getContent();
|
||||||
|
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(content.url);
|
||||||
|
var ImageView = sdk.getComponent("elements.ImageView");
|
||||||
|
var params = {
|
||||||
|
src: httpUrl,
|
||||||
|
mxEvent: this.props.mxEvent
|
||||||
|
};
|
||||||
|
|
||||||
|
if (content.info) {
|
||||||
|
params.width = content.info.w;
|
||||||
|
params.height = content.info.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_isGif: function() {
|
||||||
|
var content = this.props.mxEvent.getContent();
|
||||||
|
return (content && content.info && content.info.mimetype === "image/gif");
|
||||||
|
},
|
||||||
|
|
||||||
|
onImageEnter: function(e) {
|
||||||
|
if (!this._isGif()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var imgElement = e.target;
|
||||||
|
imgElement.src = MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
|
this.props.mxEvent.getContent().url
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
onImageLeave: function(e) {
|
||||||
|
if (!this._isGif()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var imgElement = e.target;
|
||||||
|
imgElement.src = this._getThumbUrl();
|
||||||
|
},
|
||||||
|
|
||||||
|
_getThumbUrl: function() {
|
||||||
|
var content = this.props.mxEvent.getContent();
|
||||||
|
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 480, 360);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var content = this.props.mxEvent.getContent();
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
var thumbHeight = null;
|
||||||
|
if (content.info) thumbHeight = this.thumbHeight(content.info.w, content.info.h, 480, 360);
|
||||||
|
|
||||||
|
var imgStyle = {};
|
||||||
|
if (thumbHeight) imgStyle['height'] = thumbHeight;
|
||||||
|
|
||||||
|
var thumbUrl = this._getThumbUrl();
|
||||||
|
if (thumbUrl) {
|
||||||
|
return (
|
||||||
|
<span className="mx_MImageTile">
|
||||||
|
<a href={cli.mxcUrlToHttp(content.url)} onClick={ this.onClick }>
|
||||||
|
<img className="mx_MImageTile_thumbnail" src={thumbUrl}
|
||||||
|
alt={content.body} style={imgStyle}
|
||||||
|
onMouseEnter={this.onImageEnter}
|
||||||
|
onMouseLeave={this.onImageLeave} />
|
||||||
|
</a>
|
||||||
|
<div className="mx_MImageTile_download">
|
||||||
|
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
||||||
|
<img src="img/download.png" width="10" height="12"/>
|
||||||
|
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (content.body) {
|
||||||
|
return (
|
||||||
|
<span className="mx_MImageTile">
|
||||||
|
Image '{content.body}' cannot be displayed.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<span className="mx_MImageTile">
|
||||||
|
This image cannot be displayed.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
81
src/components/views/messages/MVideoBody.js
Normal file
81
src/components/views/messages/MVideoBody.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var filesize = require('filesize');
|
||||||
|
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var Modal = require('../../../Modal');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MVideoBody',
|
||||||
|
|
||||||
|
thumbScale: function(fullWidth, fullHeight, thumbWidth, thumbHeight) {
|
||||||
|
if (!fullWidth || !fullHeight) {
|
||||||
|
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
||||||
|
// log this because it's spammy
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
|
||||||
|
// no scaling needs to be applied
|
||||||
|
return fullHeight;
|
||||||
|
}
|
||||||
|
var widthMulti = thumbWidth / fullWidth;
|
||||||
|
var heightMulti = thumbHeight / fullHeight;
|
||||||
|
if (widthMulti < heightMulti) {
|
||||||
|
// width is the dominant dimension so scaling will be fixed on that
|
||||||
|
return widthMulti;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// height is the dominant dimension so scaling will be fixed on that
|
||||||
|
return heightMulti;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var content = this.props.mxEvent.getContent();
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
var height = null;
|
||||||
|
var width = null;
|
||||||
|
var poster = null;
|
||||||
|
var preload = "metadata";
|
||||||
|
if (content.info) {
|
||||||
|
var scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
|
||||||
|
if (scale) {
|
||||||
|
width = Math.floor(content.info.w * scale);
|
||||||
|
height = Math.floor(content.info.h * scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.info.thumbnail_url) {
|
||||||
|
poster = cli.mxcUrlToHttp(content.info.thumbnail_url);
|
||||||
|
preload = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="mx_MVideoTile">
|
||||||
|
<video className="mx_MVideoTile" src={cli.mxcUrlToHttp(content.url)} alt={content.body}
|
||||||
|
controls preload={preload} autoPlay="0"
|
||||||
|
height={height} width={width} poster={poster}>
|
||||||
|
</video>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
52
src/components/views/messages/MessageEvent.js
Normal file
52
src/components/views/messages/MessageEvent.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MessageEvent',
|
||||||
|
|
||||||
|
statics: {
|
||||||
|
needsSenderProfile: function() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var UnknownMessageTile = sdk.getComponent('messages.UnknownBody');
|
||||||
|
|
||||||
|
var tileTypes = {
|
||||||
|
'm.text': sdk.getComponent('messages.TextualBody'),
|
||||||
|
'm.notice': sdk.getComponent('messages.TextualBody'),
|
||||||
|
'm.emote': sdk.getComponent('messages.TextualBody'),
|
||||||
|
'm.image': sdk.getComponent('messages.MImageBody'),
|
||||||
|
'm.file': sdk.getComponent('messages.MFileBody'),
|
||||||
|
'm.video': sdk.getComponent('messages.MVideoBody')
|
||||||
|
};
|
||||||
|
|
||||||
|
var content = this.props.mxEvent.getContent();
|
||||||
|
var msgtype = content.msgtype;
|
||||||
|
var TileType = UnknownMessageTile;
|
||||||
|
if (msgtype && tileTypes[msgtype]) {
|
||||||
|
TileType = tileTypes[msgtype];
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TileType mxEvent={this.props.mxEvent} highlights={this.props.highlights} />;
|
||||||
|
},
|
||||||
|
});
|
77
src/components/views/messages/TextualBody.js
Normal file
77
src/components/views/messages/TextualBody.js
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ReactDOM = require('react-dom');
|
||||||
|
var HtmlUtils = require('../../../HtmlUtils');
|
||||||
|
var linkify = require('linkifyjs');
|
||||||
|
var linkifyElement = require('linkifyjs/element');
|
||||||
|
var linkifyMatrix = require('../../../linkify-matrix');
|
||||||
|
|
||||||
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'TextualBody',
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
linkifyElement(this.refs.content, linkifyMatrix.options);
|
||||||
|
|
||||||
|
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
|
||||||
|
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidUpdate: function() {
|
||||||
|
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
|
||||||
|
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldComponentUpdate: function(nextProps) {
|
||||||
|
// exploit that events are immutable :)
|
||||||
|
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
|
||||||
|
nextProps.highlights !== this.props.highlights);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var mxEvent = this.props.mxEvent;
|
||||||
|
var content = mxEvent.getContent();
|
||||||
|
var body = HtmlUtils.bodyToHtml(content, this.props.highlights);
|
||||||
|
|
||||||
|
switch (content.msgtype) {
|
||||||
|
case "m.emote":
|
||||||
|
var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||||
|
return (
|
||||||
|
<span ref="content" className="mx_MEmoteTile mx_MessageTile_content">
|
||||||
|
* { name } { body }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
case "m.notice":
|
||||||
|
return (
|
||||||
|
<span ref="content" className="mx_MNoticeTile mx_MessageTile_content">
|
||||||
|
{ body }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
default: // including "m.text"
|
||||||
|
return (
|
||||||
|
<span ref="content" className="mx_MTextTile mx_MessageTile_content">
|
||||||
|
{ body }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -18,24 +18,26 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
|
||||||
module.exports = {
|
var TextForEvent = require('../../../TextForEvent');
|
||||||
propTypes: {
|
|
||||||
default_name: React.PropTypes.string
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
module.exports = React.createClass({
|
||||||
return {
|
displayName: 'TextualEvent',
|
||||||
default_name: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
statics: {
|
||||||
return {
|
needsSenderProfile: function() {
|
||||||
room_name: this.props.default_name,
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getName: function() {
|
render: function() {
|
||||||
return this.state.room_name;
|
var text = TextForEvent.textForEvent(this.props.mxEvent);
|
||||||
|
if (text == null || text.length == 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_EventAsTextTile">
|
||||||
|
{TextForEvent.textForEvent(this.props.mxEvent)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
|
@ -16,12 +16,17 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var dis = require("../../dispatcher");
|
var React = require('react');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
onClick: function() {
|
displayName: 'UnknownBody',
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_user_settings'
|
render: function() {
|
||||||
});
|
var content = this.props.mxEvent.getContent();
|
||||||
|
return (
|
||||||
|
<span className="mx_UnknownMessageTile">
|
||||||
|
{content.body}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
});
|
287
src/components/views/rooms/EventTile.js
Normal file
287
src/components/views/rooms/EventTile.js
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ReactDom = require('react-dom');
|
||||||
|
var classNames = require("classnames");
|
||||||
|
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg')
|
||||||
|
var TextForEvent = require('../../../TextForEvent');
|
||||||
|
|
||||||
|
var ContextualMenu = require('../../../ContextualMenu');
|
||||||
|
var Velociraptor = require('../../../Velociraptor');
|
||||||
|
require('../../../VelocityBounce');
|
||||||
|
|
||||||
|
var bounce = false;
|
||||||
|
try {
|
||||||
|
if (global.localStorage) {
|
||||||
|
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventTileTypes = {
|
||||||
|
'm.room.message': 'messages.MessageEvent',
|
||||||
|
'm.room.member' : 'messages.TextualEvent',
|
||||||
|
'm.call.invite' : 'messages.TextualEvent',
|
||||||
|
'm.call.answer' : 'messages.TextualEvent',
|
||||||
|
'm.call.hangup' : 'messages.TextualEvent',
|
||||||
|
'm.room.name' : 'messages.TextualEvent',
|
||||||
|
'm.room.topic' : 'messages.TextualEvent',
|
||||||
|
};
|
||||||
|
|
||||||
|
var MAX_READ_AVATARS = 5;
|
||||||
|
|
||||||
|
// Our component structure for EventTiles on the timeline is:
|
||||||
|
//
|
||||||
|
// .-EventTile------------------------------------------------.
|
||||||
|
// | MemberAvatar (SenderProfile) TimeStamp |
|
||||||
|
// | .-{Message,Textual}Event---------------. Read Avatars |
|
||||||
|
// | | .-MFooBody-------------------. | |
|
||||||
|
// | | | (only if MessageEvent) | | |
|
||||||
|
// | | '----------------------------' | |
|
||||||
|
// | '--------------------------------------' |
|
||||||
|
// '----------------------------------------------------------'
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'Event',
|
||||||
|
|
||||||
|
statics: {
|
||||||
|
haveTileForEvent: function(e) {
|
||||||
|
if (eventTileTypes[e.getType()] == undefined) return false;
|
||||||
|
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
|
||||||
|
return TextForEvent.textForEvent(e) !== '';
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {menu: false, allReadAvatars: false};
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldHighlight: function() {
|
||||||
|
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
|
||||||
|
if (!actions || !actions.tweaks) { return false; }
|
||||||
|
return actions.tweaks.highlight;
|
||||||
|
},
|
||||||
|
|
||||||
|
onEditClicked: function(e) {
|
||||||
|
var MessageContextMenu = sdk.getComponent('rooms.MessageContextMenu');
|
||||||
|
var buttonRect = e.target.getBoundingClientRect()
|
||||||
|
var x = buttonRect.right;
|
||||||
|
var y = buttonRect.top + (e.target.height / 2);
|
||||||
|
var self = this;
|
||||||
|
ContextualMenu.createMenu(MessageContextMenu, {
|
||||||
|
mxEvent: this.props.mxEvent,
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
onFinished: function() {
|
||||||
|
self.setState({menu: false});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.setState({menu: true});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleAllReadAvatars: function() {
|
||||||
|
this.setState({
|
||||||
|
allReadAvatars: !this.state.allReadAvatars
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getReadAvatars: function() {
|
||||||
|
var avatars = [];
|
||||||
|
|
||||||
|
var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||||
|
|
||||||
|
if (!room) return [];
|
||||||
|
|
||||||
|
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
|
||||||
|
// get list of read receipts, sorted most recent first
|
||||||
|
var receipts = room.getReceiptsForEvent(this.props.mxEvent).filter(function(r) {
|
||||||
|
return r.type === "m.read" && r.userId != myUserId;
|
||||||
|
}).sort(function(r1, r2) {
|
||||||
|
return r2.data.ts - r1.data.ts;
|
||||||
|
});
|
||||||
|
|
||||||
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
|
||||||
|
var left = 0;
|
||||||
|
|
||||||
|
var reorderTransitionOpts = {
|
||||||
|
duration: 100,
|
||||||
|
easing: 'easeOut'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = 0; i < receipts.length; ++i) {
|
||||||
|
var member = room.getMember(receipts[i].userId);
|
||||||
|
|
||||||
|
// Using react refs here would mean both getting Velociraptor to expose
|
||||||
|
// them and making them scoped to the whole RoomView. Not impossible, but
|
||||||
|
// getElementById seems simpler at least for a first cut.
|
||||||
|
var oldAvatarDomNode = document.getElementById('mx_readAvatar'+member.userId);
|
||||||
|
var startStyles = [];
|
||||||
|
var enterTransitionOpts = [];
|
||||||
|
var oldNodeTop = -15; // For avatars that weren't on screen, act as if they were just off the top
|
||||||
|
if (oldAvatarDomNode) {
|
||||||
|
oldNodeTop = oldAvatarDomNode.getBoundingClientRect().top;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.readAvatarNode) {
|
||||||
|
var topOffset = oldNodeTop - this.readAvatarNode.getBoundingClientRect().top;
|
||||||
|
|
||||||
|
if (oldAvatarDomNode && oldAvatarDomNode.style.left !== '0px') {
|
||||||
|
var leftOffset = oldAvatarDomNode.style.left;
|
||||||
|
// start at the old height and in the old h pos
|
||||||
|
startStyles.push({ top: topOffset, left: leftOffset });
|
||||||
|
enterTransitionOpts.push(reorderTransitionOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// then shift to the rightmost column,
|
||||||
|
// and then it will drop down to its resting position
|
||||||
|
startStyles.push({ top: topOffset, left: '0px' });
|
||||||
|
enterTransitionOpts.push({
|
||||||
|
duration: bounce ? Math.min(Math.log(Math.abs(topOffset)) * 200, 3000) : 300,
|
||||||
|
easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var style = {
|
||||||
|
left: left+'px',
|
||||||
|
top: '0px',
|
||||||
|
visibility: ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) ? 'visible' : 'hidden'
|
||||||
|
};
|
||||||
|
|
||||||
|
//console.log("i = " + i + ", MAX_READ_AVATARS = " + MAX_READ_AVATARS + ", allReadAvatars = " + this.state.allReadAvatars + " visibility = " + style.visibility);
|
||||||
|
|
||||||
|
// add to the start so the most recent is on the end (ie. ends up rightmost)
|
||||||
|
avatars.unshift(
|
||||||
|
<MemberAvatar key={member.userId} member={member}
|
||||||
|
width={14} height={14} resizeMethod="crop"
|
||||||
|
style={style}
|
||||||
|
startStyle={startStyles}
|
||||||
|
enterTransitionOpts={enterTransitionOpts}
|
||||||
|
id={'mx_readAvatar'+member.userId}
|
||||||
|
onClick={this.toggleAllReadAvatars}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
// TODO: we keep the extra read avatars in the dom to make animation simpler
|
||||||
|
// we could optimise this to reduce the dom size.
|
||||||
|
if (i < MAX_READ_AVATARS - 1 || this.state.allReadAvatars) { // XXX: where does this -1 come from? is it to make the max'th avatar animate properly?
|
||||||
|
left -= 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var editButton;
|
||||||
|
if (!this.state.allReadAvatars) {
|
||||||
|
var remainder = receipts.length - MAX_READ_AVATARS;
|
||||||
|
var remText;
|
||||||
|
if (i >= MAX_READ_AVATARS - 1) left -= 15;
|
||||||
|
if (remainder > 0) {
|
||||||
|
remText = <span className="mx_EventTile_readAvatarRemainder"
|
||||||
|
onClick={this.toggleAllReadAvatars}
|
||||||
|
style={{ left: left }}>{ remainder }+
|
||||||
|
</span>;
|
||||||
|
left -= 15;
|
||||||
|
}
|
||||||
|
editButton = (
|
||||||
|
<input style={{ left: left }}
|
||||||
|
type="image" src="img/edit.png" alt="Options" title="Options" width="14" height="14"
|
||||||
|
className="mx_EventTile_editButton" onClick={this.onEditClicked} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span className="mx_EventTile_readAvatars" ref={this.collectReadAvatarNode}>
|
||||||
|
{ editButton }
|
||||||
|
{ remText }
|
||||||
|
<Velociraptor transition={ reorderTransitionOpts }>
|
||||||
|
{ avatars }
|
||||||
|
</Velociraptor>
|
||||||
|
</span>;
|
||||||
|
},
|
||||||
|
|
||||||
|
collectReadAvatarNode: function(node) {
|
||||||
|
this.readAvatarNode = ReactDom.findDOMNode(node);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
||||||
|
var SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||||
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
|
||||||
|
var content = this.props.mxEvent.getContent();
|
||||||
|
var msgtype = content.msgtype;
|
||||||
|
|
||||||
|
var EventTileType = sdk.getComponent(eventTileTypes[this.props.mxEvent.getType()]);
|
||||||
|
// This shouldn't happen: the caller should check we support this type
|
||||||
|
// before trying to instantiate us
|
||||||
|
if (!EventTileType) {
|
||||||
|
throw new Error("Event type not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
var classes = classNames({
|
||||||
|
mx_EventTile: true,
|
||||||
|
mx_EventTile_sending: ['sending', 'queued'].indexOf(
|
||||||
|
this.props.mxEvent.status
|
||||||
|
) !== -1,
|
||||||
|
mx_EventTile_notSent: this.props.mxEvent.status == 'not_sent',
|
||||||
|
mx_EventTile_highlight: this.shouldHighlight(),
|
||||||
|
mx_EventTile_continuation: this.props.continuation,
|
||||||
|
mx_EventTile_last: this.props.last,
|
||||||
|
mx_EventTile_contextual: this.props.contextual,
|
||||||
|
menu: this.state.menu,
|
||||||
|
});
|
||||||
|
var timestamp = <MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
||||||
|
|
||||||
|
var aux = null;
|
||||||
|
if (msgtype === 'm.image') aux = "sent an image";
|
||||||
|
else if (msgtype === 'm.video') aux = "sent a video";
|
||||||
|
else if (msgtype === 'm.file') aux = "uploaded a file";
|
||||||
|
|
||||||
|
var readAvatars = this.getReadAvatars();
|
||||||
|
|
||||||
|
var avatar, sender;
|
||||||
|
if (!this.props.continuation) {
|
||||||
|
if (this.props.mxEvent.sender) {
|
||||||
|
avatar = (
|
||||||
|
<div className="mx_EventTile_avatar">
|
||||||
|
<MemberAvatar member={this.props.mxEvent.sender} width={24} height={24} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (EventTileType.needsSenderProfile()) {
|
||||||
|
sender = <SenderProfile mxEvent={this.props.mxEvent} aux={aux} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={classes}>
|
||||||
|
<div className="mx_EventTile_msgOption">
|
||||||
|
{ timestamp }
|
||||||
|
{ readAvatars }
|
||||||
|
</div>
|
||||||
|
{ avatar }
|
||||||
|
{ sender }
|
||||||
|
<div className="mx_EventTile_line">
|
||||||
|
<EventTileType mxEvent={this.props.mxEvent} highlights={this.props.highlights} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -25,23 +25,36 @@ limitations under the License.
|
||||||
* 'muted': boolean,
|
* 'muted': boolean,
|
||||||
* 'isTargetMod': boolean
|
* 'isTargetMod': boolean
|
||||||
*/
|
*/
|
||||||
|
var React = require('react');
|
||||||
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
var Modal = require("../../../Modal");
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
module.exports = React.createClass({
|
||||||
var dis = require("../../dispatcher");
|
displayName: 'MemberInfo',
|
||||||
var Modal = require("../../Modal");
|
|
||||||
var sdk = require('../../index');
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
onFinished: function() {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
// work out the current state
|
// work out the current state
|
||||||
if (this.props.member) {
|
if (this.props.member) {
|
||||||
var memberState = this._calculateOpsPermissions();
|
var memberState = this._calculateOpsPermissions(this.props.member);
|
||||||
this.setState(memberState);
|
this.setState(memberState);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
var memberState = this._calculateOpsPermissions(newProps.member);
|
||||||
|
this.setState(memberState);
|
||||||
|
},
|
||||||
|
|
||||||
onKick: function() {
|
onKick: function() {
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
var roomId = this.props.member.roomId;
|
var roomId = this.props.member.roomId;
|
||||||
var target = this.props.member.userId;
|
var target = this.props.member.userId;
|
||||||
MatrixClientPeg.get().kick(roomId, target).done(function() {
|
MatrixClientPeg.get().kick(roomId, target).done(function() {
|
||||||
|
@ -58,7 +71,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
onBan: function() {
|
onBan: function() {
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
var roomId = this.props.member.roomId;
|
var roomId = this.props.member.roomId;
|
||||||
var target = this.props.member.userId;
|
var target = this.props.member.userId;
|
||||||
MatrixClientPeg.get().ban(roomId, target).done(function() {
|
MatrixClientPeg.get().ban(roomId, target).done(function() {
|
||||||
|
@ -75,7 +88,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
onMuteToggle: function() {
|
onMuteToggle: function() {
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
var roomId = this.props.member.roomId;
|
var roomId = this.props.member.roomId;
|
||||||
var target = this.props.member.userId;
|
var target = this.props.member.userId;
|
||||||
var room = MatrixClientPeg.get().getRoom(roomId);
|
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
@ -119,7 +132,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
onModToggle: function() {
|
onModToggle: function() {
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
var roomId = this.props.member.roomId;
|
var roomId = this.props.member.roomId;
|
||||||
var target = this.props.member.userId;
|
var target = this.props.member.userId;
|
||||||
var room = MatrixClientPeg.get().getRoom(roomId);
|
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
@ -213,36 +226,10 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// FIXME: this is horribly duplicated with MemberTile's onLeaveClick.
|
|
||||||
// Not sure what the right solution to this is.
|
|
||||||
onLeaveClick: function() {
|
onLeaveClick: function() {
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
dis.dispatch({
|
||||||
var QuestionDialog = sdk.getComponent("organisms.QuestionDialog");
|
action: 'leave_room',
|
||||||
|
room_id: this.props.member.roomId,
|
||||||
var roomId = this.props.member.roomId;
|
|
||||||
Modal.createDialog(QuestionDialog, {
|
|
||||||
title: "Leave room",
|
|
||||||
description: "Are you sure you want to leave the room?",
|
|
||||||
onFinished: function(should_leave) {
|
|
||||||
if (should_leave) {
|
|
||||||
var d = MatrixClientPeg.get().leave(roomId);
|
|
||||||
|
|
||||||
// FIXME: controller shouldn't be loading a view :(
|
|
||||||
var Loader = sdk.getComponent("atoms.Spinner");
|
|
||||||
var modal = Modal.createDialog(Loader);
|
|
||||||
|
|
||||||
d.then(function() {
|
|
||||||
modal.close();
|
|
||||||
dis.dispatch({action: 'view_next_room'});
|
|
||||||
}, function(err) {
|
|
||||||
modal.close();
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: "Failed to leave room",
|
|
||||||
description: err.toString()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
},
|
},
|
||||||
|
@ -261,13 +248,13 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_calculateOpsPermissions: function() {
|
_calculateOpsPermissions: function(member) {
|
||||||
var defaultPerms = {
|
var defaultPerms = {
|
||||||
can: {},
|
can: {},
|
||||||
muted: false,
|
muted: false,
|
||||||
modifyLevel: false
|
modifyLevel: false
|
||||||
};
|
};
|
||||||
var room = MatrixClientPeg.get().getRoom(this.props.member.roomId);
|
var room = MatrixClientPeg.get().getRoom(member.roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return defaultPerms;
|
return defaultPerms;
|
||||||
}
|
}
|
||||||
|
@ -278,7 +265,7 @@ module.exports = {
|
||||||
return defaultPerms;
|
return defaultPerms;
|
||||||
}
|
}
|
||||||
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
var them = this.props.member;
|
var them = member;
|
||||||
return {
|
return {
|
||||||
can: this._calculateCanPermissions(
|
can: this._calculateCanPermissions(
|
||||||
me, them, powerLevels.getContent()
|
me, them, powerLevels.getContent()
|
||||||
|
@ -320,6 +307,76 @@ module.exports = {
|
||||||
powerLevelContent.events_default
|
powerLevelContent.events_default
|
||||||
);
|
);
|
||||||
return member.powerLevel < levelToSend;
|
return member.powerLevel < levelToSend;
|
||||||
}
|
},
|
||||||
};
|
|
||||||
|
onCancel: function(e) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "view_user",
|
||||||
|
member: null
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var interactButton, kickButton, banButton, muteButton, giveModButton, spinner;
|
||||||
|
if (this.props.member.userId === MatrixClientPeg.get().credentials.userId) {
|
||||||
|
interactButton = <div className="mx_MemberInfo_field" onClick={this.onLeaveClick}>Leave room</div>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
interactButton = <div className="mx_MemberInfo_field" onClick={this.onChatClick}>Start chat</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.creatingRoom) {
|
||||||
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
spinner = <Loader imgClassName="mx_ContextualMenu_spinner"/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.can.kick) {
|
||||||
|
kickButton = <div className="mx_MemberInfo_field" onClick={this.onKick}>
|
||||||
|
Kick
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
if (this.state.can.ban) {
|
||||||
|
banButton = <div className="mx_MemberInfo_field" onClick={this.onBan}>
|
||||||
|
Ban
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
if (this.state.can.mute) {
|
||||||
|
var muteLabel = this.state.muted ? "Unmute" : "Mute";
|
||||||
|
muteButton = <div className="mx_MemberInfo_field" onClick={this.onMuteToggle}>
|
||||||
|
{muteLabel}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
if (this.state.can.modifyLevel) {
|
||||||
|
var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod";
|
||||||
|
giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||||
|
{giveOpLabel}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
return (
|
||||||
|
<div className="mx_MemberInfo">
|
||||||
|
<img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/>
|
||||||
|
<div className="mx_MemberInfo_avatar">
|
||||||
|
<MemberAvatar member={this.props.member} width={48} height={48} />
|
||||||
|
</div>
|
||||||
|
<h2>{ this.props.member.name }</h2>
|
||||||
|
<div className="mx_MemberInfo_profileField">
|
||||||
|
{ this.props.member.userId }
|
||||||
|
</div>
|
||||||
|
<div className="mx_MemberInfo_profileField">
|
||||||
|
power: { this.props.member.powerLevelNorm }%
|
||||||
|
</div>
|
||||||
|
<div className="mx_MemberInfo_buttons">
|
||||||
|
{interactButton}
|
||||||
|
{muteButton}
|
||||||
|
{kickButton}
|
||||||
|
{banButton}
|
||||||
|
{giveModButton}
|
||||||
|
{spinner}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -13,14 +13,17 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
var React = require('react');
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var classNames = require('classnames');
|
||||||
var Modal = require("../../Modal");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var sdk = require('../../index');
|
var Modal = require("../../../Modal");
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
|
|
||||||
var INITIAL_LOAD_NUM_MEMBERS = 50;
|
var INITIAL_LOAD_NUM_MEMBERS = 50;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MemberList',
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
if (!this.props.roomId) return { members: [] };
|
if (!this.props.roomId) return { members: [] };
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
|
@ -38,6 +41,7 @@ module.exports = {
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
cli.on("RoomState.members", this.onRoomStateMember);
|
cli.on("RoomState.members", this.onRoomStateMember);
|
||||||
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
cli.on("Room", this.onRoom); // invites
|
cli.on("Room", this.onRoom); // invites
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -45,6 +49,7 @@ module.exports = {
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
|
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||||
MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
|
MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -97,6 +102,10 @@ module.exports = {
|
||||||
this._updateList();
|
this._updateList();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRoomMemberName: function(ev, member) {
|
||||||
|
this._updateList();
|
||||||
|
},
|
||||||
|
|
||||||
_updateList: function() {
|
_updateList: function() {
|
||||||
this.memberDict = this.getMemberDict();
|
this.memberDict = this.getMemberDict();
|
||||||
|
|
||||||
|
@ -107,7 +116,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
onInvite: function(inputText) {
|
onInvite: function(inputText) {
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
var self = this;
|
var self = this;
|
||||||
inputText = inputText.trim(); // react requires es5-shim so we know trim() exists
|
inputText = inputText.trim(); // react requires es5-shim so we know trim() exists
|
||||||
var isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText);
|
var isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText);
|
||||||
|
@ -189,6 +198,94 @@ module.exports = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return to_display;
|
return to_display;
|
||||||
}
|
},
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
memberSort: function(userIdA, userIdB) {
|
||||||
|
var userA = this.memberDict[userIdA].user;
|
||||||
|
var userB = this.memberDict[userIdB].user;
|
||||||
|
|
||||||
|
var presenceMap = {
|
||||||
|
online: 3,
|
||||||
|
unavailable: 2,
|
||||||
|
offline: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
var presenceOrdA = userA ? presenceMap[userA.presence] : 0;
|
||||||
|
var presenceOrdB = userB ? presenceMap[userB.presence] : 0;
|
||||||
|
|
||||||
|
if (presenceOrdA != presenceOrdB) {
|
||||||
|
return presenceOrdB - presenceOrdA;
|
||||||
|
}
|
||||||
|
|
||||||
|
var latA = userA ? (userA.lastPresenceTs - (userA.lastActiveAgo || userA.lastPresenceTs)) : 0;
|
||||||
|
var latB = userB ? (userB.lastPresenceTs - (userB.lastActiveAgo || userB.lastPresenceTs)) : 0;
|
||||||
|
|
||||||
|
return latB - latA;
|
||||||
|
},
|
||||||
|
|
||||||
|
makeMemberTiles: function(membership) {
|
||||||
|
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
return self.state.members.filter(function(userId) {
|
||||||
|
var m = self.memberDict[userId];
|
||||||
|
return m.membership == membership;
|
||||||
|
}).map(function(userId) {
|
||||||
|
var m = self.memberDict[userId];
|
||||||
|
return (
|
||||||
|
<MemberTile key={userId} member={m} ref={userId} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onPopulateInvite: function(e) {
|
||||||
|
this.onInvite(this.refs.invite.value);
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
|
||||||
|
inviteTile: function() {
|
||||||
|
if (this.state.inviting) {
|
||||||
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
return (
|
||||||
|
<Loader />
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<form onSubmit={this.onPopulateInvite}>
|
||||||
|
<input className="mx_MemberList_invite" ref="invite" placeholder="Invite another user"/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var invitedSection = null;
|
||||||
|
var invitedMemberTiles = this.makeMemberTiles('invite');
|
||||||
|
if (invitedMemberTiles.length > 0) {
|
||||||
|
invitedSection = (
|
||||||
|
<div className="mx_MemberList_invited">
|
||||||
|
<h2>Invited</h2>
|
||||||
|
<div className="mx_MemberList_wrapper">
|
||||||
|
{invitedMemberTiles}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="mx_MemberList">
|
||||||
|
<GeminiScrollbar autoshow={true} className="mx_MemberList_border">
|
||||||
|
{this.inviteTile()}
|
||||||
|
<div>
|
||||||
|
<div className="mx_MemberList_wrapper">
|
||||||
|
{this.makeMemberTiles('join')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{invitedSection}
|
||||||
|
</GeminiScrollbar>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
184
src/components/views/rooms/MemberTile.js
Normal file
184
src/components/views/rooms/MemberTile.js
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var dis = require('../../../dispatcher');
|
||||||
|
var Modal = require("../../../Modal");
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MemberTile',
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
onLeaveClick: function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'leave_room',
|
||||||
|
room_id: this.props.member.roomId,
|
||||||
|
});
|
||||||
|
this.props.onFinished();
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldComponentUpdate: function(nextProps, nextState) {
|
||||||
|
if (this.state.hover !== nextState.hover) return true;
|
||||||
|
if (
|
||||||
|
this.member_last_modified_time === undefined ||
|
||||||
|
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
nextProps.member.user &&
|
||||||
|
(this.user_last_modified_time === undefined ||
|
||||||
|
this.user_last_modified_time < nextProps.member.user.getLastModifiedTime())
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
mouseEnter: function(e) {
|
||||||
|
this.setState({ 'hover': true });
|
||||||
|
},
|
||||||
|
|
||||||
|
mouseLeave: function(e) {
|
||||||
|
this.setState({ 'hover': false });
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick: function(e) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_user',
|
||||||
|
member: this.props.member,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getDuration: function(time) {
|
||||||
|
if (!time) return;
|
||||||
|
var t = parseInt(time / 1000);
|
||||||
|
var s = t % 60;
|
||||||
|
var m = parseInt(t / 60) % 60;
|
||||||
|
var h = parseInt(t / (60 * 60)) % 24;
|
||||||
|
var d = parseInt(t / (60 * 60 * 24));
|
||||||
|
if (t < 60) {
|
||||||
|
if (t < 0) {
|
||||||
|
return "0s";
|
||||||
|
}
|
||||||
|
return s + "s";
|
||||||
|
}
|
||||||
|
if (t < 60 * 60) {
|
||||||
|
return m + "m";
|
||||||
|
}
|
||||||
|
if (t < 24 * 60 * 60) {
|
||||||
|
return h + "h";
|
||||||
|
}
|
||||||
|
return d + "d ";
|
||||||
|
},
|
||||||
|
|
||||||
|
getPrettyPresence: function(user) {
|
||||||
|
if (!user) return "Unknown";
|
||||||
|
var presence = user.presence;
|
||||||
|
if (presence === "online") return "Online";
|
||||||
|
if (presence === "unavailable") return "Idle"; // XXX: is this actually right?
|
||||||
|
if (presence === "offline") return "Offline";
|
||||||
|
return "Unknown";
|
||||||
|
},
|
||||||
|
|
||||||
|
getPowerLabel: function() {
|
||||||
|
var label = this.props.member.userId;
|
||||||
|
if (this.state.isTargetMod) {
|
||||||
|
label += " - Mod (" + this.props.member.powerLevelNorm + "%)";
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
this.member_last_modified_time = this.props.member.getLastModifiedTime();
|
||||||
|
if (this.props.member.user) {
|
||||||
|
this.user_last_modified_time = this.props.member.user.getLastModifiedTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId;
|
||||||
|
|
||||||
|
var power;
|
||||||
|
// if (this.props.member && this.props.member.powerLevelNorm > 0) {
|
||||||
|
// var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
|
||||||
|
// power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
|
||||||
|
// }
|
||||||
|
var presenceClass = "mx_MemberTile_offline";
|
||||||
|
var mainClassName = "mx_MemberTile ";
|
||||||
|
if (this.props.member.user) {
|
||||||
|
if (this.props.member.user.presence === "online") {
|
||||||
|
presenceClass = "mx_MemberTile_online";
|
||||||
|
}
|
||||||
|
else if (this.props.member.user.presence === "unavailable") {
|
||||||
|
presenceClass = "mx_MemberTile_unavailable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainClassName += presenceClass;
|
||||||
|
if (this.state.hover) {
|
||||||
|
mainClassName += " mx_MemberTile_hover";
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = this.props.member.name;
|
||||||
|
// if (isMyUser) name += " (me)"; // this does nothing other than introduce line wrapping and pain
|
||||||
|
//var leave = isMyUser ? <img className="mx_MemberTile_leave" src="img/delete.png" width="10" height="10" onClick={this.onLeaveClick}/> : null;
|
||||||
|
|
||||||
|
var nameEl;
|
||||||
|
if (this.state.hover) {
|
||||||
|
var presence;
|
||||||
|
// FIXME: make presence data update whenever User.presence changes...
|
||||||
|
var active = this.props.member.user ? ((Date.now() - (this.props.member.user.lastPresenceTs - this.props.member.user.lastActiveAgo)) || -1) : -1;
|
||||||
|
if (active >= 0) {
|
||||||
|
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) } { this.getDuration(active) } ago</div>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) }</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
nameEl =
|
||||||
|
<div className="mx_MemberTile_details">
|
||||||
|
<img className="mx_MemberTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
|
||||||
|
<div className="mx_MemberTile_userId">{ name }</div>
|
||||||
|
{ presence }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nameEl =
|
||||||
|
<div className="mx_MemberTile_name">
|
||||||
|
{ name }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
return (
|
||||||
|
<div className={mainClassName} title={ this.getPowerLabel() }
|
||||||
|
onClick={ this.onClick } onMouseEnter={ this.mouseEnter }
|
||||||
|
onMouseLeave={ this.mouseLeave }>
|
||||||
|
<div className="mx_MemberTile_avatar">
|
||||||
|
<MemberAvatar member={this.props.member} width={36} height={36} />
|
||||||
|
{ power }
|
||||||
|
</div>
|
||||||
|
{ nameEl }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -13,13 +13,27 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
var React = require("react");
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var marked = require("marked");
|
||||||
var SlashCommands = require("../../SlashCommands");
|
marked.setOptions({
|
||||||
var Modal = require("../../Modal");
|
renderer: new marked.Renderer(),
|
||||||
var sdk = require('../../index');
|
gfm: true,
|
||||||
|
tables: true,
|
||||||
|
breaks: true,
|
||||||
|
pedantic: false,
|
||||||
|
sanitize: true,
|
||||||
|
smartLists: true,
|
||||||
|
smartypants: false
|
||||||
|
});
|
||||||
|
|
||||||
var dis = require("../../dispatcher");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
var SlashCommands = require("../../../SlashCommands");
|
||||||
|
var Modal = require("../../../Modal");
|
||||||
|
var CallHandler = require('../../../CallHandler');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
var KeyCode = {
|
var KeyCode = {
|
||||||
ENTER: 13,
|
ENTER: 13,
|
||||||
BACKSPACE: 8,
|
BACKSPACE: 8,
|
||||||
|
@ -32,16 +46,33 @@ var KeyCode = {
|
||||||
|
|
||||||
var TYPING_USER_TIMEOUT = 10000;
|
var TYPING_USER_TIMEOUT = 10000;
|
||||||
var TYPING_SERVER_TIMEOUT = 30000;
|
var TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
var MARKDOWN_ENABLED = true;
|
||||||
|
|
||||||
module.exports = {
|
function mdownToHtml(mdown) {
|
||||||
oldScrollHeight: 0,
|
var html = marked(mdown) || "";
|
||||||
|
html = html.trim();
|
||||||
|
// strip start and end <p> tags else you get 'orrible spacing
|
||||||
|
if (html.indexOf("<p>") === 0) {
|
||||||
|
html = html.substring("<p>".length);
|
||||||
|
}
|
||||||
|
if (html.lastIndexOf("</p>") === (html.length - "</p>".length)) {
|
||||||
|
html = html.substring(0, html.length - "</p>".length);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MessageComposer',
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
this.oldScrollHeight = 0;
|
||||||
|
this.markdownEnabled = MARKDOWN_ENABLED;
|
||||||
this.tabStruct = {
|
this.tabStruct = {
|
||||||
completing: false,
|
completing: false,
|
||||||
original: null,
|
original: null,
|
||||||
index: 0
|
index: 0
|
||||||
};
|
};
|
||||||
|
var self = this;
|
||||||
this.sentHistory = {
|
this.sentHistory = {
|
||||||
// The list of typed messages. Index 0 is more recent
|
// The list of typed messages. Index 0 is more recent
|
||||||
data: [],
|
data: [],
|
||||||
|
@ -111,6 +142,8 @@ module.exports = {
|
||||||
// restore the original text the user was typing.
|
// restore the original text the user was typing.
|
||||||
this.element.value = this.originalText;
|
this.element.value = this.originalText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.resizeInput();
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -126,6 +159,7 @@ module.exports = {
|
||||||
var text = window.sessionStorage.getItem("input_" + this.roomId);
|
var text = window.sessionStorage.getItem("input_" + this.roomId);
|
||||||
if (text) {
|
if (text) {
|
||||||
this.element.value = text;
|
this.element.value = text;
|
||||||
|
self.resizeInput();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -137,6 +171,7 @@ module.exports = {
|
||||||
this.refs.textarea,
|
this.refs.textarea,
|
||||||
this.props.room.roomId
|
this.props.room.roomId
|
||||||
);
|
);
|
||||||
|
this.resizeInput();
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -208,7 +243,7 @@ module.exports = {
|
||||||
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
|
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
|
||||||
this.refs.textarea.style.height = "0px";
|
this.refs.textarea.style.height = "0px";
|
||||||
var newHeight = this.refs.textarea.scrollHeight < 100 ? this.refs.textarea.scrollHeight : 100;
|
var newHeight = this.refs.textarea.scrollHeight < 100 ? this.refs.textarea.scrollHeight : 100;
|
||||||
this.refs.textarea.style.height = newHeight + "px";
|
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
|
||||||
if (this.props.roomView) {
|
if (this.props.roomView) {
|
||||||
// kick gemini-scrollbar to re-layout
|
// kick gemini-scrollbar to re-layout
|
||||||
this.props.roomView.forceUpdate();
|
this.props.roomView.forceUpdate();
|
||||||
|
@ -228,6 +263,27 @@ module.exports = {
|
||||||
onEnter: function(ev) {
|
onEnter: function(ev) {
|
||||||
var contentText = this.refs.textarea.value;
|
var contentText = this.refs.textarea.value;
|
||||||
|
|
||||||
|
// bodge for now to set markdown state on/off. We probably want a separate
|
||||||
|
// area for "local" commands which don't hit out to the server.
|
||||||
|
if (contentText.indexOf("/markdown") === 0) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.refs.textarea.value = '';
|
||||||
|
if (contentText.indexOf("/markdown on") === 0) {
|
||||||
|
this.markdownEnabled = true;
|
||||||
|
}
|
||||||
|
else if (contentText.indexOf("/markdown off") === 0) {
|
||||||
|
this.markdownEnabled = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Unknown command",
|
||||||
|
description: "Usage: /markdown on|off"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
|
var cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -239,7 +295,7 @@ module.exports = {
|
||||||
console.log("Command success.");
|
console.log("Command success.");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Command failure: %s", err);
|
console.error("Command failure: %s", err);
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Server error",
|
title: "Server error",
|
||||||
description: err.message
|
description: err.message
|
||||||
|
@ -248,7 +304,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
else if (cmd.error) {
|
else if (cmd.error) {
|
||||||
console.error(cmd.error);
|
console.error(cmd.error);
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Command error",
|
title: "Command error",
|
||||||
description: cmd.error
|
description: cmd.error
|
||||||
|
@ -257,20 +313,26 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = null;
|
var isEmote = /^\/me /i.test(contentText);
|
||||||
if (/^\/me /i.test(contentText)) {
|
var sendMessagePromise;
|
||||||
content = {
|
|
||||||
msgtype: 'm.emote',
|
if (isEmote) {
|
||||||
body: contentText.substring(4)
|
contentText = contentText.substring(4);
|
||||||
};
|
|
||||||
} else {
|
|
||||||
content = {
|
|
||||||
msgtype: 'm.text',
|
|
||||||
body: contentText
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, content).then(function() {
|
var htmlText;
|
||||||
|
if (this.markdownEnabled && (htmlText = mdownToHtml(contentText)) !== contentText) {
|
||||||
|
sendMessagePromise = isEmote ?
|
||||||
|
MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) :
|
||||||
|
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendMessagePromise = isEmote ?
|
||||||
|
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
|
||||||
|
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessagePromise.then(function() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent'
|
action: 'message_sent'
|
||||||
});
|
});
|
||||||
|
@ -280,6 +342,7 @@ module.exports = {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.refs.textarea.value = '';
|
this.refs.textarea.value = '';
|
||||||
|
this.resizeInput();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -445,7 +508,99 @@ module.exports = {
|
||||||
clearTimeout(this.typingTimeout);
|
clearTimeout(this.typingTimeout);
|
||||||
this.typingTimeout = null;
|
this.typingTimeout = null;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputClick: function(ev) {
|
||||||
|
this.refs.textarea.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUploadClick: function(ev) {
|
||||||
|
this.refs.uploadInput.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
onUploadFileSelected: function(ev) {
|
||||||
|
var files = ev.target.files;
|
||||||
|
// MessageComposer shouldn't have to rely on it's parent passing in a callback to upload a file
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
this.props.uploadFile(files[0]);
|
||||||
|
}
|
||||||
|
this.refs.uploadInput.value = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
onHangupClick: function() {
|
||||||
|
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||||
|
if (!call) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'hangup',
|
||||||
|
// hangup the call for this room, which may not be the room in props
|
||||||
|
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||||
|
room_id: call.roomId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onCallClick: function(ev) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'place_call',
|
||||||
|
type: ev.shiftKey ? "screensharing" : "video",
|
||||||
|
room_id: this.props.room.roomId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onVoiceCallClick: function(ev) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'place_call',
|
||||||
|
type: 'voice',
|
||||||
|
room_id: this.props.room.roomId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
|
var uploadInputStyle = {display: 'none'};
|
||||||
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
|
||||||
|
var callButton, videoCallButton, hangupButton;
|
||||||
|
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||||
|
if (this.props.callState && this.props.callState !== 'ended') {
|
||||||
|
hangupButton =
|
||||||
|
<div className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||||
|
<img src="img/hangup.svg" alt="Hangup" title="Hangup" width="25" height="26"/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callButton =
|
||||||
|
<div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick}>
|
||||||
|
<img src="img/voice.svg" alt="Voice call" title="Voice call" width="16" height="26"/>
|
||||||
|
</div>
|
||||||
|
videoCallButton =
|
||||||
|
<div className="mx_MessageComposer_videocall" onClick={this.onCallClick}>
|
||||||
|
<img src="img/call.svg" alt="Video call" title="Video call" width="30" height="22"/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_MessageComposer">
|
||||||
|
<div className="mx_MessageComposer_wrapper">
|
||||||
|
<div className="mx_MessageComposer_row">
|
||||||
|
<div className="mx_MessageComposer_avatar">
|
||||||
|
<MemberAvatar member={me} width={24} height={24} />
|
||||||
|
</div>
|
||||||
|
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
||||||
|
<textarea ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." />
|
||||||
|
</div>
|
||||||
|
<div className="mx_MessageComposer_upload" onClick={this.onUploadClick}>
|
||||||
|
<img src="img/upload.svg" alt="Upload file" title="Upload file" width="19" height="24"/>
|
||||||
|
<input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} />
|
||||||
|
</div>
|
||||||
|
{ hangupButton }
|
||||||
|
{ callButton }
|
||||||
|
{ videoCallButton }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
164
src/components/views/rooms/RoomHeader.js
Normal file
164
src/components/views/rooms/RoomHeader.js
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'RoomHeader',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
room: React.PropTypes.object,
|
||||||
|
editing: React.PropTypes.bool,
|
||||||
|
onSettingsClick: React.PropTypes.func,
|
||||||
|
onSaveClick: React.PropTypes.func,
|
||||||
|
onSearchClick: React.PropTypes.func,
|
||||||
|
onLeaveClick: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
editing: false,
|
||||||
|
onSettingsClick: function() {},
|
||||||
|
onSaveClick: function() {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onVideoClick: function(e) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'place_call',
|
||||||
|
type: e.shiftKey ? "screensharing" : "video",
|
||||||
|
room_id: this.props.room.roomId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onVoiceClick: function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'place_call',
|
||||||
|
type: "voice",
|
||||||
|
room_id: this.props.room.roomId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onNameChange: function(new_name) {
|
||||||
|
if (this.props.room.name != new_name && new_name) {
|
||||||
|
MatrixClientPeg.get().setRoomName(this.props.room.roomId, new_name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getRoomName: function() {
|
||||||
|
return this.refs.name_edit.value;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var EditableText = sdk.getComponent("elements.EditableText");
|
||||||
|
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||||
|
|
||||||
|
var header;
|
||||||
|
if (this.props.simpleHeader) {
|
||||||
|
header =
|
||||||
|
<div className="mx_RoomHeader_wrapper">
|
||||||
|
<div className="mx_RoomHeader_simpleHeader">
|
||||||
|
{ this.props.simpleHeader }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||||
|
|
||||||
|
var name = null;
|
||||||
|
var searchStatus = null;
|
||||||
|
var topic_el = null;
|
||||||
|
var cancel_button = null;
|
||||||
|
var save_button = null;
|
||||||
|
var settings_button = null;
|
||||||
|
var actual_name = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||||
|
if (actual_name) actual_name = actual_name.getContent().name;
|
||||||
|
if (this.props.editing) {
|
||||||
|
name =
|
||||||
|
<div className="mx_RoomHeader_nameEditing">
|
||||||
|
<input className="mx_RoomHeader_nameInput" type="text" defaultValue={actual_name} placeholder="Name" ref="name_edit"/>
|
||||||
|
</div>
|
||||||
|
// if (topic) topic_el = <div className="mx_RoomHeader_topic"><textarea>{ topic.getContent().topic }</textarea></div>
|
||||||
|
cancel_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onCancelClick}>Cancel</div>
|
||||||
|
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save Changes</div>
|
||||||
|
} else {
|
||||||
|
// <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />
|
||||||
|
|
||||||
|
var searchStatus;
|
||||||
|
if (this.props.searchInfo && this.props.searchInfo.searchTerm) {
|
||||||
|
searchStatus = <div className="mx_RoomHeader_searchStatus"> ({ this.props.searchInfo.searchCount } results)</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
name =
|
||||||
|
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||||
|
<div className="mx_RoomHeader_nametext" title={ this.props.room.name }>{ this.props.room.name }</div>
|
||||||
|
{ searchStatus }
|
||||||
|
<div className="mx_RoomHeader_settingsButton">
|
||||||
|
<img src="img/settings.svg" width="12" height="12"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
if (topic) topic_el = <div className="mx_RoomHeader_topic" title={topic.getContent().topic}>{ topic.getContent().topic }</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
var roomAvatar = null;
|
||||||
|
if (this.props.room) {
|
||||||
|
roomAvatar = (
|
||||||
|
<RoomAvatar room={this.props.room} width="48" height="48" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var leave_button;
|
||||||
|
if (this.props.onLeaveClick) {
|
||||||
|
leave_button =
|
||||||
|
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton">
|
||||||
|
<img src="img/leave.svg" title="Leave room" alt="Leave room" width="26" height="20" onClick={this.props.onLeaveClick}/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
header =
|
||||||
|
<div className="mx_RoomHeader_wrapper">
|
||||||
|
<div className="mx_RoomHeader_leftRow">
|
||||||
|
<div className="mx_RoomHeader_avatar">
|
||||||
|
{ roomAvatar }
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomHeader_info">
|
||||||
|
{ name }
|
||||||
|
{ topic_el }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{cancel_button}
|
||||||
|
{save_button}
|
||||||
|
<div className="mx_RoomHeader_rightRow">
|
||||||
|
{ leave_button }
|
||||||
|
<div className="mx_RoomHeader_button">
|
||||||
|
<img src="img/search.svg" title="Search" alt="Search" width="21" height="19" onClick={this.props.onSearchClick}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomHeader">
|
||||||
|
{ header }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
307
src/components/views/rooms/RoomList.js
Normal file
307
src/components/views/rooms/RoomList.js
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
var React = require("react");
|
||||||
|
var ReactDOM = require("react-dom");
|
||||||
|
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
var RoomListSorter = require("../../../RoomListSorter");
|
||||||
|
var UnreadStatus = require('../../../UnreadStatus');
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
var HIDE_CONFERENCE_CHANS = true;
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'RoomList',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
ConferenceHandler: React.PropTypes.any,
|
||||||
|
collapsed: React.PropTypes.bool,
|
||||||
|
currentRoom: React.PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
activityMap: null,
|
||||||
|
lists: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
cli.on("Room", this.onRoom);
|
||||||
|
cli.on("Room.timeline", this.onRoomTimeline);
|
||||||
|
cli.on("Room.name", this.onRoomName);
|
||||||
|
cli.on("Room.tags", this.onRoomTags);
|
||||||
|
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||||
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
|
|
||||||
|
var s = this.getRoomLists();
|
||||||
|
s.activityMap = {};
|
||||||
|
this.setState(s);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
},
|
||||||
|
|
||||||
|
onAction: function(payload) {
|
||||||
|
switch (payload.action) {
|
||||||
|
case 'view_tooltip':
|
||||||
|
this.tooltip = payload.tooltip;
|
||||||
|
this._repositionTooltip();
|
||||||
|
if (this.tooltip) this.tooltip.style.display = 'block';
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
if (MatrixClientPeg.get()) {
|
||||||
|
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||||
|
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
|
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||||
|
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
this.state.activityMap[newProps.selectedRoom] = undefined;
|
||||||
|
this.setState({
|
||||||
|
activityMap: this.state.activityMap
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoom: function(room) {
|
||||||
|
this.refreshRoomList();
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
||||||
|
if (toStartOfTimeline) return;
|
||||||
|
|
||||||
|
var hl = 0;
|
||||||
|
if (
|
||||||
|
room.roomId != this.props.selectedRoom &&
|
||||||
|
ev.getSender() != MatrixClientPeg.get().credentials.userId)
|
||||||
|
{
|
||||||
|
if (UnreadStatus.eventTriggersUnreadCount(ev)) {
|
||||||
|
hl = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
|
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||||
|
if ((actions && actions.tweaks && actions.tweaks.highlight) ||
|
||||||
|
(me && me.membership == "invite"))
|
||||||
|
{
|
||||||
|
hl = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newState = this.getRoomLists();
|
||||||
|
if (hl > 0) {
|
||||||
|
// obviously this won't deep copy but this shouldn't be necessary
|
||||||
|
var amap = this.state.activityMap;
|
||||||
|
amap[room.roomId] = Math.max(amap[room.roomId] || 0, hl);
|
||||||
|
|
||||||
|
newState.activityMap = amap;
|
||||||
|
|
||||||
|
}
|
||||||
|
// still want to update the list even if the highlight status
|
||||||
|
// hasn't changed because the ordering may have
|
||||||
|
this.setState(newState);
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomName: function(room) {
|
||||||
|
this.refreshRoomList();
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomTags: function(event, room) {
|
||||||
|
this.refreshRoomList();
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomStateEvents: function(ev, state) {
|
||||||
|
setTimeout(this.refreshRoomList, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomMemberName: function(ev, member) {
|
||||||
|
setTimeout(this.refreshRoomList, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshRoomList: function() {
|
||||||
|
// TODO: rather than bluntly regenerating and re-sorting everything
|
||||||
|
// every time we see any kind of room change from the JS SDK
|
||||||
|
// we could do incremental updates on our copy of the state
|
||||||
|
// based on the room which has actually changed. This would stop
|
||||||
|
// us re-rendering all the sublists every time anything changes anywhere
|
||||||
|
// in the state of the client.
|
||||||
|
this.setState(this.getRoomLists());
|
||||||
|
},
|
||||||
|
|
||||||
|
getRoomLists: function() {
|
||||||
|
var self = this;
|
||||||
|
var s = { lists: {} };
|
||||||
|
|
||||||
|
s.lists["im.vector.fake.invite"] = [];
|
||||||
|
s.lists["m.favourite"] = [];
|
||||||
|
s.lists["im.vector.fake.recent"] = [];
|
||||||
|
s.lists["m.lowpriority"] = [];
|
||||||
|
s.lists["im.vector.fake.archived"] = [];
|
||||||
|
|
||||||
|
MatrixClientPeg.get().getRooms().forEach(function(room) {
|
||||||
|
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
|
|
||||||
|
if (me && me.membership == "invite") {
|
||||||
|
s.lists["im.vector.fake.invite"].push(room);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var shouldShowRoom = (
|
||||||
|
me && (me.membership == "join")
|
||||||
|
);
|
||||||
|
|
||||||
|
// hiding conf rooms only ever toggles shouldShowRoom to false
|
||||||
|
if (shouldShowRoom && HIDE_CONFERENCE_CHANS) {
|
||||||
|
// we want to hide the 1:1 conf<->user room and not the group chat
|
||||||
|
var joinedMembers = room.getJoinedMembers();
|
||||||
|
if (joinedMembers.length === 2) {
|
||||||
|
var otherMember = joinedMembers.filter(function(m) {
|
||||||
|
return m.userId !== me.userId
|
||||||
|
})[0];
|
||||||
|
var ConfHandler = self.props.ConferenceHandler;
|
||||||
|
if (ConfHandler && ConfHandler.isConferenceUser(otherMember.userId)) {
|
||||||
|
// console.log("Hiding conference 1:1 room %s", room.roomId);
|
||||||
|
shouldShowRoom = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldShowRoom) {
|
||||||
|
var tagNames = Object.keys(room.tags);
|
||||||
|
if (tagNames.length) {
|
||||||
|
for (var i = 0; i < tagNames.length; i++) {
|
||||||
|
var tagName = tagNames[i];
|
||||||
|
s.lists[tagName] = s.lists[tagName] || [];
|
||||||
|
s.lists[tagNames[i]].push(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s.lists["im.vector.fake.recent"].push(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//console.log("calculated new roomLists; im.vector.fake.recent = " + s.lists["im.vector.fake.recent"]);
|
||||||
|
|
||||||
|
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
|
||||||
|
|
||||||
|
return s;
|
||||||
|
},
|
||||||
|
|
||||||
|
_repositionTooltip: function(e) {
|
||||||
|
if (this.tooltip && this.tooltip.parentElement) {
|
||||||
|
var scroll = ReactDOM.findDOMNode(this);
|
||||||
|
this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.children[2].scrollTop) + "px";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onShowClick: function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'show_left_panel',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var expandButton = this.props.collapsed ?
|
||||||
|
<img className="mx_RoomList_expandButton" onClick={ this.onShowClick } src="img/menu.png" width="20" alt=">"/> :
|
||||||
|
null;
|
||||||
|
|
||||||
|
var RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GeminiScrollbar className="mx_RoomList_scrollbar" autoshow={true} onScroll={self._repositionTooltip}>
|
||||||
|
<div className="mx_RoomList">
|
||||||
|
{ expandButton }
|
||||||
|
|
||||||
|
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
||||||
|
label="Invites"
|
||||||
|
editable={ false }
|
||||||
|
order="recent"
|
||||||
|
activityMap={ self.state.activityMap }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
|
collapsed={ self.props.collapsed } />
|
||||||
|
|
||||||
|
<RoomSubList list={ self.state.lists['m.favourite'] }
|
||||||
|
label="Favourites"
|
||||||
|
tagName="m.favourite"
|
||||||
|
verb="favourite"
|
||||||
|
editable={ true }
|
||||||
|
order="manual"
|
||||||
|
activityMap={ self.state.activityMap }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
|
collapsed={ self.props.collapsed } />
|
||||||
|
|
||||||
|
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
|
||||||
|
label="Rooms"
|
||||||
|
editable={ true }
|
||||||
|
verb="restore"
|
||||||
|
order="recent"
|
||||||
|
activityMap={ self.state.activityMap }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
|
collapsed={ self.props.collapsed } />
|
||||||
|
|
||||||
|
{ Object.keys(self.state.lists).map(function(tagName) {
|
||||||
|
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|archived))$/)) {
|
||||||
|
return <RoomSubList list={ self.state.lists[tagName] }
|
||||||
|
key={ tagName }
|
||||||
|
label={ tagName }
|
||||||
|
tagName={ tagName }
|
||||||
|
verb={ "tag as " + tagName }
|
||||||
|
editable={ true }
|
||||||
|
order="manual"
|
||||||
|
activityMap={ self.state.activityMap }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
|
collapsed={ self.props.collapsed } />
|
||||||
|
|
||||||
|
}
|
||||||
|
}) }
|
||||||
|
|
||||||
|
<RoomSubList list={ self.state.lists['m.lowpriority'] }
|
||||||
|
label="Low priority"
|
||||||
|
tagName="m.lowpriority"
|
||||||
|
verb="demote"
|
||||||
|
editable={ true }
|
||||||
|
order="recent"
|
||||||
|
bottommost={ self.state.lists['im.vector.fake.archived'].length === 0 }
|
||||||
|
activityMap={ self.state.activityMap }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
|
collapsed={ self.props.collapsed } />
|
||||||
|
|
||||||
|
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
|
||||||
|
label="Historical"
|
||||||
|
editable={ false }
|
||||||
|
order="recent"
|
||||||
|
bottommost={ true }
|
||||||
|
activityMap={ self.state.activityMap }
|
||||||
|
selectedRoom={ self.props.selectedRoom }
|
||||||
|
collapsed={ self.props.collapsed } />
|
||||||
|
</div>
|
||||||
|
</GeminiScrollbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
237
src/components/views/rooms/RoomSettings.js
Normal file
237
src/components/views/rooms/RoomSettings.js
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'RoomSettings',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
room: React.PropTypes.object.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
power_levels_changed: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getTopic: function() {
|
||||||
|
return this.refs.topic.value;
|
||||||
|
},
|
||||||
|
|
||||||
|
getJoinRules: function() {
|
||||||
|
return this.refs.is_private.checked ? "invite" : "public";
|
||||||
|
},
|
||||||
|
|
||||||
|
getHistoryVisibility: function() {
|
||||||
|
return this.refs.share_history.checked ? "shared" : "invited";
|
||||||
|
},
|
||||||
|
|
||||||
|
getPowerLevels: function() {
|
||||||
|
if (!this.state.power_levels_changed) return undefined;
|
||||||
|
|
||||||
|
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
|
||||||
|
power_levels = power_levels.getContent();
|
||||||
|
|
||||||
|
var new_power_levels = {
|
||||||
|
ban: parseInt(this.refs.ban.value),
|
||||||
|
kick: parseInt(this.refs.kick.value),
|
||||||
|
redact: parseInt(this.refs.redact.value),
|
||||||
|
invite: parseInt(this.refs.invite.value),
|
||||||
|
events_default: parseInt(this.refs.events_default.value),
|
||||||
|
state_default: parseInt(this.refs.state_default.value),
|
||||||
|
users_default: parseInt(this.refs.users_default.value),
|
||||||
|
users: power_levels.users,
|
||||||
|
events: power_levels.events,
|
||||||
|
};
|
||||||
|
|
||||||
|
return new_power_levels;
|
||||||
|
},
|
||||||
|
|
||||||
|
onPowerLevelsChanged: function() {
|
||||||
|
this.setState({
|
||||||
|
power_levels_changed: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||||
|
|
||||||
|
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||||
|
if (topic) topic = topic.getContent().topic;
|
||||||
|
|
||||||
|
var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', '');
|
||||||
|
if (join_rule) join_rule = join_rule.getContent().join_rule;
|
||||||
|
|
||||||
|
var history_visibility = this.props.room.currentState.getStateEvents('m.room.history_visibility', '');
|
||||||
|
if (history_visibility) history_visibility = history_visibility.getContent().history_visibility;
|
||||||
|
|
||||||
|
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
|
||||||
|
|
||||||
|
var events_levels = power_levels.events || {};
|
||||||
|
|
||||||
|
if (power_levels) {
|
||||||
|
power_levels = power_levels.getContent();
|
||||||
|
|
||||||
|
var ban_level = parseInt(power_levels.ban);
|
||||||
|
var kick_level = parseInt(power_levels.kick);
|
||||||
|
var redact_level = parseInt(power_levels.redact);
|
||||||
|
var invite_level = parseInt(power_levels.invite || 0);
|
||||||
|
var send_level = parseInt(power_levels.events_default || 0);
|
||||||
|
var state_level = parseInt(power_levels.state_default || 0);
|
||||||
|
var default_user_level = parseInt(power_levels.users_default || 0);
|
||||||
|
|
||||||
|
if (power_levels.ban == undefined) ban_level = 50;
|
||||||
|
if (power_levels.kick == undefined) kick_level = 50;
|
||||||
|
if (power_levels.redact == undefined) redact_level = 50;
|
||||||
|
|
||||||
|
var user_levels = power_levels.users || {};
|
||||||
|
|
||||||
|
var user_id = MatrixClientPeg.get().credentials.userId;
|
||||||
|
|
||||||
|
var current_user_level = user_levels[user_id];
|
||||||
|
if (current_user_level == undefined) current_user_level = default_user_level;
|
||||||
|
|
||||||
|
var power_level_level = events_levels["m.room.power_levels"];
|
||||||
|
if (power_level_level == undefined) {
|
||||||
|
power_level_level = state_level;
|
||||||
|
}
|
||||||
|
|
||||||
|
var can_change_levels = current_user_level >= power_level_level;
|
||||||
|
} else {
|
||||||
|
var ban_level = 50;
|
||||||
|
var kick_level = 50;
|
||||||
|
var redact_level = 50;
|
||||||
|
var invite_level = 0;
|
||||||
|
var send_level = 0;
|
||||||
|
var state_level = 0;
|
||||||
|
var default_user_level = 0;
|
||||||
|
|
||||||
|
var user_levels = [];
|
||||||
|
var events_levels = [];
|
||||||
|
|
||||||
|
var current_user_level = 0;
|
||||||
|
|
||||||
|
var power_level_level = 0;
|
||||||
|
|
||||||
|
var can_change_levels = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var room_avatar_level = parseInt(power_levels.state_default || 0);
|
||||||
|
if (events_levels['m.room.avatar'] !== undefined) {
|
||||||
|
room_avatar_level = events_levels['m.room.avatar'];
|
||||||
|
}
|
||||||
|
var can_set_room_avatar = current_user_level >= room_avatar_level;
|
||||||
|
|
||||||
|
var change_avatar;
|
||||||
|
if (can_set_room_avatar) {
|
||||||
|
change_avatar = <div>
|
||||||
|
<h3>Room Icon</h3>
|
||||||
|
<ChangeAvatar room={this.props.room} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
var banned = this.props.room.getMembersWithMembership("ban");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomSettings">
|
||||||
|
<textarea className="mx_RoomSettings_description" placeholder="Topic" defaultValue={topic} ref="topic"/> <br/>
|
||||||
|
<label><input type="checkbox" ref="is_private" defaultChecked={join_rule != "public"}/> Make this room private</label> <br/>
|
||||||
|
<label><input type="checkbox" ref="share_history" defaultChecked={history_visibility == "shared"}/> Share message history with new users</label> <br/>
|
||||||
|
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label> <br/>
|
||||||
|
|
||||||
|
<h3>Power levels</h3>
|
||||||
|
<div className="mx_RoomSettings_power_levels mx_RoomSettings_settings">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="mx_RoomSettings_ban_level">Ban level</label>
|
||||||
|
<input type="text" defaultValue={ban_level} size="3" ref="ban" id="mx_RoomSettings_ban_level"
|
||||||
|
disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="mx_RoomSettings_kick_level">Kick level</label>
|
||||||
|
<input type="text" defaultValue={kick_level} size="3" ref="kick" id="mx_RoomSettings_kick_level"
|
||||||
|
disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="mx_RoomSettings_redact_level">Redact level</label>
|
||||||
|
<input type="text" defaultValue={redact_level} size="3" ref="redact" id="mx_RoomSettings_redact_level"
|
||||||
|
disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="mx_RoomSettings_invite_level">Invite level</label>
|
||||||
|
<input type="text" defaultValue={invite_level} size="3" ref="invite" id="mx_RoomSettings_invite_level"
|
||||||
|
disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="mx_RoomSettings_event_level">Send event level</label>
|
||||||
|
<input type="text" defaultValue={send_level} size="3" ref="events_default" id="mx_RoomSettings_event_level"
|
||||||
|
disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="mx_RoomSettings_state_level">Set state level</label>
|
||||||
|
<input type="text" defaultValue={state_level} size="3" ref="state_default" id="mx_RoomSettings_state_level"
|
||||||
|
disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="mx_RoomSettings_user_level">Default user level</label>
|
||||||
|
<input type="text" defaultValue={default_user_level} size="3" ref="users_default"
|
||||||
|
id="mx_RoomSettings_user_level" disabled={!can_change_levels || current_user_level < default_user_level}
|
||||||
|
onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>User levels</h3>
|
||||||
|
<div className="mx_RoomSettings_user_levels mx_RoomSettings_settings">
|
||||||
|
{Object.keys(user_levels).map(function(user, i) {
|
||||||
|
return (
|
||||||
|
<div key={user}>
|
||||||
|
<label htmlFor={"mx_RoomSettings_user_"+i}>{user}</label>
|
||||||
|
<input type="text" defaultValue={user_levels[user]} size="3" id={"mx_RoomSettings_user_"+i} disabled/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Event levels</h3>
|
||||||
|
<div className="mx_RoomSettings_event_lvels mx_RoomSettings_settings">
|
||||||
|
{Object.keys(events_levels).map(function(event_type, i) {
|
||||||
|
return (
|
||||||
|
<div key={event_type}>
|
||||||
|
<label htmlFor={"mx_RoomSettings_event_"+i}>{event_type}</label>
|
||||||
|
<input type="text" defaultValue={events_levels[event_type]} size="3" id={"mx_RoomSettings_event_"+i} disabled/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Banned users</h3>
|
||||||
|
<div className="mx_RoomSettings_banned">
|
||||||
|
{banned.map(function(member, i) {
|
||||||
|
return (
|
||||||
|
<div key={i}>
|
||||||
|
{member.userId}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{change_avatar}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
126
src/components/views/rooms/RoomTile.js
Normal file
126
src/components/views/rooms/RoomTile.js
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var classNames = require('classnames');
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'RoomTile',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// TODO: We should *optionally* support DND stuff and ideally be impl agnostic about it
|
||||||
|
connectDragSource: React.PropTypes.func.isRequired,
|
||||||
|
connectDropTarget: React.PropTypes.func.isRequired,
|
||||||
|
isDragging: React.PropTypes.bool.isRequired,
|
||||||
|
|
||||||
|
room: React.PropTypes.object.isRequired,
|
||||||
|
collapsed: React.PropTypes.bool.isRequired,
|
||||||
|
selected: React.PropTypes.bool.isRequired,
|
||||||
|
unread: React.PropTypes.bool.isRequired,
|
||||||
|
highlight: React.PropTypes.bool.isRequired,
|
||||||
|
isInvite: React.PropTypes.bool.isRequired,
|
||||||
|
roomSubList: React.PropTypes.object.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return( { hover : false });
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick: function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: this.props.room.roomId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseEnter: function() {
|
||||||
|
this.setState( { hover : true });
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseLeave: function() {
|
||||||
|
this.setState( { hover : false });
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
var me = this.props.room.currentState.members[myUserId];
|
||||||
|
var classes = classNames({
|
||||||
|
'mx_RoomTile': true,
|
||||||
|
'mx_RoomTile_selected': this.props.selected,
|
||||||
|
'mx_RoomTile_unread': this.props.unread,
|
||||||
|
'mx_RoomTile_highlight': this.props.highlight,
|
||||||
|
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// XXX: We should never display raw room IDs, but sometimes the
|
||||||
|
// room name js sdk gives is undefined (cannot repro this -- k)
|
||||||
|
var name = this.props.room.name || this.props.room.roomId;
|
||||||
|
|
||||||
|
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||||
|
var badge;
|
||||||
|
if (this.props.highlight) {
|
||||||
|
badge = <div className="mx_RoomTile_badge"/>;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if (this.props.highlight) {
|
||||||
|
badge = <div className="mx_RoomTile_badge">!</div>;
|
||||||
|
}
|
||||||
|
else if (this.props.unread) {
|
||||||
|
badge = <div className="mx_RoomTile_badge">1</div>;
|
||||||
|
}
|
||||||
|
var nameCell;
|
||||||
|
if (badge) {
|
||||||
|
nameCell = <div className="mx_RoomTile_nameBadge"><div className="mx_RoomTile_name">{name}</div><div className="mx_RoomTile_badgeCell">{badge}</div></div>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nameCell = <div className="mx_RoomTile_name">{name}</div>;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
var label;
|
||||||
|
if (!this.props.collapsed) {
|
||||||
|
var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : '');
|
||||||
|
label = <div className={ className }>{name}</div>;
|
||||||
|
}
|
||||||
|
else if (this.state.hover) {
|
||||||
|
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||||
|
label = <RoomTooltip room={this.props.room}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||||
|
|
||||||
|
// These props are injected by React DnD,
|
||||||
|
// as defined by your `collect` function above:
|
||||||
|
var isDragging = this.props.isDragging;
|
||||||
|
var connectDragSource = this.props.connectDragSource;
|
||||||
|
var connectDropTarget = this.props.connectDropTarget;
|
||||||
|
|
||||||
|
return connectDragSource(connectDropTarget(
|
||||||
|
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||||
|
<div className="mx_RoomTile_avatar">
|
||||||
|
<RoomAvatar room={this.props.room} width="24" height="24" />
|
||||||
|
{ badge }
|
||||||
|
</div>
|
||||||
|
{ label }
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
131
src/components/views/settings/ChangeAvatar.js
Normal file
131
src/components/views/settings/ChangeAvatar.js
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'ChangeAvatar',
|
||||||
|
propTypes: {
|
||||||
|
initialAvatarUrl: React.PropTypes.string,
|
||||||
|
room: React.PropTypes.object,
|
||||||
|
},
|
||||||
|
|
||||||
|
Phases: {
|
||||||
|
Display: "display",
|
||||||
|
Uploading: "uploading",
|
||||||
|
Error: "error",
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
avatarUrl: this.props.initialAvatarUrl,
|
||||||
|
phase: this.Phases.Display,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
if (this.avatarSet) {
|
||||||
|
// don't clobber what the user has just set
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
avatarUrl: newProps.initialAvatarUrl
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setAvatarFromFile: function(file) {
|
||||||
|
var newUrl = null;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
phase: this.Phases.Uploading
|
||||||
|
});
|
||||||
|
var self = this;
|
||||||
|
MatrixClientPeg.get().uploadContent(file).then(function(url) {
|
||||||
|
newUrl = url;
|
||||||
|
if (self.props.room) {
|
||||||
|
return MatrixClientPeg.get().sendStateEvent(
|
||||||
|
self.props.room.roomId,
|
||||||
|
'm.room.avatar',
|
||||||
|
{url: url},
|
||||||
|
''
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return MatrixClientPeg.get().setAvatarUrl(url);
|
||||||
|
}
|
||||||
|
}).done(function() {
|
||||||
|
self.setState({
|
||||||
|
phase: self.Phases.Display,
|
||||||
|
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl)
|
||||||
|
});
|
||||||
|
}, function(error) {
|
||||||
|
self.setState({
|
||||||
|
phase: self.Phases.Error
|
||||||
|
});
|
||||||
|
self.onError(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onFileSelected: function(ev) {
|
||||||
|
this.avatarSet = true;
|
||||||
|
this.setAvatarFromFile(ev.target.files[0]);
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: function(error) {
|
||||||
|
this.setState({
|
||||||
|
errorText: "Failed to upload profile picture!"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||||
|
var avatarImg;
|
||||||
|
// Having just set an avatar we just display that since it will take a little
|
||||||
|
// time to propagate through to the RoomAvatar.
|
||||||
|
if (this.props.room && !this.avatarSet) {
|
||||||
|
avatarImg = <RoomAvatar room={this.props.room} width='320' height='240' resizeMethod='scale' />;
|
||||||
|
} else {
|
||||||
|
var style = {
|
||||||
|
maxWidth: 320,
|
||||||
|
maxHeight: 240,
|
||||||
|
};
|
||||||
|
avatarImg = <img src={this.state.avatarUrl} style={style} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.state.phase) {
|
||||||
|
case this.Phases.Display:
|
||||||
|
case this.Phases.Error:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
{avatarImg}
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
Upload new:
|
||||||
|
<input type="file" onChange={this.onFileSelected}/>
|
||||||
|
{this.state.errorText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case this.Phases.Uploading:
|
||||||
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
return (
|
||||||
|
<Loader />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -15,10 +15,16 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
var React = require('react');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
module.exports = React.createClass({
|
||||||
|
displayName: 'ChangeDisplayName',
|
||||||
|
propTypes: {
|
||||||
|
onFinished: React.PropTypes.func
|
||||||
|
},
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
onFinished: function() {},
|
onFinished: function() {},
|
||||||
|
@ -68,4 +74,32 @@ module.exports = {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
edit: function() {
|
||||||
|
this.refs.displayname_edit.edit()
|
||||||
|
},
|
||||||
|
|
||||||
|
onValueChanged: function(new_value, shouldSubmit) {
|
||||||
|
if (shouldSubmit) {
|
||||||
|
this.changeDisplayname(new_value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
if (this.state.busy) {
|
||||||
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
return (
|
||||||
|
<Loader />
|
||||||
|
);
|
||||||
|
} else if (this.state.errorString) {
|
||||||
|
return (
|
||||||
|
<div className="error">{this.state.errorString}</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var EditableText = sdk.getComponent('elements.EditableText');
|
||||||
|
return (
|
||||||
|
<EditableText ref="displayname_edit" initialValue={this.state.displayName} label="Click to set display name." onValueChanged={this.onValueChanged}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
135
src/components/views/settings/ChangePassword.js
Normal file
135
src/components/views/settings/ChangePassword.js
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'ChangePassword',
|
||||||
|
propTypes: {
|
||||||
|
onFinished: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
Phases: {
|
||||||
|
Edit: "edit",
|
||||||
|
Uploading: "uploading",
|
||||||
|
Error: "error",
|
||||||
|
Success: "Success"
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
onFinished: function() {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
phase: this.Phases.Edit,
|
||||||
|
errorString: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
changePassword: function(old_password, new_password) {
|
||||||
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
var authDict = {
|
||||||
|
type: 'm.login.password',
|
||||||
|
user: cli.credentials.userId,
|
||||||
|
password: old_password
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
phase: this.Phases.Uploading,
|
||||||
|
errorString: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
var d = cli.setPassword(authDict, new_password);
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
d.then(function() {
|
||||||
|
self.setState({
|
||||||
|
phase: self.Phases.Success,
|
||||||
|
errorString: '',
|
||||||
|
})
|
||||||
|
}, function(err) {
|
||||||
|
self.setState({
|
||||||
|
phase: self.Phases.Error,
|
||||||
|
errorString: err.toString()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onClickChange: function() {
|
||||||
|
var old_password = this.refs.old_input.value;
|
||||||
|
var new_password = this.refs.new_input.value;
|
||||||
|
var confirm_password = this.refs.confirm_input.value;
|
||||||
|
if (new_password != confirm_password) {
|
||||||
|
this.setState({
|
||||||
|
state: this.Phases.Error,
|
||||||
|
errorString: "Passwords don't match"
|
||||||
|
});
|
||||||
|
} else if (new_password == '' || old_password == '') {
|
||||||
|
this.setState({
|
||||||
|
state: this.Phases.Error,
|
||||||
|
errorString: "Passwords can't be empty"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.changePassword(old_password, new_password);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
switch (this.state.phase) {
|
||||||
|
case this.Phases.Edit:
|
||||||
|
case this.Phases.Error:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
<div>{this.state.errorString}</div>
|
||||||
|
<div><label>Old password <input type="password" ref="old_input"/></label></div>
|
||||||
|
<div><label>New password <input type="password" ref="new_input"/></label></div>
|
||||||
|
<div><label>Confirm password <input type="password" ref="confirm_input"/></label></div>
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button onClick={this.onClickChange}>Change Password</button>
|
||||||
|
<button onClick={this.props.onFinished}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case this.Phases.Uploading:
|
||||||
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
return (
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case this.Phases.Success:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
Success!
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button onClick={this.props.onFinished}>Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -15,10 +15,13 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
var sdk = require('../../index');
|
var React = require("react");
|
||||||
var dis = require("../../dispatcher");
|
var Notifier = require("../../../Notifier");
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
|
displayName: 'EnableNotificationsButton',
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
@ -36,12 +39,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
enabled: function() {
|
enabled: function() {
|
||||||
var Notifier = sdk.getComponent('organisms.Notifier');
|
|
||||||
return Notifier.isEnabled();
|
return Notifier.isEnabled();
|
||||||
},
|
},
|
||||||
|
|
||||||
onClick: function() {
|
onClick: function() {
|
||||||
var Notifier = sdk.getComponent('organisms.Notifier');
|
|
||||||
var self = this;
|
var self = this;
|
||||||
if (!Notifier.supportsDesktopNotifications()) {
|
if (!Notifier.supportsDesktopNotifications()) {
|
||||||
return;
|
return;
|
||||||
|
@ -55,4 +56,20 @@ module.exports = {
|
||||||
}
|
}
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
render: function() {
|
||||||
|
if (this.enabled()) {
|
||||||
|
return (
|
||||||
|
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>
|
||||||
|
Disable Notifications
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<button className="mx_EnableNotificationsButton" onClick={this.onClick}>
|
||||||
|
Enable Notifications
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -13,9 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
var React = require("react");
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
var CallHandler = require("../../../CallHandler");
|
var CallHandler = require("../../../CallHandler");
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* State vars:
|
* State vars:
|
||||||
|
@ -23,14 +25,31 @@ var CallHandler = require("../../../CallHandler");
|
||||||
*
|
*
|
||||||
* Props:
|
* Props:
|
||||||
* this.props.room = Room (JS SDK)
|
* this.props.room = Room (JS SDK)
|
||||||
|
* this.props.ConferenceHandler = A Conference Handler implementation
|
||||||
|
* Must have a function signature:
|
||||||
|
* getConferenceCallForRoom(roomId: string): MatrixCall
|
||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
|
displayName: 'CallView',
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
this._trackedRoom = null;
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
this.showCall(this.props.room.roomId);
|
this._trackedRoom = this.props.room;
|
||||||
|
this.showCall(this._trackedRoom.roomId);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var call = CallHandler.getAnyActiveCall();
|
||||||
|
if (call) {
|
||||||
|
console.log(
|
||||||
|
"Global CallView is now tracking active call in room %s",
|
||||||
|
call.roomId
|
||||||
|
);
|
||||||
|
this._trackedRoom = MatrixClientPeg.get().getRoom(call.roomId);
|
||||||
|
this.showCall(call.roomId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -39,19 +58,22 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
// if we were given a room_id to track, don't handle anything else.
|
// don't filter out payloads for room IDs other than props.room because
|
||||||
if (payload.room_id && this.props.room &&
|
// we may be interested in the conf 1:1 room
|
||||||
this.props.room.roomId !== payload.room_id) {
|
if (payload.action !== 'call_state' || !payload.room_id) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (payload.action !== 'call_state') {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.showCall(payload.room_id);
|
this.showCall(payload.room_id);
|
||||||
},
|
},
|
||||||
|
|
||||||
showCall: function(roomId) {
|
showCall: function(roomId) {
|
||||||
var call = CallHandler.getCall(roomId);
|
var call = (
|
||||||
|
CallHandler.getCallForRoom(roomId) ||
|
||||||
|
(this.props.ConferenceHandler ?
|
||||||
|
this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
|
||||||
|
null
|
||||||
|
)
|
||||||
|
);
|
||||||
if (call) {
|
if (call) {
|
||||||
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
|
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
|
||||||
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
|
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
|
||||||
|
@ -60,13 +82,29 @@ module.exports = {
|
||||||
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
||||||
}
|
}
|
||||||
if (call && call.type === "video" && call.state !== 'ended') {
|
if (call && call.type === "video" && call.state !== 'ended') {
|
||||||
this.getVideoView().getLocalVideoElement().style.display = "initial";
|
// if this call is a conf call, don't display local video as the
|
||||||
|
// conference will have us in it
|
||||||
|
this.getVideoView().getLocalVideoElement().style.display = (
|
||||||
|
call.confUserId ? "none" : "initial"
|
||||||
|
);
|
||||||
this.getVideoView().getRemoteVideoElement().style.display = "initial";
|
this.getVideoView().getRemoteVideoElement().style.display = "initial";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.getVideoView().getLocalVideoElement().style.display = "none";
|
this.getVideoView().getLocalVideoElement().style.display = "none";
|
||||||
this.getVideoView().getRemoteVideoElement().style.display = "none";
|
this.getVideoView().getRemoteVideoElement().style.display = "none";
|
||||||
|
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
|
||||||
|
getVideoView: function() {
|
||||||
|
return this.refs.video;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(){
|
||||||
|
var VideoView = sdk.getComponent('voip.VideoView');
|
||||||
|
return (
|
||||||
|
<VideoView ref="video" onClick={ this.props.onClick }/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
122
src/components/views/voip/IncomingCallBox.js
Normal file
122
src/components/views/voip/IncomingCallBox.js
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
var React = require('react');
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
var CallHandler = require("../../../CallHandler");
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'IncomingCallBox',
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
incomingCall: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onAction: function(payload) {
|
||||||
|
if (payload.action !== 'call_state') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var call = CallHandler.getCall(payload.room_id);
|
||||||
|
if (!call || call.call_state !== 'ringing') {
|
||||||
|
this.setState({
|
||||||
|
incomingCall: null,
|
||||||
|
});
|
||||||
|
this.getRingAudio().pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (call.call_state === "ringing") {
|
||||||
|
this.getRingAudio().load();
|
||||||
|
this.getRingAudio().play();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.getRingAudio().pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
incomingCall: call
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onAnswerClick: function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'answer',
|
||||||
|
room_id: this.state.incomingCall.roomId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onRejectClick: function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'hangup',
|
||||||
|
room_id: this.state.incomingCall.roomId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getRingAudio: function() {
|
||||||
|
return this.refs.ringAudio;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
// NB: This block MUST have a "key" so React doesn't clobber the elements
|
||||||
|
// between in-call / not-in-call.
|
||||||
|
var audioBlock = (
|
||||||
|
<audio ref="ringAudio" key="voip_ring_audio" loop>
|
||||||
|
<source src="media/ring.ogg" type="audio/ogg" />
|
||||||
|
<source src="media/ring.mp3" type="audio/mpeg" />
|
||||||
|
</audio>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.state.incomingCall || !this.state.incomingCall.roomId) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{audioBlock}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var caller = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId).name;
|
||||||
|
return (
|
||||||
|
<div className="mx_IncomingCallBox">
|
||||||
|
{audioBlock}
|
||||||
|
<img className="mx_IncomingCallBox_chevron" src="img/chevron-left.png" width="9" height="16" />
|
||||||
|
<div className="mx_IncomingCallBox_title">
|
||||||
|
Incoming { this.state.incomingCall ? this.state.incomingCall.type : '' } call from { caller }
|
||||||
|
</div>
|
||||||
|
<div className="mx_IncomingCallBox_buttons">
|
||||||
|
<div className="mx_IncomingCallBox_buttons_cell">
|
||||||
|
<div className="mx_IncomingCallBox_buttons_decline" onClick={this.onRejectClick}>
|
||||||
|
Decline
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx_IncomingCallBox_buttons_cell">
|
||||||
|
<div className="mx_IncomingCallBox_buttons_accept" onClick={this.onAnswerClick}>
|
||||||
|
Accept
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -18,9 +18,14 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = React.createClass({
|
||||||
propTypes: {
|
displayName: 'VideoFeed',
|
||||||
value: React.PropTypes.number,
|
|
||||||
max: React.PropTypes.number
|
render: function() {
|
||||||
|
return (
|
||||||
|
<video>
|
||||||
|
</video>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
93
src/components/views/voip/VideoView.js
Normal file
93
src/components/views/voip/VideoView.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ReactDOM = require('react-dom');
|
||||||
|
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var dis = require('../../../dispatcher');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'VideoView',
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
dis.register(this.onAction);
|
||||||
|
},
|
||||||
|
|
||||||
|
getRemoteVideoElement: function() {
|
||||||
|
return ReactDOM.findDOMNode(this.refs.remote);
|
||||||
|
},
|
||||||
|
|
||||||
|
getRemoteAudioElement: function() {
|
||||||
|
return this.refs.remoteAudio;
|
||||||
|
},
|
||||||
|
|
||||||
|
getLocalVideoElement: function() {
|
||||||
|
return ReactDOM.findDOMNode(this.refs.local);
|
||||||
|
},
|
||||||
|
|
||||||
|
setContainer: function(c) {
|
||||||
|
this.container = c;
|
||||||
|
},
|
||||||
|
|
||||||
|
onAction: function(payload) {
|
||||||
|
switch (payload.action) {
|
||||||
|
case 'video_fullscreen':
|
||||||
|
if (!this.container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var element = this.container;
|
||||||
|
if (payload.fullscreen) {
|
||||||
|
var requestMethod = (
|
||||||
|
element.requestFullScreen ||
|
||||||
|
element.webkitRequestFullScreen ||
|
||||||
|
element.mozRequestFullScreen ||
|
||||||
|
element.msRequestFullscreen
|
||||||
|
);
|
||||||
|
requestMethod.call(element);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var exitMethod = (
|
||||||
|
document.exitFullscreen ||
|
||||||
|
document.mozCancelFullScreen ||
|
||||||
|
document.webkitExitFullscreen ||
|
||||||
|
document.msExitFullscreen
|
||||||
|
);
|
||||||
|
if (exitMethod) {
|
||||||
|
exitMethod.call(document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var VideoFeed = sdk.getComponent('voip.VideoFeed');
|
||||||
|
return (
|
||||||
|
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
|
||||||
|
<div className="mx_VideoView_remoteVideoFeed">
|
||||||
|
<VideoFeed ref="remote"/>
|
||||||
|
<audio ref="remoteAudio"/>
|
||||||
|
</div>
|
||||||
|
<div className="mx_VideoView_localVideoFeed">
|
||||||
|
<VideoFeed ref="local"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,27 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var dis = require("../../dispatcher");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
onClick: function() {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'logout'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var React = require('react');
|
|
||||||
var MatrixClientPeg = require('../../MatrixClientPeg');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
propTypes: {
|
|
||||||
member: React.PropTypes.object.isRequired,
|
|
||||||
width: React.PropTypes.number,
|
|
||||||
height: React.PropTypes.number,
|
|
||||||
resizeMethod: React.PropTypes.string,
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
resizeMethod: 'crop'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
defaultAvatarUrl: function(member, width, height, resizeMethod) {
|
|
||||||
if (this.skinnedDefaultAvatarUrl) {
|
|
||||||
return this.skinnedDefaultAvatarUrl(member, width, height, resizeMethod);
|
|
||||||
}
|
|
||||||
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9QjNbxSKP4eagAFnTseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAABSwCRWJw31gAAAAASUVORK5CYII=";
|
|
||||||
},
|
|
||||||
|
|
||||||
onError: function(ev) {
|
|
||||||
// don't tightloop if the browser can't load a data url
|
|
||||||
if (ev.target.src == this.defaultAvatarUrl(this.props.member)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
imageUrl: this.defaultAvatarUrl(this.props.member)
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
var url = this.props.member.getAvatarUrl(
|
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
|
||||||
this.props.width,
|
|
||||||
this.props.height,
|
|
||||||
this.props.resizeMethod,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
if (!url) {
|
|
||||||
url = this.defaultAvatarUrl(
|
|
||||||
this.props.member,
|
|
||||||
this.props.width,
|
|
||||||
this.props.height,
|
|
||||||
this.props.resizeMethod
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
imageUrl: url
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var React = require('react');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
propTypes: {
|
|
||||||
// Specifying a homeserver will make magical things happen when you,
|
|
||||||
// e.g. start typing in the room alias box.
|
|
||||||
homeserver: React.PropTypes.string,
|
|
||||||
alias: React.PropTypes.string,
|
|
||||||
onChange: React.PropTypes.func,
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
onChange: function() {},
|
|
||||||
alias: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getAliasLocalpart: function() {
|
|
||||||
var room_alias = this.props.alias;
|
|
||||||
|
|
||||||
if (room_alias && this.props.homeserver) {
|
|
||||||
var suffix = ":" + this.props.homeserver;
|
|
||||||
if (room_alias.startsWith("#") && room_alias.endsWith(suffix)) {
|
|
||||||
room_alias = room_alias.slice(1, -suffix.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return room_alias;
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,70 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var React = require('react');
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
propTypes: {
|
|
||||||
initialAvatarUrl: React.PropTypes.string,
|
|
||||||
room: React.PropTypes.object,
|
|
||||||
},
|
|
||||||
|
|
||||||
Phases: {
|
|
||||||
Display: "display",
|
|
||||||
Uploading: "uploading",
|
|
||||||
Error: "error",
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
avatarUrl: this.props.initialAvatarUrl,
|
|
||||||
phase: this.Phases.Display,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setAvatarFromFile: function(file) {
|
|
||||||
var newUrl = null;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
phase: this.Phases.Uploading
|
|
||||||
});
|
|
||||||
var self = this;
|
|
||||||
MatrixClientPeg.get().uploadContent(file).then(function(url) {
|
|
||||||
newUrl = url;
|
|
||||||
if (self.props.room) {
|
|
||||||
return MatrixClientPeg.get().sendStateEvent(
|
|
||||||
self.props.room.roomId,
|
|
||||||
'm.room.avatar',
|
|
||||||
{url: url},
|
|
||||||
''
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return MatrixClientPeg.get().setAvatarUrl(url);
|
|
||||||
}
|
|
||||||
}).done(function() {
|
|
||||||
self.setState({
|
|
||||||
phase: self.Phases.Display,
|
|
||||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl)
|
|
||||||
});
|
|
||||||
}, function(error) {
|
|
||||||
self.setState({
|
|
||||||
phase: self.Phases.Error
|
|
||||||
});
|
|
||||||
self.onError(error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var React = require('react');
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
propTypes: {
|
|
||||||
onFinished: React.PropTypes.func,
|
|
||||||
},
|
|
||||||
|
|
||||||
Phases: {
|
|
||||||
Edit: "edit",
|
|
||||||
Uploading: "uploading",
|
|
||||||
Error: "error",
|
|
||||||
Success: "Success"
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
onFinished: function() {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
phase: this.Phases.Edit,
|
|
||||||
errorString: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
changePassword: function(old_password, new_password) {
|
|
||||||
var cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
var authDict = {
|
|
||||||
type: 'm.login.password',
|
|
||||||
user: cli.credentials.userId,
|
|
||||||
password: old_password
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
phase: this.Phases.Uploading,
|
|
||||||
errorString: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
var d = cli.setPassword(authDict, new_password);
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
d.then(function() {
|
|
||||||
self.setState({
|
|
||||||
phase: self.Phases.Success,
|
|
||||||
errorString: '',
|
|
||||||
})
|
|
||||||
}, function(err) {
|
|
||||||
self.setState({
|
|
||||||
phase: self.Phases.Error,
|
|
||||||
errorString: err.toString()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
shouldHighlight: function() {
|
|
||||||
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
|
|
||||||
if (!actions || !actions.tweaks) { return false; }
|
|
||||||
return actions.tweaks.highlight;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var linkify = require('linkifyjs');
|
|
||||||
var linkifyElement = require('linkifyjs/element');
|
|
||||||
var linkifyMatrix = require('../../linkify-matrix');
|
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
componentDidMount: function() {
|
|
||||||
linkifyElement(this.refs.content, linkifyMatrix.options);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var linkify = require('linkifyjs');
|
|
||||||
var linkifyElement = require('linkifyjs/element');
|
|
||||||
var linkifyMatrix = require('../../linkify-matrix.js');
|
|
||||||
linkifyMatrix(linkify);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
componentDidMount: function() {
|
|
||||||
linkifyElement(this.refs.content, linkifyMatrix.options);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,59 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var dis = require("../../dispatcher");
|
|
||||||
var Modal = require("../../Modal");
|
|
||||||
var sdk = require('../../index.js');
|
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getInitialState: function() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
onLeaveClick: function() {
|
|
||||||
var QuestionDialog = sdk.getComponent("organisms.QuestionDialog");
|
|
||||||
|
|
||||||
var roomId = this.props.member.roomId;
|
|
||||||
Modal.createDialog(QuestionDialog, {
|
|
||||||
title: "Leave room",
|
|
||||||
description: "Are you sure you want to leave the room?",
|
|
||||||
onFinished: function(should_leave) {
|
|
||||||
if (should_leave) {
|
|
||||||
var d = MatrixClientPeg.get().leave(roomId);
|
|
||||||
|
|
||||||
// FIXME: controller shouldn't be loading a view :(
|
|
||||||
var Loader = sdk.getComponent("atoms.Spinner");
|
|
||||||
var modal = Modal.createDialog(Loader);
|
|
||||||
|
|
||||||
d.then(function() {
|
|
||||||
modal.close();
|
|
||||||
dis.dispatch({action: 'view_next_room'});
|
|
||||||
}, function(err) {
|
|
||||||
modal.close();
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: "Failed to leave room",
|
|
||||||
description: err.toString()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,118 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* State vars:
|
|
||||||
* this.state.call_state = the UI state of the call (see CallHandler)
|
|
||||||
*/
|
|
||||||
|
|
||||||
var React = require('react');
|
|
||||||
var dis = require("../../dispatcher");
|
|
||||||
var CallHandler = require("../../CallHandler");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
propTypes: {
|
|
||||||
room: React.PropTypes.object,
|
|
||||||
editing: React.PropTypes.bool,
|
|
||||||
onSettingsClick: React.PropTypes.func,
|
|
||||||
onSaveClick: React.PropTypes.func,
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
editing: false,
|
|
||||||
onSettingsClick: function() {},
|
|
||||||
onSaveClick: function() {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
|
||||||
if (this.props.room) {
|
|
||||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
|
||||||
var callState = call ? call.call_state : "ended";
|
|
||||||
this.setState({
|
|
||||||
call_state: callState
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
dis.unregister(this.dispatcherRef);
|
|
||||||
},
|
|
||||||
|
|
||||||
onAction: function(payload) {
|
|
||||||
// don't filter out payloads for room IDs other than props.room because
|
|
||||||
// we may be interested in the conf 1:1 room
|
|
||||||
if (payload.action !== 'call_state' || !payload.room_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var call = CallHandler.getCallForRoom(payload.room_id);
|
|
||||||
var callState = call ? call.call_state : "ended";
|
|
||||||
this.setState({
|
|
||||||
call_state: callState
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onVideoClick: function(e) {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'place_call',
|
|
||||||
type: e.shiftKey ? "screensharing" : "video",
|
|
||||||
room_id: this.props.room.roomId
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onVoiceClick: function() {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'place_call',
|
|
||||||
type: "voice",
|
|
||||||
room_id: this.props.room.roomId
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onHangupClick: function() {
|
|
||||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
|
||||||
if (!call) { return; }
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'hangup',
|
|
||||||
// hangup the call for this room, which may not be the room in props
|
|
||||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
|
||||||
room_id: call.roomId
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onMuteAudioClick: function() {
|
|
||||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
|
||||||
if (!call) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var newState = !call.isMicrophoneMuted();
|
|
||||||
call.setMicrophoneMuted(newState);
|
|
||||||
this.setState({
|
|
||||||
audioMuted: newState
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onMuteVideoClick: function() {
|
|
||||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
|
||||||
if (!call) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var newState = !call.isLocalVideoMuted();
|
|
||||||
call.setLocalVideoMuted(newState);
|
|
||||||
this.setState({
|
|
||||||
videoMuted: newState
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var dis = require("../../dispatcher");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
onClick: function() {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: this.props.room.roomId
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var React = require("react");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
propTypes: {
|
|
||||||
onHsUrlChanged: React.PropTypes.func,
|
|
||||||
onIsUrlChanged: React.PropTypes.func,
|
|
||||||
defaultHsUrl: React.PropTypes.string,
|
|
||||||
defaultIsUrl: React.PropTypes.string
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
onHsUrlChanged: function() {},
|
|
||||||
onIsUrlChanged: function() {},
|
|
||||||
defaultHsUrl: 'https://matrix.org/',
|
|
||||||
defaultIsUrl: 'https://matrix.org/'
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
hs_url: this.props.defaultHsUrl,
|
|
||||||
is_url: this.props.defaultIsUrl,
|
|
||||||
original_hs_url: this.props.defaultHsUrl,
|
|
||||||
original_is_url: this.props.defaultIsUrl,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hsChanged: function(ev) {
|
|
||||||
this.setState({hs_url: ev.target.value}, function() {
|
|
||||||
this.props.onHsUrlChanged(this.state.hs_url);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// XXX: horrible naming due to potential confusion between the word 'is' and the acronym 'IS'
|
|
||||||
isChanged: function(ev) {
|
|
||||||
this.setState({is_url: ev.target.value}, function() {
|
|
||||||
this.props.onIsUrlChanged(this.state.is_url);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getHsUrl: function() {
|
|
||||||
return this.state.hs_url;
|
|
||||||
},
|
|
||||||
|
|
||||||
getIsUrl: function() {
|
|
||||||
return this.state.is_url;
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,73 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var dis = require("../../../dispatcher");
|
|
||||||
var CallHandler = require("../../../CallHandler");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
componentDidMount: function() {
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
dis.unregister(this.dispatcherRef);
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
incomingCall: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onAction: function(payload) {
|
|
||||||
if (payload.action !== 'call_state') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var call = CallHandler.getCall(payload.room_id);
|
|
||||||
if (!call || call.call_state !== 'ringing') {
|
|
||||||
this.setState({
|
|
||||||
incomingCall: null,
|
|
||||||
});
|
|
||||||
this.getRingAudio().pause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (call.call_state === "ringing") {
|
|
||||||
this.getRingAudio().load();
|
|
||||||
this.getRingAudio().play();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.getRingAudio().pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
incomingCall: call
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onAnswerClick: function() {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'answer',
|
|
||||||
room_id: this.state.incomingCall.roomId
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onRejectClick: function() {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'hangup',
|
|
||||||
room_id: this.state.incomingCall.roomId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,135 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var React = require("react");
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
||||||
var PresetValues = require('../atoms/create_room/Presets').Presets;
|
|
||||||
var q = require('q');
|
|
||||||
var encryption = require("../../encryption");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
propTypes: {
|
|
||||||
onRoomCreated: React.PropTypes.func,
|
|
||||||
},
|
|
||||||
|
|
||||||
phases: {
|
|
||||||
CONFIG: "CONFIG", // We're waiting for user to configure and hit create.
|
|
||||||
CREATING: "CREATING", // We're sending the request.
|
|
||||||
CREATED: "CREATED", // We successfully created the room.
|
|
||||||
ERROR: "ERROR", // There was an error while trying to create room.
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
onRoomCreated: function() {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
phase: this.phases.CONFIG,
|
|
||||||
error_string: "",
|
|
||||||
is_private: true,
|
|
||||||
share_history: false,
|
|
||||||
default_preset: PresetValues.PrivateChat,
|
|
||||||
topic: '',
|
|
||||||
room_name: '',
|
|
||||||
invited_users: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
onCreateRoom: function() {
|
|
||||||
var options = {};
|
|
||||||
|
|
||||||
if (this.state.room_name) {
|
|
||||||
options.name = this.state.room_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.topic) {
|
|
||||||
options.topic = this.state.topic;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.preset) {
|
|
||||||
if (this.state.preset != PresetValues.Custom) {
|
|
||||||
options.preset = this.state.preset;
|
|
||||||
} else {
|
|
||||||
options.initial_state = [
|
|
||||||
{
|
|
||||||
type: "m.room.join_rules",
|
|
||||||
content: {
|
|
||||||
"join_rule": this.state.is_private ? "invite" : "public"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "m.room.history_visibility",
|
|
||||||
content: {
|
|
||||||
"history_visibility": this.state.share_history ? "shared" : "invited"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
options.invite = this.state.invited_users;
|
|
||||||
|
|
||||||
var alias = this.getAliasLocalpart();
|
|
||||||
if (alias) {
|
|
||||||
options.room_alias_name = alias;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cli = MatrixClientPeg.get();
|
|
||||||
if (!cli) {
|
|
||||||
// TODO: Error.
|
|
||||||
console.error("Cannot create room: No matrix client.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var deferred = cli.createRoom(options);
|
|
||||||
|
|
||||||
var response;
|
|
||||||
|
|
||||||
if (this.state.encrypt) {
|
|
||||||
deferred = deferred.then(function(res) {
|
|
||||||
response = res;
|
|
||||||
return encryption.enableEncryption(
|
|
||||||
cli, response.room_id, options.invite
|
|
||||||
);
|
|
||||||
}).then(function() {
|
|
||||||
return q(response) }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
phase: this.phases.CREATING,
|
|
||||||
});
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
deferred.then(function (resp) {
|
|
||||||
self.setState({
|
|
||||||
phase: self.phases.CREATED,
|
|
||||||
});
|
|
||||||
self.props.onRoomCreated(resp.room_id);
|
|
||||||
}, function(err) {
|
|
||||||
self.setState({
|
|
||||||
phase: self.phases.ERROR,
|
|
||||||
error_string: err.toString(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,124 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var React = require("react");
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
||||||
var RoomListSorter = require("../../RoomListSorter");
|
|
||||||
|
|
||||||
var sdk = require('../../index');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
componentWillMount: function() {
|
|
||||||
var cli = MatrixClientPeg.get();
|
|
||||||
cli.on("Room", this.onRoom);
|
|
||||||
cli.on("Room.timeline", this.onRoomTimeline);
|
|
||||||
cli.on("Room.name", this.onRoomName);
|
|
||||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
|
||||||
|
|
||||||
var rooms = this.getRoomList();
|
|
||||||
this.setState({
|
|
||||||
roomList: rooms,
|
|
||||||
activityMap: {}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
if (MatrixClientPeg.get()) {
|
|
||||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
|
||||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
|
||||||
this.state.activityMap[newProps.selectedRoom] = undefined;
|
|
||||||
this.setState({
|
|
||||||
activityMap: this.state.activityMap
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onRoom: function(room) {
|
|
||||||
this.refreshRoomList();
|
|
||||||
},
|
|
||||||
|
|
||||||
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
|
||||||
if (toStartOfTimeline) return;
|
|
||||||
|
|
||||||
var newState = {
|
|
||||||
roomList: this.getRoomList()
|
|
||||||
};
|
|
||||||
if (
|
|
||||||
room.roomId != this.props.selectedRoom &&
|
|
||||||
ev.getSender() != MatrixClientPeg.get().credentials.userId)
|
|
||||||
{
|
|
||||||
var hl = 1;
|
|
||||||
|
|
||||||
var actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
|
||||||
if (actions && actions.tweaks && actions.tweaks.highlight) {
|
|
||||||
hl = 2;
|
|
||||||
}
|
|
||||||
// obviously this won't deep copy but this shouldn't be necessary
|
|
||||||
var amap = this.state.activityMap;
|
|
||||||
amap[room.roomId] = Math.max(amap[room.roomId] || 0, hl);
|
|
||||||
|
|
||||||
newState.activityMap = amap;
|
|
||||||
}
|
|
||||||
this.setState(newState);
|
|
||||||
},
|
|
||||||
|
|
||||||
onRoomName: function(room) {
|
|
||||||
this.refreshRoomList();
|
|
||||||
},
|
|
||||||
|
|
||||||
onRoomStateEvents: function(ev, state) {
|
|
||||||
this.refreshRoomList();
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshRoomList: function() {
|
|
||||||
var rooms = this.getRoomList();
|
|
||||||
this.setState({
|
|
||||||
roomList: rooms
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getRoomList: function() {
|
|
||||||
return RoomListSorter.mostRecentActivityFirst(
|
|
||||||
MatrixClientPeg.get().getRooms().filter(function(room) {
|
|
||||||
var member = room.getMember(MatrixClientPeg.get().credentials.userId);
|
|
||||||
return member && (member.membership == "join" || member.membership == "invite");
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
makeRoomTiles: function() {
|
|
||||||
var RoomTile = sdk.getComponent('molecules.RoomTile');
|
|
||||||
var self = this;
|
|
||||||
return this.state.roomList.map(function(room) {
|
|
||||||
var selected = room.roomId == self.props.selectedRoom;
|
|
||||||
return (
|
|
||||||
<RoomTile
|
|
||||||
room={room}
|
|
||||||
key={room.roomId}
|
|
||||||
selected={selected}
|
|
||||||
unread={self.state.activityMap[room.roomId] === 1}
|
|
||||||
highlight={self.state.activityMap[room.roomId] === 2}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,442 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
||||||
var React = require("react");
|
|
||||||
var q = require("q");
|
|
||||||
var ContentMessages = require("../../ContentMessages");
|
|
||||||
var WhoIsTyping = require("../../WhoIsTyping");
|
|
||||||
var Modal = require("../../Modal");
|
|
||||||
var sdk = require('../../index');
|
|
||||||
|
|
||||||
var dis = require("../../dispatcher");
|
|
||||||
|
|
||||||
var PAGINATE_SIZE = 20;
|
|
||||||
var INITIAL_SIZE = 20;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
room: this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null,
|
|
||||||
messageCap: INITIAL_SIZE,
|
|
||||||
editingRoomSettings: false,
|
|
||||||
uploadingRoomSettings: false,
|
|
||||||
numUnreadMessages: 0,
|
|
||||||
draggingFile: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
|
||||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
|
||||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
|
||||||
this.atBottom = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
if (this.refs.messageWrapper) {
|
|
||||||
var messageWrapper = this.refs.messageWrapper;
|
|
||||||
messageWrapper.removeEventListener('drop', this.onDrop);
|
|
||||||
messageWrapper.removeEventListener('dragover', this.onDragOver);
|
|
||||||
messageWrapper.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
|
||||||
messageWrapper.removeEventListener('dragend', this.onDragLeaveOrEnd);
|
|
||||||
}
|
|
||||||
dis.unregister(this.dispatcherRef);
|
|
||||||
if (MatrixClientPeg.get()) {
|
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
|
||||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
|
||||||
MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onAction: function(payload) {
|
|
||||||
switch (payload.action) {
|
|
||||||
case 'message_send_failed':
|
|
||||||
case 'message_sent':
|
|
||||||
case 'message_resend_started':
|
|
||||||
this.setState({
|
|
||||||
room: MatrixClientPeg.get().getRoom(this.props.roomId)
|
|
||||||
});
|
|
||||||
this.forceUpdate();
|
|
||||||
break;
|
|
||||||
case 'notifier_enabled':
|
|
||||||
this.forceUpdate();
|
|
||||||
break;
|
|
||||||
case 'call_state':
|
|
||||||
if (this.props.roomId !== payload.room_id) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// scroll to bottom
|
|
||||||
var messageWrapper = this.refs.messageWrapper;
|
|
||||||
if (messageWrapper) {
|
|
||||||
messageWrapper = messageWrapper;
|
|
||||||
messageWrapper.scrollTop = messageWrapper.scrollHeight;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// MatrixRoom still showing the messages from the old room?
|
|
||||||
// Set the key to the room_id. Sadly you can no longer get at
|
|
||||||
// the key from inside the component, or we'd check this in code.
|
|
||||||
/*componentWillReceiveProps: function(props) {
|
|
||||||
},*/
|
|
||||||
|
|
||||||
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
|
||||||
if (!this.isMounted()) return;
|
|
||||||
|
|
||||||
// ignore anything that comes in whilst pagingating: we get one
|
|
||||||
// event for each new matrix event so this would cause a huge
|
|
||||||
// number of UI updates. Just update the UI when the paginate
|
|
||||||
// call returns.
|
|
||||||
if (this.state.paginating) return;
|
|
||||||
|
|
||||||
// no point handling anything while we're waiting for the join to finish:
|
|
||||||
// we'll only be showing a spinner.
|
|
||||||
if (this.state.joining) return;
|
|
||||||
if (room.roomId != this.props.roomId) return;
|
|
||||||
|
|
||||||
if (this.refs.messageWrapper) {
|
|
||||||
var messageWrapper = this.refs.messageWrapper;
|
|
||||||
this.atBottom = (
|
|
||||||
messageWrapper.scrollHeight - messageWrapper.scrollTop <=
|
|
||||||
(messageWrapper.clientHeight + 150)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentUnread = this.state.numUnreadMessages;
|
|
||||||
if (!toStartOfTimeline &&
|
|
||||||
(ev.getSender() !== MatrixClientPeg.get().credentials.userId)) {
|
|
||||||
// update unread count when scrolled up
|
|
||||||
if (this.atBottom) {
|
|
||||||
currentUnread = 0;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
currentUnread += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
room: MatrixClientPeg.get().getRoom(this.props.roomId),
|
|
||||||
numUnreadMessages: currentUnread
|
|
||||||
});
|
|
||||||
|
|
||||||
if (toStartOfTimeline && !this.state.paginating) {
|
|
||||||
this.fillSpace();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onRoomName: function(room) {
|
|
||||||
if (room.roomId == this.props.roomId) {
|
|
||||||
this.setState({
|
|
||||||
room: room
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onRoomMemberTyping: function(ev, member) {
|
|
||||||
this.forceUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
if (this.refs.messageWrapper) {
|
|
||||||
var messageWrapper = this.refs.messageWrapper;
|
|
||||||
|
|
||||||
messageWrapper.addEventListener('drop', this.onDrop);
|
|
||||||
messageWrapper.addEventListener('dragover', this.onDragOver);
|
|
||||||
messageWrapper.addEventListener('dragleave', this.onDragLeaveOrEnd);
|
|
||||||
messageWrapper.addEventListener('dragend', this.onDragLeaveOrEnd);
|
|
||||||
|
|
||||||
messageWrapper.scrollTop = messageWrapper.scrollHeight;
|
|
||||||
|
|
||||||
this.fillSpace();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
|
||||||
if (!this.refs.messageWrapper) return;
|
|
||||||
|
|
||||||
var messageWrapper = this.refs.messageWrapper;
|
|
||||||
|
|
||||||
if (this.state.paginating && !this.waiting_for_paginate) {
|
|
||||||
var heightGained = messageWrapper.scrollHeight - this.oldScrollHeight;
|
|
||||||
messageWrapper.scrollTop += heightGained;
|
|
||||||
this.oldScrollHeight = undefined;
|
|
||||||
if (!this.fillSpace()) {
|
|
||||||
this.setState({paginating: false});
|
|
||||||
}
|
|
||||||
} else if (this.atBottom) {
|
|
||||||
messageWrapper.scrollTop = messageWrapper.scrollHeight;
|
|
||||||
if (this.state.numUnreadMessages !== 0) {
|
|
||||||
this.setState({numUnreadMessages: 0});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
fillSpace: function() {
|
|
||||||
if (!this.refs.messageWrapper) return;
|
|
||||||
var messageWrapper = this.refs.messageWrapper;
|
|
||||||
if (messageWrapper.scrollTop < messageWrapper.clientHeight && this.state.room.oldState.paginationToken) {
|
|
||||||
this.setState({paginating: true});
|
|
||||||
|
|
||||||
this.oldScrollHeight = messageWrapper.scrollHeight;
|
|
||||||
|
|
||||||
if (this.state.messageCap < this.state.room.timeline.length) {
|
|
||||||
this.waiting_for_paginate = false;
|
|
||||||
var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length);
|
|
||||||
this.setState({messageCap: cap, paginating: true});
|
|
||||||
} else {
|
|
||||||
this.waiting_for_paginate = true;
|
|
||||||
var cap = this.state.messageCap + PAGINATE_SIZE;
|
|
||||||
this.setState({messageCap: cap, paginating: true});
|
|
||||||
var self = this;
|
|
||||||
MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(function() {
|
|
||||||
self.waiting_for_paginate = false;
|
|
||||||
if (self.isMounted()) {
|
|
||||||
self.setState({
|
|
||||||
room: MatrixClientPeg.get().getRoom(self.props.roomId)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// wait and set paginating to false when the component updates
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
onJoinButtonClicked: function(ev) {
|
|
||||||
var self = this;
|
|
||||||
MatrixClientPeg.get().joinRoom(this.props.roomId).then(function() {
|
|
||||||
self.setState({
|
|
||||||
joining: false,
|
|
||||||
room: MatrixClientPeg.get().getRoom(self.props.roomId)
|
|
||||||
});
|
|
||||||
}, function(error) {
|
|
||||||
self.setState({
|
|
||||||
joining: false,
|
|
||||||
joinError: error
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.setState({
|
|
||||||
joining: true
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onMessageListScroll: function(ev) {
|
|
||||||
if (this.refs.messageWrapper) {
|
|
||||||
var messageWrapper = this.refs.messageWrapper;
|
|
||||||
var wasAtBottom = this.atBottom;
|
|
||||||
this.atBottom = messageWrapper.scrollHeight - messageWrapper.scrollTop <= messageWrapper.clientHeight;
|
|
||||||
if (this.atBottom && !wasAtBottom) {
|
|
||||||
this.forceUpdate(); // remove unread msg count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!this.state.paginating) this.fillSpace();
|
|
||||||
},
|
|
||||||
|
|
||||||
onDragOver: function(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
ev.dataTransfer.dropEffect = 'none';
|
|
||||||
|
|
||||||
var items = ev.dataTransfer.items;
|
|
||||||
if (items.length == 1) {
|
|
||||||
if (items[0].kind == 'file') {
|
|
||||||
this.setState({ draggingFile : true });
|
|
||||||
ev.dataTransfer.dropEffect = 'copy';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onDrop: function(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
this.setState({ draggingFile : false });
|
|
||||||
var files = ev.dataTransfer.files;
|
|
||||||
if (files.length == 1) {
|
|
||||||
this.uploadFile(files[0]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onDragLeaveOrEnd: function(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
this.setState({ draggingFile : false });
|
|
||||||
},
|
|
||||||
|
|
||||||
uploadFile: function(file) {
|
|
||||||
this.setState({
|
|
||||||
upload: {
|
|
||||||
fileName: file.name,
|
|
||||||
uploadedBytes: 0,
|
|
||||||
totalBytes: file.size
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var self = this;
|
|
||||||
ContentMessages.sendContentToRoom(
|
|
||||||
file, this.props.roomId, MatrixClientPeg.get()
|
|
||||||
).progress(function(ev) {
|
|
||||||
//console.log("Upload: "+ev.loaded+" / "+ev.total);
|
|
||||||
self.setState({
|
|
||||||
upload: {
|
|
||||||
fileName: file.name,
|
|
||||||
uploadedBytes: ev.loaded,
|
|
||||||
totalBytes: ev.total
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).finally(function() {
|
|
||||||
self.setState({
|
|
||||||
upload: undefined
|
|
||||||
});
|
|
||||||
}).done(undefined, function() {
|
|
||||||
// display error message
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getWhoIsTypingString: function() {
|
|
||||||
return WhoIsTyping.whoIsTypingString(this.state.room);
|
|
||||||
},
|
|
||||||
|
|
||||||
getEventTiles: function() {
|
|
||||||
var ret = [];
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
var EventTile = sdk.getComponent('molecules.EventTile');
|
|
||||||
|
|
||||||
for (var i = this.state.room.timeline.length-1; i >= 0 && count < this.state.messageCap; --i) {
|
|
||||||
var mxEv = this.state.room.timeline[i];
|
|
||||||
|
|
||||||
if (!EventTile.supportsEventType(mxEv.getType())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var continuation = false;
|
|
||||||
var last = false;
|
|
||||||
if (i == this.state.room.timeline.length - 1) {
|
|
||||||
last = true;
|
|
||||||
}
|
|
||||||
if (i > 0 && count < this.state.messageCap - 1) {
|
|
||||||
if (this.state.room.timeline[i].sender &&
|
|
||||||
this.state.room.timeline[i - 1].sender &&
|
|
||||||
(this.state.room.timeline[i].sender.userId ===
|
|
||||||
this.state.room.timeline[i - 1].sender.userId) &&
|
|
||||||
(this.state.room.timeline[i].getType() ==
|
|
||||||
this.state.room.timeline[i - 1].getType())
|
|
||||||
)
|
|
||||||
{
|
|
||||||
continuation = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret.unshift(
|
|
||||||
<li key={mxEv.getId()}><EventTile mxEvent={mxEv} continuation={continuation} last={last}/></li>
|
|
||||||
);
|
|
||||||
++count;
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
uploadNewState: function(new_name, new_topic, new_join_rule, new_history_visibility, new_power_levels) {
|
|
||||||
var old_name = this.state.room.name;
|
|
||||||
|
|
||||||
var old_topic = this.state.room.currentState.getStateEvents('m.room.topic', '');
|
|
||||||
if (old_topic) {
|
|
||||||
old_topic = old_topic.getContent().topic;
|
|
||||||
} else {
|
|
||||||
old_topic = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
var old_join_rule = this.state.room.currentState.getStateEvents('m.room.join_rules', '');
|
|
||||||
if (old_join_rule) {
|
|
||||||
old_join_rule = old_join_rule.getContent().join_rule;
|
|
||||||
} else {
|
|
||||||
old_join_rule = "invite";
|
|
||||||
}
|
|
||||||
|
|
||||||
var old_history_visibility = this.state.room.currentState.getStateEvents('m.room.history_visibility', '');
|
|
||||||
if (old_history_visibility) {
|
|
||||||
old_history_visibility = old_history_visibility.getContent().history_visibility;
|
|
||||||
} else {
|
|
||||||
old_history_visibility = "shared";
|
|
||||||
}
|
|
||||||
|
|
||||||
var deferreds = [];
|
|
||||||
|
|
||||||
if (old_name != new_name && new_name != undefined && new_name) {
|
|
||||||
deferreds.push(
|
|
||||||
MatrixClientPeg.get().setRoomName(this.state.room.roomId, new_name)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (old_topic != new_topic && new_topic != undefined) {
|
|
||||||
deferreds.push(
|
|
||||||
MatrixClientPeg.get().setRoomTopic(this.state.room.roomId, new_topic)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (old_join_rule != new_join_rule && new_join_rule != undefined) {
|
|
||||||
deferreds.push(
|
|
||||||
MatrixClientPeg.get().sendStateEvent(
|
|
||||||
this.state.room.roomId, "m.room.join_rules", {
|
|
||||||
join_rule: new_join_rule,
|
|
||||||
}, ""
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (old_history_visibility != new_history_visibility && new_history_visibility != undefined) {
|
|
||||||
deferreds.push(
|
|
||||||
MatrixClientPeg.get().sendStateEvent(
|
|
||||||
this.state.room.roomId, "m.room.history_visibility", {
|
|
||||||
history_visibility: new_history_visibility,
|
|
||||||
}, ""
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new_power_levels) {
|
|
||||||
deferreds.push(
|
|
||||||
MatrixClientPeg.get().sendStateEvent(
|
|
||||||
this.state.room.roomId, "m.room.power_levels", new_power_levels, ""
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deferreds.length) {
|
|
||||||
var self = this;
|
|
||||||
q.all(deferreds).fail(function(err) {
|
|
||||||
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
|
||||||
title: "Failed to set state",
|
|
||||||
description: err.toString()
|
|
||||||
});
|
|
||||||
}).finally(function() {
|
|
||||||
self.setState({
|
|
||||||
uploadingRoomSettings: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
editingRoomSettings: false,
|
|
||||||
uploadingRoomSettings: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,116 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
||||||
var dis = require("../../dispatcher");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
step: 'choose_hs',
|
|
||||||
busy: false,
|
|
||||||
currentStep: 0,
|
|
||||||
totalSteps: 1
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
setStep: function(step) {
|
|
||||||
this.setState({ step: step, busy: false });
|
|
||||||
},
|
|
||||||
|
|
||||||
onHSChosen: function() {
|
|
||||||
MatrixClientPeg.replaceUsingUrls(
|
|
||||||
// XXX: why is the controller invoking methods from the view? :( -matthew
|
|
||||||
this.getHsUrl(),
|
|
||||||
this.getIsUrl()
|
|
||||||
);
|
|
||||||
this.setState({
|
|
||||||
hs_url: this.getHsUrl(),
|
|
||||||
is_url: this.getIsUrl(),
|
|
||||||
});
|
|
||||||
this.setStep("fetch_stages");
|
|
||||||
var cli = MatrixClientPeg.get();
|
|
||||||
this.setState({
|
|
||||||
busy: true,
|
|
||||||
errorText: "",
|
|
||||||
});
|
|
||||||
var self = this;
|
|
||||||
cli.loginFlows().done(function(result) {
|
|
||||||
self.setState({
|
|
||||||
flows: result.flows,
|
|
||||||
currentStep: 1,
|
|
||||||
totalSteps: result.flows.length+1
|
|
||||||
});
|
|
||||||
self.setStep('stage_'+result.flows[0].type);
|
|
||||||
}, function(error) {
|
|
||||||
self.setStep("choose_hs");
|
|
||||||
self.setState({errorText: 'Unable to contact the given home server'});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onUserPassEntered: function(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.setState({
|
|
||||||
busy: true,
|
|
||||||
errorText: "",
|
|
||||||
});
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var formVals = this.getFormVals();
|
|
||||||
|
|
||||||
var loginParams = {
|
|
||||||
password: formVals.password
|
|
||||||
};
|
|
||||||
if (formVals.username.indexOf('@') > 0) {
|
|
||||||
loginParams.medium = 'email';
|
|
||||||
loginParams.address = formVals.username;
|
|
||||||
} else {
|
|
||||||
loginParams.user = formVals.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
MatrixClientPeg.get().login('m.login.password', loginParams).done(function(data) {
|
|
||||||
MatrixClientPeg.replaceUsingAccessToken(
|
|
||||||
self.state.hs_url, self.state.is_url,
|
|
||||||
data.user_id, data.access_token
|
|
||||||
);
|
|
||||||
if (self.props.onLoggedIn) {
|
|
||||||
self.props.onLoggedIn();
|
|
||||||
}
|
|
||||||
}, function(error) {
|
|
||||||
self.setStep("stage_m.login.password");
|
|
||||||
if (error.httpStatus == 400 && loginParams.medium) {
|
|
||||||
self.setState({errorText: 'This Home Server does not support login using email address.'});
|
|
||||||
}
|
|
||||||
else if (error.httpStatus === 403) {
|
|
||||||
self.setState({errorText: 'Incorrect username and/or password.'});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.setState({
|
|
||||||
errorText: 'There was a problem logging in. (HTTP ' + error.httpStatus + ")"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
showRegister: function(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'start_registration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,352 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
|
||||||
var dis = require("../../dispatcher");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
FieldErrors: {
|
|
||||||
PasswordMismatch: 'PasswordMismatch',
|
|
||||||
TooShort: 'TooShort',
|
|
||||||
Missing: 'Missing',
|
|
||||||
InUse: 'InUse',
|
|
||||||
Length: 'Length'
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
step: 'initial',
|
|
||||||
busy: false,
|
|
||||||
currentStep: 0,
|
|
||||||
totalSteps: 1
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillMount: function() {
|
|
||||||
this.savedParams = {
|
|
||||||
email: '',
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
confirmPassword: ''
|
|
||||||
};
|
|
||||||
this.readNewProps();
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps: function() {
|
|
||||||
this.readNewProps();
|
|
||||||
},
|
|
||||||
|
|
||||||
readNewProps: function() {
|
|
||||||
if (this.props.clientSecret && this.props.hsUrl &&
|
|
||||||
this.props.isUrl && this.props.sessionId &&
|
|
||||||
this.props.idSid) {
|
|
||||||
this.authSessionId = this.props.sessionId;
|
|
||||||
MatrixClientPeg.replaceUsingUrls(
|
|
||||||
this.props.hsUrl,
|
|
||||||
this.props.isUrl
|
|
||||||
);
|
|
||||||
this.setState({
|
|
||||||
hs_url: this.props.hsUrl,
|
|
||||||
is_url: this.props.isUrl
|
|
||||||
});
|
|
||||||
this.savedParams = {client_secret: this.props.clientSecret};
|
|
||||||
this.setState({busy: true});
|
|
||||||
|
|
||||||
var isLocation = document.createElement('a');
|
|
||||||
isLocation.href = this.props.isUrl;
|
|
||||||
|
|
||||||
var auth = {
|
|
||||||
type: 'm.login.email.identity',
|
|
||||||
threepid_creds: {
|
|
||||||
sid: this.props.idSid,
|
|
||||||
client_secret: this.savedParams.client_secret,
|
|
||||||
id_server: isLocation.host
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.tryRegister(auth);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
|
||||||
// Just putting a script tag into the returned jsx doesn't work, annoyingly,
|
|
||||||
// so we do this instead.
|
|
||||||
if (this.refs.recaptchaContainer) {
|
|
||||||
var scriptTag = document.createElement('script');
|
|
||||||
window.mx_on_recaptcha_loaded = this.onCaptchaLoaded;
|
|
||||||
scriptTag.setAttribute('src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit");
|
|
||||||
this.refs.recaptchaContainer.appendChild(scriptTag);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setStep: function(step) {
|
|
||||||
this.setState({ step: step, errorText: '', busy: false });
|
|
||||||
},
|
|
||||||
|
|
||||||
getSupportedStageTypes: function() {
|
|
||||||
return ['m.login.email.identity', 'm.login.recaptcha'];
|
|
||||||
},
|
|
||||||
|
|
||||||
chooseFlow: function(flows) {
|
|
||||||
// this is fairly simple right now
|
|
||||||
var supportedTypes = this.getSupportedStageTypes();
|
|
||||||
|
|
||||||
var emailFlow = null;
|
|
||||||
var otherFlow = null;
|
|
||||||
for (var flowI = 0; flowI < flows.length; ++flowI) {
|
|
||||||
var flow = flows[flowI];
|
|
||||||
var flowHasEmail = false;
|
|
||||||
var flowSupported = true;
|
|
||||||
for (var stageI = 0; stageI < flow.stages.length; ++stageI) {
|
|
||||||
var stage = flow.stages[stageI];
|
|
||||||
|
|
||||||
if (supportedTypes.indexOf(stage) == -1) {
|
|
||||||
flowSupported = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stage == 'm.login.email.identity') {
|
|
||||||
flowHasEmail = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (flowSupported) {
|
|
||||||
if (flowHasEmail) {
|
|
||||||
emailFlow = flow;
|
|
||||||
} else {
|
|
||||||
otherFlow = flow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.savedParams.email != '' ||
|
|
||||||
this.completedStages.indexOf('m.login.email.identity') > -1
|
|
||||||
) {
|
|
||||||
return emailFlow;
|
|
||||||
} else {
|
|
||||||
return otherFlow;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
firstUncompletedStageIndex: function(flow) {
|
|
||||||
if (this.completedStages === undefined) return 0;
|
|
||||||
for (var i = 0; i < flow.stages.length; ++i) {
|
|
||||||
if (this.completedStages.indexOf(flow.stages[i]) == -1) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
numCompletedStages: function(flow) {
|
|
||||||
if (this.completedStages === undefined) return 0;
|
|
||||||
var nCompleted = 0;
|
|
||||||
for (var i = 0; i < flow.stages.length; ++i) {
|
|
||||||
if (this.completedStages.indexOf(flow.stages[i]) > -1) {
|
|
||||||
++nCompleted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nCompleted;
|
|
||||||
},
|
|
||||||
|
|
||||||
onInitialStageSubmit: function(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
var formVals = this.getRegFormVals();
|
|
||||||
this.savedParams = formVals;
|
|
||||||
|
|
||||||
var badFields = {};
|
|
||||||
if (formVals.password != formVals.confirmPassword) {
|
|
||||||
badFields.confirmPassword = this.FieldErrors.PasswordMismatch;
|
|
||||||
}
|
|
||||||
if (formVals.password == '') {
|
|
||||||
badFields.password = this.FieldErrors.Missing;
|
|
||||||
} else if (formVals.password.length < 6) {
|
|
||||||
badFields.password = this.FieldErrors.Length;
|
|
||||||
}
|
|
||||||
if (formVals.username == '') {
|
|
||||||
badFields.username = this.FieldErrors.Missing;
|
|
||||||
}
|
|
||||||
if (formVals.email == '') {
|
|
||||||
badFields.email = this.FieldErrors.Missing;
|
|
||||||
}
|
|
||||||
if (Object.keys(badFields).length > 0) {
|
|
||||||
this.onBadFields(badFields);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MatrixClientPeg.replaceUsingUrls(
|
|
||||||
this.getHsUrl(),
|
|
||||||
this.getIsUrl()
|
|
||||||
);
|
|
||||||
this.setState({
|
|
||||||
hs_url: this.getHsUrl(),
|
|
||||||
is_url: this.getIsUrl()
|
|
||||||
});
|
|
||||||
this.setState({busy: true});
|
|
||||||
|
|
||||||
this.tryRegister();
|
|
||||||
},
|
|
||||||
|
|
||||||
startStage: function(stageName) {
|
|
||||||
var self = this;
|
|
||||||
this.setStep('stage_'+stageName);
|
|
||||||
switch(stageName) {
|
|
||||||
case 'm.login.email.identity':
|
|
||||||
self.setState({
|
|
||||||
busy: true
|
|
||||||
});
|
|
||||||
var cli = MatrixClientPeg.get();
|
|
||||||
this.savedParams.client_secret = cli.generateClientSecret();
|
|
||||||
this.savedParams.send_attempt = 1;
|
|
||||||
|
|
||||||
var nextLink = this.props.registrationUrl +
|
|
||||||
'?client_secret=' +
|
|
||||||
encodeURIComponent(this.savedParams.client_secret) +
|
|
||||||
"&hs_url=" +
|
|
||||||
encodeURIComponent(this.state.hs_url) +
|
|
||||||
"&is_url=" +
|
|
||||||
encodeURIComponent(this.state.is_url) +
|
|
||||||
"&session_id=" +
|
|
||||||
encodeURIComponent(this.authSessionId);
|
|
||||||
|
|
||||||
cli.requestEmailToken(
|
|
||||||
this.savedParams.email,
|
|
||||||
this.savedParams.client_secret,
|
|
||||||
this.savedParams.send_attempt,
|
|
||||||
nextLink
|
|
||||||
).done(function(response) {
|
|
||||||
self.setState({
|
|
||||||
busy: false,
|
|
||||||
});
|
|
||||||
self.setStep('stage_m.login.email.identity');
|
|
||||||
}, function(error) {
|
|
||||||
console.error(error);
|
|
||||||
self.setStep('initial');
|
|
||||||
var newState = {busy: false};
|
|
||||||
if (error.errcode == 'THREEPID_IN_USE') {
|
|
||||||
self.onBadFields({email: self.FieldErrors.InUse});
|
|
||||||
} else {
|
|
||||||
newState.errorText = 'Unable to contact the given identity server';
|
|
||||||
}
|
|
||||||
self.setState(newState);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'm.login.recaptcha':
|
|
||||||
if (!this.authParams || !this.authParams['m.login.recaptcha'].public_key) {
|
|
||||||
this.setState({
|
|
||||||
errorText: "This server has not supplied enough information for Recaptcha authentication"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onRegistered: function(user_id, access_token) {
|
|
||||||
MatrixClientPeg.replaceUsingAccessToken(
|
|
||||||
this.state.hs_url, this.state.is_url, user_id, access_token
|
|
||||||
);
|
|
||||||
if (this.props.onLoggedIn) {
|
|
||||||
this.props.onLoggedIn();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onCaptchaLoaded: function() {
|
|
||||||
if (this.refs.recaptchaContainer) {
|
|
||||||
var sitekey = this.authParams['m.login.recaptcha'].public_key;
|
|
||||||
global.grecaptcha.render('mx_recaptcha', {
|
|
||||||
'sitekey': sitekey,
|
|
||||||
'callback': this.onCaptchaDone
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onCaptchaDone: function(captcha_response) {
|
|
||||||
this.tryRegister({
|
|
||||||
type: 'm.login.recaptcha',
|
|
||||||
response: captcha_response
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
tryRegister: function(auth) {
|
|
||||||
var self = this;
|
|
||||||
MatrixClientPeg.get().register(
|
|
||||||
this.savedParams.username,
|
|
||||||
this.savedParams.password,
|
|
||||||
this.authSessionId,
|
|
||||||
auth
|
|
||||||
).done(function(result) {
|
|
||||||
self.onRegistered(result.user_id, result.access_token);
|
|
||||||
}, function(error) {
|
|
||||||
if (error.httpStatus == 401 && error.data.flows) {
|
|
||||||
self.authParams = error.data.params;
|
|
||||||
self.authSessionId = error.data.session;
|
|
||||||
|
|
||||||
self.completedStages = error.data.completed || [];
|
|
||||||
|
|
||||||
var flow = self.chooseFlow(error.data.flows);
|
|
||||||
|
|
||||||
if (flow) {
|
|
||||||
var flowStage = self.firstUncompletedStageIndex(flow);
|
|
||||||
var numDone = self.numCompletedStages(flow);
|
|
||||||
|
|
||||||
self.setState({
|
|
||||||
busy: false,
|
|
||||||
flows: flow,
|
|
||||||
currentStep: 1+numDone,
|
|
||||||
totalSteps: flow.stages.length+1,
|
|
||||||
flowStage: flowStage
|
|
||||||
});
|
|
||||||
self.startStage(flow.stages[flowStage]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.setState({
|
|
||||||
busy: false,
|
|
||||||
errorText: "Unable to register - missing email address?"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(error);
|
|
||||||
self.setStep("initial");
|
|
||||||
var newState = {
|
|
||||||
busy: false,
|
|
||||||
errorText: "Unable to contact the given Home Server"
|
|
||||||
};
|
|
||||||
if (error.name == 'M_USER_IN_USE') {
|
|
||||||
delete newState.errorText;
|
|
||||||
self.onBadFields({
|
|
||||||
username: self.FieldErrors.InUse
|
|
||||||
});
|
|
||||||
} else if (error.httpStatus == 401) {
|
|
||||||
newState.errorText = "Authorisation failed!";
|
|
||||||
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
|
|
||||||
newState.errorText = "Registration failed!";
|
|
||||||
} else if (error.httpStatus >= 500 && error.httpStatus < 600) {
|
|
||||||
newState.errorText = "Server error during registration!";
|
|
||||||
} else if (error.name == "M_MISSING_PARAM") {
|
|
||||||
// The HS hasn't remembered the login params from
|
|
||||||
// the first try when the login email was sent.
|
|
||||||
newState.errorText = "This home server does not support resuming registration.";
|
|
||||||
}
|
|
||||||
self.setState(newState);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
showLogin: function(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'start_login'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -15,16 +15,11 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Skinner = require('./Skinner');
|
var Skinner = require('./Skinner');
|
||||||
var Modulator = require('./Modulator');
|
|
||||||
|
|
||||||
module.exports.loadSkin = function(skinObject) {
|
module.exports.loadSkin = function(skinObject) {
|
||||||
Skinner.load(skinObject);
|
Skinner.load(skinObject);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.loadModule = function(moduleObject) {
|
|
||||||
Modulator.loadModule(moduleObject);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.resetSkin = function() {
|
module.exports.resetSkin = function() {
|
||||||
Skinner.reset();
|
Skinner.reset();
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue