Merge branch 'develop' into matthew/settings

This commit is contained in:
Matthew Hodgson 2015-12-18 00:11:43 +00:00
commit f30b7eec2f
91 changed files with 7929 additions and 2382 deletions

View file

@ -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
limitations under the License.
*/
'use strict';
var MatrixClientPeg = require("../../MatrixClientPeg");
module.exports = {
};

View file

@ -13,6 +13,7 @@
"reskindex": "./reskindex.js"
},
"scripts": {
"reskindex": "reskindex -h header",
"build": "babel src -d lib --source-maps",
"start": "babel src -w -d lib --source-maps",
"clean": "rimraf lib",
@ -23,19 +24,25 @@
"filesize": "^3.1.2",
"flux": "^2.0.3",
"glob": "^5.0.14",
"highlight.js": "^8.9.1",
"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",
"q": "^1.4.1",
"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",
"//depsbuglink": "https://github.com/webpack/webpack/issues/1472",
"devDependencies": {
"babel": "^5.8.23",
"rimraf": "^2.4.3",
"json-loader": "^0.5.3",
"require-json": "0.0.1",
"rimraf": "^2.4.3",
"source-map-loader": "^0.1.5"
}
}

View file

@ -8,40 +8,13 @@ var args = require('optimist').argv;
var header = args.h || args.header;
if (args._.length == 0) {
console.log("No skin given");
process.exit(1);
}
var componentsDir = path.join('src', 'components');
var skin = args._[0];
var componentIndex = path.join('src', 'component-index.js');
try {
fs.accessSync(path.join('src', 'skins', skin), fs.F_OK);
} catch (e) {
console.log("Skin "+skin+" not found: "+e);
process.exit(1);
}
var packageJson = JSON.parse(fs.readFileSync('./package.json'));
var skinfoFile = path.join('src', 'skins', skin, 'skinfo.json');
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);
var strm = fs.createWriteStream(componentIndex);
if (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(" */\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");
strm.write('\n');
var files = glob.sync('**/*.js', {cwd: viewsDir});
var files = glob.sync('**/*.js', {cwd: componentsDir});
for (var i = 0; i < files.length; ++i) {
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.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();

49
src/Avatar.js Normal file
View 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';
}
}

View file

@ -56,12 +56,12 @@ var Modal = require('./Modal');
var sdk = require('./index');
var Matrix = require("matrix-js-sdk");
var dis = require("./dispatcher");
var Modulator = require("./Modulator");
global.mxCalls = {
//room_id: MatrixCall
};
var calls = global.mxCalls;
var ConferenceHandler = null;
function play(audioId) {
// TODO: Attach an invisible element for this instead
@ -115,7 +115,7 @@ function _setCallListeners(call) {
_setCallState(call, call.roomId, "busy");
pause("ringbackAudio");
play("busyAudio");
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Call Timeout",
description: "The remote side failed to pick up."
@ -173,7 +173,7 @@ function _onAction(payload) {
console.error("Unknown conf call type: %s", payload.type);
}
}
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
switch (payload.action) {
case 'place_call':
@ -202,7 +202,7 @@ function _onAction(payload) {
var members = room.getJoinedMembers();
if (members.length <= 1) {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
description: "You cannot place a call with yourself."
});
@ -227,7 +227,7 @@ function _onAction(payload) {
break;
case 'place_conference_call':
console.log("Place conference call in %s", payload.room_id);
if (!Modulator.hasConferenceHandler()) {
if (!ConferenceHandler) {
Modal.createDialog(ErrorDialog, {
description: "Conference calls are not supported in this client"
});
@ -239,7 +239,6 @@ function _onAction(payload) {
});
}
else {
var ConferenceHandler = Modulator.getConferenceHandler();
ConferenceHandler.createNewMatrixCall(
MatrixClientPeg.get(), payload.room_id
).done(function(call) {
@ -295,8 +294,7 @@ var callHandler = {
var call = module.exports.getCall(roomId);
if (call) return call;
if (Modulator.hasConferenceHandler()) {
var ConferenceHandler = Modulator.getConferenceHandler();
if (ConferenceHandler) {
call = ConferenceHandler.getConferenceCallForRoom(roomId);
}
if (call) return call;
@ -317,6 +315,10 @@ var callHandler = {
}
}
return null;
},
setConferenceHandler: function(confHandler) {
ConferenceHandler = confHandler;
}
};
// Only things in here which actually need to be global are the

View file

@ -18,6 +18,10 @@ limitations under the License.
var q = require('q');
var extend = require('./extend');
var dis = require('./dispatcher');
var MatrixClientPeg = require('./MatrixClientPeg');
var sdk = require('./index');
var Modal = require('./Modal');
function infoForImageFile(imageFile) {
var deferred = q.defer();
@ -48,39 +52,108 @@ function infoForImageFile(imageFile) {
return deferred.promise;
}
function sendContentToRoom(file, roomId, matrixClient) {
var content = {
body: file.name,
info: {
size: file.size,
class ContentMessages {
constructor() {
this.inprogress = [];
this.nextId = 0;
}
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
if (file.type) {
content.info.mimetype = file.type;
}
var def = q.defer();
if (file.type.indexOf('image/') == 0) {
content.msgtype = 'm.image';
infoForImageFile(file).then(function(imageInfo) {
extend(content.info, imageInfo);
var def = q.defer();
if (file.type.indexOf('image/') == 0) {
content.msgtype = 'm.image';
infoForImageFile(file).then(function(imageInfo) {
extend(content.info, imageInfo);
def.resolve();
});
} else {
content.msgtype = 'm.file';
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() {
return matrixClient.uploadContent(file);
}).then(function(url) {
content.url = url;
return matrixClient.sendMessage(roomId, content);
});
getCurrentUploads() {
return this.inprogress;
}
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 = {
sendContentToRoom: sendContentToRoom
};
if (global.mx_ContentMessage === undefined) {
global.mx_ContentMessage = new ContentMessages();
}
module.exports = global.mx_ContentMessage;

82
src/ContextualMenu.js Normal file
View 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
View 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
View 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]);
}
},
}

View file

@ -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;

View file

@ -16,8 +16,10 @@ limitations under the License.
'use strict';
var MatrixClientPeg = require("../../MatrixClientPeg");
var dis = require("../../dispatcher");
var MatrixClientPeg = require("./MatrixClientPeg");
var TextForEvent = require('./TextForEvent');
var Avatar = require('./Avatar');
var dis = require("./dispatcher");
/*
* 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() {
this.boundOnRoomTimeline = this.onRoomTimeline.bind(this);
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
this.state = { 'toolbarHidden' : false };
MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange);
this.toolbarHidden = false;
},
stop: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline);
MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange);
}
},
@ -96,7 +167,7 @@ module.exports = {
},
setToolbarHidden: function(hidden) {
this.state.toolbarHidden = hidden;
this.toolbarHidden = hidden;
dis.dispatch({
action: "notifier_enabled",
value: this.isEnabled()
@ -104,11 +175,18 @@ module.exports = {
},
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) {
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 (!this.isEnabled()) {
@ -122,3 +200,8 @@ module.exports = {
}
};
if (!global.mxNotifier) {
global.mxNotifier = Notifier;
}
module.exports = global.mxNotifier;

View file

@ -15,58 +15,54 @@ limitations under the License.
*/
var MatrixClientPeg = require("./MatrixClientPeg");
var dis = require("./dispatcher");
// Time in ms after that a user is considered as unavailable/away
var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
var PRESENCE_STATES = ["online", "offline", "unavailable"];
// The current presence state
var state, timer;
module.exports = {
class Presence {
/**
* Start listening the user activity to evaluate his presence state.
* Any state change will be sent to the Home Server.
*/
start: function() {
var self = this;
start() {
this.running = true;
if (undefined === state) {
// The user is online if they move the mouse or press a key
document.onmousemove = function() { self._resetTimer(); };
document.onkeypress = function() { self._resetTimer(); };
if (undefined === this.state) {
this._resetTimer();
this.dispatcherRef = dis.register(this._onUserActivity.bind(this));
}
},
}
/**
* Stop tracking user activity
*/
stop: function() {
stop() {
this.running = false;
if (timer) {
clearTimeout(timer);
timer = undefined;
if (this.timer) {
clearInterval(this.timer);
this.timer = undefined;
dis.unregister(this.dispatcherRef);
}
state = undefined;
},
this.state = undefined;
}
/**
* Get the current presence state.
* @returns {string} the presence state (see PRESENCE enum)
*/
getState: function() {
return state;
},
getState() {
return this.state;
}
/**
* Set the presence state.
* If the state has changed, the Home Server will be notified.
* @param {string} newState the new presence state (see PRESENCE enum)
*/
setState: function(newState) {
if (newState === state) {
setState(newState) {
if (newState === this.state) {
return;
}
if (PRESENCE_STATES.indexOf(newState) === -1) {
@ -75,33 +71,42 @@ module.exports = {
if (!this.running) {
return;
}
state = newState;
MatrixClientPeg.get().setPresence(state).done(function() {
var old_state = this.state;
this.state = newState;
var self = this;
MatrixClientPeg.get().setPresence(this.state).done(function() {
console.log("Presence: %s", newState);
}, function(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.
* @private
*/
_onUnavailableTimerFire: function() {
_onUnavailableTimerFire() {
this.setState("unavailable");
},
}
_onUserActivity() {
this._resetTimer();
}
/**
* Callback called when the user made an action on the page
* @private
*/
_resetTimer: function() {
_resetTimer() {
var self = this;
this.setState("online");
// Re-arm the timer
clearTimeout(timer);
timer = setTimeout(function() {
clearTimeout(this.timer);
this.timer = setTimeout(function() {
self._onUnavailableTimerFire();
}, UNAVAILABLE_TIME_MS);
}
};
}
module.exports = new Presence();

33
src/Resend.js Normal file
View 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
View 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
View 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
};

View file

@ -32,6 +32,12 @@ class Skinner {
if (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);
}
@ -42,7 +48,24 @@ class Skinner {
"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() {

View file

@ -14,20 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var dis = require("../../dispatcher");
module.exports = {
logOut: function() {
dis.dispatch({action: 'logout'});
if (this.props.onFinished) {
this.props.onFinished();
}
},
cancelPrompt: function() {
if (this.props.onFinished) {
this.props.onFinished();
/**
* Returns true iff this event arriving in a room should affect the room's
* count of unread messages
*/
eventTriggersUnreadCount: function(ev) {
if (ev.getType() == "m.room.member") {
return false;
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
return false;
}
return true;
}
};

68
src/UserActivity.js Normal file
View 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
View 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
View 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
View 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');

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

View file

@ -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
limitations under the License.
*/
var React = require('react');
var Matrix = require("matrix-js-sdk");
var url = require('url');
var MatrixClientPeg = require("../../MatrixClientPeg");
var Notifier = require("../../Notifier");
var ContextualMenu = require("../../ContextualMenu");
var RoomListSorter = require("../../RoomListSorter");
var UserActivity = require("../../UserActivity");
var Presence = require("../../Presence");
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 MatrixTools = require('../../MatrixTools');
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: {
RoomView: "room_view",
UserSettings: "user_settings",
@ -43,6 +61,7 @@ module.exports = {
collapse_lhs: false,
collapse_rhs: false,
ready: false,
width: 10000
};
if (s.logged_in) {
if (MatrixClientPeg.get().getRooms().length) {
@ -62,6 +81,9 @@ module.exports = {
this.startMatrixClient();
}
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);
window.addEventListener("focus", this.onFocus);
if (this.state.logged_in) {
@ -78,12 +100,16 @@ module.exports = {
if (this.onUserClick) {
linkifyMatrix.onUserClick = this.onUserClick;
}
window.addEventListener('resize', this.handleResize);
this.handleResize();
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
document.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("focus", this.onFocus);
window.removeEventListener('resize', this.handleResize);
},
componentDidUpdate: function() {
@ -95,7 +121,6 @@ module.exports = {
onAction: function(payload) {
var roomIndexDelta = 1;
var Notifier = sdk.getComponent('organisms.Notifier');
var self = this;
switch (payload.action) {
@ -104,6 +129,7 @@ module.exports = {
window.localStorage.clear();
}
Notifier.stop();
UserActivity.stop();
Presence.stop();
MatrixClientPeg.get().stopClient();
MatrixClientPeg.get().removeAllListeners();
@ -142,6 +168,11 @@ module.exports = {
});
this.notifyNewScreen('login');
break;
case 'start_post_registration':
this.setState({ // don't clobber logged_in status
screen: 'post_registration'
});
break;
case 'token_login':
if (this.state.logged_in) return;
@ -174,28 +205,38 @@ module.exports = {
});
break;
case 'view_room':
this.focusComposer = true;
var newState = {
currentRoom: payload.room_id,
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 = payload.room_id;
var room = MatrixClientPeg.get().getRoom(payload.room_id);
if (room) {
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
if (theAlias) presentedId = theAlias;
case 'leave_room':
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
var roomId = payload.room_id;
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("elements.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.notifyNewScreen('room/'+presentedId);
newState.ready = true;
}
this.setState(newState);
});
break;
case 'view_room':
this._viewRoom(payload.room_id);
break;
case 'view_prev_room':
roomIndexDelta = -1;
@ -212,11 +253,7 @@ module.exports = {
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
this.focusComposer = true;
this.setState({
currentRoom: allRooms[roomIndex].roomId
});
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
this._viewRoom(allRooms[roomIndex].roomId);
break;
case 'view_indexed_room':
var allRooms = RoomListSorter.mostRecentActivityFirst(
@ -224,11 +261,7 @@ module.exports = {
);
var roomIndex = payload.roomIndex;
if (allRooms[roomIndex]) {
this.focusComposer = true;
this.setState({
currentRoom: allRooms[roomIndex].roomId
});
this.notifyNewScreen('room/'+allRooms[roomIndex].roomId);
this._viewRoom(allRooms[roomIndex].roomId);
}
break;
case 'view_room_alias':
@ -252,21 +285,15 @@ module.exports = {
});
break;
case 'view_user_settings':
this.setState({
page_type: this.PageTypes.UserSettings,
});
this._setPage(this.PageTypes.UserSettings);
this.notifyNewScreen('settings');
break;
case 'view_create_room':
this.setState({
page_type: this.PageTypes.CreateRoom,
});
this._setPage(this.PageTypes.CreateRoom);
this.notifyNewScreen('new');
break;
case 'view_room_directory':
this.setState({
page_type: this.PageTypes.RoomDirectory,
});
this._setPage(this.PageTypes.RoomDirectory);
this.notifyNewScreen('directory');
break;
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({
screen: undefined,
logged_in: true
@ -305,11 +389,14 @@ module.exports = {
},
startMatrixClient: function() {
var Notifier = sdk.getComponent('organisms.Notifier');
var cli = MatrixClientPeg.get();
var self = this;
cli.on('sync', function(state) {
if (self.sdkReady || state !== "PREPARED") { return; }
cli.on('sync', function(state, prevState) {
if (state === "SYNCING" && prevState === "SYNCING") {
return;
}
console.log("MatrixClient sync state => %s", state);
if (state !== "PREPARED") { return; }
self.sdkReady = true;
if (self.starting_room_alias) {
@ -354,8 +441,11 @@ module.exports = {
});
});
Notifier.start();
UserActivity.start();
Presence.start();
cli.startClient();
cli.startClient({
pendingEventOrdering: "end"
});
},
onKeyDown: function(ev) {
@ -420,6 +510,10 @@ module.exports = {
dis.dispatch({
action: 'view_room_directory',
});
} else if (screen == 'post_registration') {
dis.dispatch({
action: 'start_post_registration',
});
} else if (screen.indexOf('room/') == 0) {
var roomString = screen.split('/')[1];
if (roomString[0] == '#') {
@ -441,11 +535,191 @@ module.exports = {
});
}
}
else {
console.error("Unknown screen : %s", screen);
}
},
notifyNewScreen: function(screen) {
if (this.props.onNewScreen) {
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} />
);
}
}
};
});

File diff suppressed because it is too large Load diff

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

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

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

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

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

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

View file

@ -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
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() {
return {
width: 36,
@ -41,10 +36,26 @@ module.exports = {
},
componentWillReceiveProps: function(nextProps) {
this._update();
this.setState({
imageUrl: this._nextUrl()
});
this.refreshImageUrl();
},
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() {
@ -108,5 +119,53 @@ module.exports = {
this.setState({
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} />
}
}
};
});

View file

@ -18,7 +18,8 @@ limitations under the License.
var React = require('react');
module.exports = {
module.exports = React.createClass({
displayName: 'CreateRoomButton',
propTypes: {
onCreateRoom: React.PropTypes.func,
},
@ -32,4 +33,10 @@ module.exports = {
onClick: function() {
this.props.onCreateRoom();
},
};
render: function() {
return (
<button className="mx_CreateRoomButton" onClick={this.onClick}>Create Room</button>
);
}
});

View file

@ -24,7 +24,8 @@ var Presets = {
Custom: "custom",
};
module.exports = {
module.exports = React.createClass({
displayName: 'CreateRoomPresets',
propTypes: {
onChange: React.PropTypes.func,
preset: React.PropTypes.string
@ -37,4 +38,18 @@ module.exports = {
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>
);
}
});

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

View file

@ -14,9 +14,21 @@ See the License for the specific language governing permissions and
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");
module.exports = {
module.exports = React.createClass({
displayName: 'ErrorDialog',
propTypes: {
title: React.PropTypes.string,
button: React.PropTypes.string,
@ -32,4 +44,22 @@ module.exports = {
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>
);
}
});

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

View file

@ -16,7 +16,8 @@ limitations under the License.
var React = require("react");
module.exports = {
module.exports = React.createClass({
displayName: 'QuestionDialog',
propTypes: {
title: React.PropTypes.string,
description: React.PropTypes.string,
@ -33,4 +34,34 @@ module.exports = {
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>
);
}
});

View file

@ -18,7 +18,8 @@ limitations under the License.
var React = require('react');
module.exports = {
module.exports = React.createClass({
displayName: 'EditableText',
propTypes: {
onValueChanged: React.PropTypes.func,
initialValue: React.PropTypes.string,
@ -85,4 +86,54 @@ module.exports = {
onValueChanged: function(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>
);
}
});

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

View file

@ -18,7 +18,9 @@ limitations under the License.
var React = require('react');
module.exports = {
module.exports = React.createClass({
displayName: 'UserSelector',
propTypes: {
onChange: React.PropTypes.func,
selected_users: React.PropTypes.arrayOf(React.PropTypes.string),
@ -42,4 +44,26 @@ module.exports = {
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>
);
}
});

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

View file

@ -16,10 +16,12 @@ limitations under the License.
'use strict';
var MatrixClientPeg = require("../../MatrixClientPeg");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var React = require('react');
var url = require("url");
module.exports = {
module.exports = React.createClass({
displayName: 'CasLogin',
onCasClicked: function(ev) {
var cli = MatrixClientPeg.get();
@ -30,4 +32,12 @@ module.exports = {
window.location.href = casUrl;
},
};
render: function() {
return (
<div>
<button onClick={this.onCasClicked}>Sign in with CAS</button>
</div>
);
}
});

View 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&#39;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>
);
}
});

View file

@ -16,15 +16,16 @@ limitations under the License.
'use strict';
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../linkify-matrix');
var React = require('react');
linkifyMatrix(linkify);
module.exports = React.createClass({
displayName: 'LoginFooter',
module.exports = {
componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options);
render: function() {
return (
<div className="mx_Login_links">
<a href="https://matrix.org">powered by Matrix</a>
</div>
);
}
};
});

View file

@ -14,16 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var React = require('react');
module.exports = {
propTypes: {
room: React.PropTypes.object.isRequired,
},
module.exports = React.createClass({
displayName: 'LoginHeader',
getInitialState: function() {
return {
power_levels_changed: false
};
render: function() {
return (
<div className="mx_Login_logo">
Matrix
</div>
);
}
};
});

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

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

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

View file

@ -16,9 +16,13 @@ limitations under the License.
'use strict';
var React = require('react');
var filesize = require('filesize');
var MatrixClientPeg = require('../../../MatrixClientPeg');
module.exports = React.createClass({
displayName: 'MFileBody',
module.exports = {
presentableTextForFile: function(content) {
var linkText = 'Attachment';
if (content.body && content.body.length > 0) {
@ -39,6 +43,31 @@ module.exports = {
linkText += ' (' + additionals.join(', ') + ')';
}
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>
}
},
});

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

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

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

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

View file

@ -18,24 +18,26 @@ limitations under the License.
var React = require('react');
module.exports = {
propTypes: {
default_name: React.PropTypes.string
},
var TextForEvent = require('../../../TextForEvent');
getDefaultProps: function() {
return {
default_name: '',
};
},
module.exports = React.createClass({
displayName: 'TextualEvent',
getInitialState: function() {
return {
room_name: this.props.default_name,
statics: {
needsSenderProfile: function() {
return false;
}
},
getName: function() {
return this.state.room_name;
render: function() {
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>
);
},
};
});

View file

@ -16,12 +16,17 @@ limitations under the License.
'use strict';
var dis = require("../../dispatcher");
var React = require('react');
module.exports = {
onClick: function() {
dis.dispatch({
action: 'view_user_settings'
});
module.exports = React.createClass({
displayName: 'UnknownBody',
render: function() {
var content = this.props.mxEvent.getContent();
return (
<span className="mx_UnknownMessageTile">
{content.body}
</span>
);
},
};
});

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

View file

@ -25,23 +25,36 @@ limitations under the License.
* 'muted': 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");
var dis = require("../../dispatcher");
var Modal = require("../../Modal");
var sdk = require('../../index');
module.exports = React.createClass({
displayName: 'MemberInfo',
getDefaultProps: function() {
return {
onFinished: function() {}
};
},
module.exports = {
componentDidMount: function() {
// work out the current state
if (this.props.member) {
var memberState = this._calculateOpsPermissions();
var memberState = this._calculateOpsPermissions(this.props.member);
this.setState(memberState);
}
},
componentWillReceiveProps: function(newProps) {
var memberState = this._calculateOpsPermissions(newProps.member);
this.setState(memberState);
},
onKick: function() {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
MatrixClientPeg.get().kick(roomId, target).done(function() {
@ -58,7 +71,7 @@ module.exports = {
},
onBan: function() {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
MatrixClientPeg.get().ban(roomId, target).done(function() {
@ -75,7 +88,7 @@ module.exports = {
},
onMuteToggle: function() {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
var room = MatrixClientPeg.get().getRoom(roomId);
@ -119,7 +132,7 @@ module.exports = {
},
onModToggle: function() {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var roomId = this.props.member.roomId;
var target = this.props.member.userId;
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() {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
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()
});
});
}
}
dis.dispatch({
action: 'leave_room',
room_id: this.props.member.roomId,
});
this.props.onFinished();
},
@ -261,13 +248,13 @@ module.exports = {
}
},
_calculateOpsPermissions: function() {
_calculateOpsPermissions: function(member) {
var defaultPerms = {
can: {},
muted: false,
modifyLevel: false
};
var room = MatrixClientPeg.get().getRoom(this.props.member.roomId);
var room = MatrixClientPeg.get().getRoom(member.roomId);
if (!room) {
return defaultPerms;
}
@ -278,7 +265,7 @@ module.exports = {
return defaultPerms;
}
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
var them = this.props.member;
var them = member;
return {
can: this._calculateCanPermissions(
me, them, powerLevels.getContent()
@ -320,6 +307,76 @@ module.exports = {
powerLevelContent.events_default
);
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>
);
}
});

View file

@ -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
limitations under the License.
*/
var MatrixClientPeg = require("../../MatrixClientPeg");
var Modal = require("../../Modal");
var sdk = require('../../index');
var React = require('react');
var classNames = require('classnames');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var sdk = require('../../../index');
var GeminiScrollbar = require('react-gemini-scrollbar');
var INITIAL_LOAD_NUM_MEMBERS = 50;
module.exports = {
module.exports = React.createClass({
displayName: 'MemberList',
getInitialState: function() {
if (!this.props.roomId) return { members: [] };
var cli = MatrixClientPeg.get();
@ -38,6 +41,7 @@ module.exports = {
componentWillMount: function() {
var cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("Room", this.onRoom); // invites
},
@ -45,6 +49,7 @@ module.exports = {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn);
}
},
@ -97,6 +102,10 @@ module.exports = {
this._updateList();
},
onRoomMemberName: function(ev, member) {
this._updateList();
},
_updateList: function() {
this.memberDict = this.getMemberDict();
@ -107,7 +116,7 @@ module.exports = {
},
onInvite: function(inputText) {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
var self = this;
inputText = inputText.trim(); // react requires es5-shim so we know trim() exists
var isEmailAddress = /^\S+@\S+\.\S+$/.test(inputText);
@ -189,6 +198,94 @@ module.exports = {
}
}
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>
);
}
});

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

View file

@ -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
limitations under the License.
*/
var React = require("react");
var MatrixClientPeg = require("../../MatrixClientPeg");
var SlashCommands = require("../../SlashCommands");
var Modal = require("../../Modal");
var sdk = require('../../index');
var marked = require("marked");
marked.setOptions({
renderer: new marked.Renderer(),
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 = {
ENTER: 13,
BACKSPACE: 8,
@ -32,16 +46,33 @@ var KeyCode = {
var TYPING_USER_TIMEOUT = 10000;
var TYPING_SERVER_TIMEOUT = 30000;
var MARKDOWN_ENABLED = true;
module.exports = {
oldScrollHeight: 0,
function mdownToHtml(mdown) {
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() {
this.oldScrollHeight = 0;
this.markdownEnabled = MARKDOWN_ENABLED;
this.tabStruct = {
completing: false,
original: null,
index: 0
};
var self = this;
this.sentHistory = {
// The list of typed messages. Index 0 is more recent
data: [],
@ -111,6 +142,8 @@ module.exports = {
// restore the original text the user was typing.
this.element.value = this.originalText;
}
self.resizeInput();
return true;
},
@ -126,6 +159,7 @@ module.exports = {
var text = window.sessionStorage.getItem("input_" + this.roomId);
if (text) {
this.element.value = text;
self.resizeInput();
}
}
};
@ -137,6 +171,7 @@ module.exports = {
this.refs.textarea,
this.props.room.roomId
);
this.resizeInput();
},
componentWillUnmount: function() {
@ -208,7 +243,7 @@ module.exports = {
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
this.refs.textarea.style.height = "0px";
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) {
// kick gemini-scrollbar to re-layout
this.props.roomView.forceUpdate();
@ -228,6 +263,27 @@ module.exports = {
onEnter: function(ev) {
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);
if (cmd) {
ev.preventDefault();
@ -239,7 +295,7 @@ module.exports = {
console.log("Command success.");
}, function(err) {
console.error("Command failure: %s", err);
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Server error",
description: err.message
@ -248,7 +304,7 @@ module.exports = {
}
else if (cmd.error) {
console.error(cmd.error);
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Command error",
description: cmd.error
@ -257,20 +313,26 @@ module.exports = {
return;
}
var content = null;
if (/^\/me /i.test(contentText)) {
content = {
msgtype: 'm.emote',
body: contentText.substring(4)
};
} else {
content = {
msgtype: 'm.text',
body: contentText
};
var isEmote = /^\/me /i.test(contentText);
var sendMessagePromise;
if (isEmote) {
contentText = contentText.substring(4);
}
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({
action: 'message_sent'
});
@ -280,6 +342,7 @@ module.exports = {
});
});
this.refs.textarea.value = '';
this.resizeInput();
ev.preventDefault();
},
@ -445,7 +508,99 @@ module.exports = {
clearTimeout(this.typingTimeout);
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>
);
}
};
});

View 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">&nbsp;({ 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>
);
},
});

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

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

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

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

View file

@ -15,10 +15,16 @@ limitations under the License.
*/
'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() {
return {
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}/>
);
}
}
});

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

View file

@ -15,10 +15,13 @@ limitations under the License.
*/
'use strict';
var sdk = require('../../index');
var dis = require("../../dispatcher");
var React = require("react");
var Notifier = require("../../../Notifier");
var sdk = require('../../../index');
var dis = require("../../../dispatcher");
module.exports = {
module.exports = React.createClass({
displayName: 'EnableNotificationsButton',
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
@ -36,12 +39,10 @@ module.exports = {
},
enabled: function() {
var Notifier = sdk.getComponent('organisms.Notifier');
return Notifier.isEnabled();
},
onClick: function() {
var Notifier = sdk.getComponent('organisms.Notifier');
var self = this;
if (!Notifier.supportsDesktopNotifications()) {
return;
@ -55,4 +56,20 @@ module.exports = {
}
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>
);
}
}
});

View file

@ -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
limitations under the License.
*/
var React = require("react");
var dis = require("../../../dispatcher");
var CallHandler = require("../../../CallHandler");
var sdk = require('../../../index');
var MatrixClientPeg = require("../../../MatrixClientPeg");
/*
* State vars:
@ -23,14 +25,31 @@ var CallHandler = require("../../../CallHandler");
*
* Props:
* 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() {
this.dispatcherRef = dis.register(this.onAction);
this._trackedRoom = null;
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) {
// if we were given a room_id to track, don't handle anything else.
if (payload.room_id && this.props.room &&
this.props.room.roomId !== payload.room_id) {
return;
}
if (payload.action !== 'call_state') {
// 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;
}
this.showCall(payload.room_id);
},
showCall: function(roomId) {
var call = CallHandler.getCall(roomId);
var call = (
CallHandler.getCallForRoom(roomId) ||
(this.props.ConferenceHandler ?
this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
null
)
);
if (call) {
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
@ -60,13 +82,29 @@ module.exports = {
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
}
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";
}
else {
this.getVideoView().getLocalVideoElement().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 }/>
);
}
});

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

View file

@ -18,9 +18,14 @@ limitations under the License.
var React = require('react');
module.exports = {
propTypes: {
value: React.PropTypes.number,
max: React.PropTypes.number
module.exports = React.createClass({
displayName: 'VideoFeed',
render: function() {
return (
<video>
</video>
);
},
};
});

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

View file

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

View file

@ -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
};
}
};

View file

@ -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;
},
};

View file

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

View file

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

View file

@ -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;
}
};

View file

@ -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);
}
};

View file

@ -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);
}
};

View file

@ -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()
});
});
}
}
});
}
};

View file

@ -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
});
}
};

View file

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

View file

@ -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;
},
};

View file

@ -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
});
}
};

View file

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

View file

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

View file

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

View file

@ -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'
});
}
};

View file

@ -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'
});
}
};

View file

@ -15,16 +15,11 @@ limitations under the License.
*/
var Skinner = require('./Skinner');
var Modulator = require('./Modulator');
module.exports.loadSkin = function(skinObject) {
Skinner.load(skinObject);
};
module.exports.loadModule = function(moduleObject) {
Modulator.loadModule(moduleObject);
};
module.exports.resetSkin = function() {
Skinner.reset();
};