mirror of
https://github.com/element-hq/element-web
synced 2024-11-28 20:38:55 +03:00
Merge branch 'develop' into kegan/guest-access
This commit is contained in:
commit
ae7b2d54bb
44 changed files with 2654 additions and 1040 deletions
|
@ -138,9 +138,17 @@ function _setCallListeners(call) {
|
|||
|
||||
function _setCallState(call, roomId, status) {
|
||||
console.log(
|
||||
"Call state in %s changed to %s (%s)", roomId, status, (call ? call.state : "-")
|
||||
"Call state in %s changed to %s (%s)", roomId, status, (call ? call.call_state : "-")
|
||||
);
|
||||
calls[roomId] = call;
|
||||
|
||||
if (status === "ringing") {
|
||||
play("ringAudio")
|
||||
}
|
||||
else if (call && call.call_state === "ringing") {
|
||||
pause("ringAudio")
|
||||
}
|
||||
|
||||
if (call) {
|
||||
call.call_state = status;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -35,10 +35,10 @@ module.exports = {
|
|||
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());
|
||||
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + 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());
|
||||
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
117
src/HtmlUtils.js
117
src/HtmlUtils.js
|
@ -48,8 +48,15 @@ var sanitizeHtmlParams = {
|
|||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
_applyHighlights: function(safeSnippet, highlights, html, k) {
|
||||
class Highlighter {
|
||||
constructor(html, highlightClass, onHighlightClick) {
|
||||
this.html = html;
|
||||
this.highlightClass = highlightClass;
|
||||
this.onHighlightClick = onHighlightClick;
|
||||
this._key = 0;
|
||||
}
|
||||
|
||||
applyHighlights(safeSnippet, highlights) {
|
||||
var lastOffset = 0;
|
||||
var offset;
|
||||
var nodes = [];
|
||||
|
@ -61,77 +68,97 @@ module.exports = {
|
|||
// 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) {
|
||||
var safeHighlight = this.html ? sanitizeHtml(highlights[0], sanitizeHtmlParams) : highlights[0];
|
||||
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
|
||||
// handle preamble
|
||||
if (offset > lastOffset) {
|
||||
nodes = nodes.concat(this._applySubHighlightsInRange(safeSnippet, lastOffset, offset, highlights, html, k));
|
||||
k += nodes.length;
|
||||
var subSnippet = safeSnippet.substring(lastOffset, offset);
|
||||
nodes = nodes.concat(this._applySubHighlights(subSnippet, highlights));
|
||||
}
|
||||
|
||||
// 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>);
|
||||
}
|
||||
nodes.push(this._createSpan(safeHighlight, true));
|
||||
|
||||
lastOffset = offset + safeHighlight.length;
|
||||
}
|
||||
|
||||
// handle postamble
|
||||
if (lastOffset != safeSnippet.length) {
|
||||
nodes = nodes.concat(this._applySubHighlightsInRange(safeSnippet, lastOffset, undefined, highlights, html, k));
|
||||
k += nodes.length;
|
||||
var subSnippet = safeSnippet.substring(lastOffset, undefined);
|
||||
nodes = nodes.concat(this._applySubHighlights(subSnippet, highlights));
|
||||
}
|
||||
return nodes;
|
||||
},
|
||||
}
|
||||
|
||||
_applySubHighlightsInRange: function(safeSnippet, lastOffset, offset, highlights, html, k) {
|
||||
var nodes = [];
|
||||
_applySubHighlights(safeSnippet, highlights) {
|
||||
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;
|
||||
return this.applyHighlights(safeSnippet, highlights.slice(1));
|
||||
}
|
||||
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 [this._createSpan(safeSnippet, false)];
|
||||
}
|
||||
return nodes;
|
||||
},
|
||||
}
|
||||
|
||||
bodyToHtml: function(content, highlights) {
|
||||
var originalBody = content.body;
|
||||
var body;
|
||||
var k = 0;
|
||||
/* create a <span> node to hold the given content
|
||||
*
|
||||
* spanBody: content of the span. If html, must have been sanitised
|
||||
* highlight: true to highlight as a search match
|
||||
*/
|
||||
_createSpan(spanBody, highlight) {
|
||||
var spanProps = {
|
||||
key: this._key++,
|
||||
};
|
||||
|
||||
if (highlights && highlights.length > 0) {
|
||||
var bodyList = [];
|
||||
if (highlight) {
|
||||
spanProps.onClick = this.onHighlightClick;
|
||||
spanProps.className = this.highlightClass;
|
||||
}
|
||||
|
||||
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;
|
||||
if (this.html) {
|
||||
return (<span {...spanProps} dangerouslySetInnerHTML={{ __html: spanBody }} />);
|
||||
}
|
||||
else {
|
||||
if (content.format === "org.matrix.custom.html") {
|
||||
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
||||
return (<span {...spanProps}>{ spanBody }</span>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
/* turn a matrix event body into html
|
||||
*
|
||||
* content: 'content' of the MatrixEvent
|
||||
*
|
||||
* highlights: optional list of words to highlight
|
||||
*
|
||||
* opts.onHighlightClick: optional callback function to be called when a
|
||||
* highlighted word is clicked
|
||||
*/
|
||||
bodyToHtml: function(content, highlights, opts) {
|
||||
opts = opts || {};
|
||||
|
||||
var isHtml = (content.format === "org.matrix.custom.html");
|
||||
|
||||
var safeBody;
|
||||
if (isHtml) {
|
||||
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
||||
} else {
|
||||
safeBody = content.body;
|
||||
}
|
||||
|
||||
var body;
|
||||
if (highlights && highlights.length > 0) {
|
||||
var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
|
||||
body = highlighter.applyHighlights(safeBody, highlights);
|
||||
}
|
||||
else {
|
||||
if (isHtml) {
|
||||
body = <span className="markdown-body" dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
||||
}
|
||||
else {
|
||||
body = originalBody;
|
||||
body = safeBody;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ var dis = require("./dispatcher");
|
|||
* }
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
var Notifier = {
|
||||
|
||||
notificationMessageForEvent: function(ev) {
|
||||
return TextForEvent.textForEvent(ev);
|
||||
|
@ -98,13 +98,16 @@ module.exports = {
|
|||
|
||||
start: function() {
|
||||
this.boundOnRoomTimeline = this.onRoomTimeline.bind(this);
|
||||
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
||||
MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline);
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -175,8 +178,15 @@ module.exports = {
|
|||
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()) {
|
||||
|
@ -190,3 +200,8 @@ module.exports = {
|
|||
}
|
||||
};
|
||||
|
||||
if (!global.mxNotifier) {
|
||||
global.mxNotifier = Notifier;
|
||||
}
|
||||
|
||||
module.exports = global.mxNotifier;
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
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');
|
||||
|
||||
|
@ -21,4 +37,13 @@ module.exports = {
|
|||
event: event
|
||||
});
|
||||
},
|
||||
|
||||
removeFromQueue: function(event) {
|
||||
MatrixClientPeg.get().getScheduler().removeEventFromQueue(event);
|
||||
var room = MatrixClientPeg.get().getRoom(event.getRoomId());
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
room.removeEvents([event.getId()]);
|
||||
}
|
||||
};
|
|
@ -116,8 +116,17 @@ class Register extends Signup {
|
|||
|
||||
_tryRegister(authDict) {
|
||||
var self = this;
|
||||
|
||||
var bindEmail;
|
||||
|
||||
if (this.username && this.password) {
|
||||
// only need to bind_email when sending u/p - sending it at other
|
||||
// times clobbers the u/p resulting in M_MISSING_PARAM (password)
|
||||
bindEmail = true;
|
||||
}
|
||||
|
||||
return MatrixClientPeg.get().register(
|
||||
this.username, this.password, this.params.sessionId, authDict
|
||||
this.username, this.password, this.params.sessionId, authDict, bindEmail
|
||||
).then(function(result) {
|
||||
self.credentials = result;
|
||||
self.setStep("COMPLETE");
|
||||
|
|
|
@ -99,29 +99,33 @@ var commands = {
|
|||
}
|
||||
|
||||
// Try to find a room with this alias
|
||||
// XXX: do we need to do this? Doesn't the JS SDK suppress duplicate attempts to join the same room?
|
||||
var foundRoom = MatrixTools.getRoomForAlias(
|
||||
MatrixClientPeg.get().getRooms(),
|
||||
room_alias
|
||||
);
|
||||
if (foundRoom) { // we've already joined this room, view it.
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: foundRoom.roomId
|
||||
});
|
||||
return success();
|
||||
}
|
||||
else {
|
||||
// attempt to join this alias.
|
||||
return success(
|
||||
MatrixClientPeg.get().joinRoom(room_alias).then(
|
||||
function(room) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
if (foundRoom) { // we've already joined this room, view it if it's not archived.
|
||||
var me = foundRoom.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (me && me.membership !== "leave") {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: foundRoom.roomId
|
||||
});
|
||||
return success();
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise attempt to join this alias.
|
||||
return success(
|
||||
MatrixClientPeg.get().joinRoom(room_alias).then(
|
||||
function(room) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
return reject("Usage: /join <room_alias>");
|
||||
|
|
307
src/TabComplete.js
Normal file
307
src/TabComplete.js
Normal file
|
@ -0,0 +1,307 @@
|
|||
/*
|
||||
Copyright 2015 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
var Entry = require("./TabCompleteEntries").Entry;
|
||||
|
||||
const DELAY_TIME_MS = 1000;
|
||||
const KEY_TAB = 9;
|
||||
const KEY_SHIFT = 16;
|
||||
const KEY_WINDOWS = 91;
|
||||
|
||||
// NB: DO NOT USE \b its "words" are roman alphabet only!
|
||||
//
|
||||
// Capturing group containing the start
|
||||
// of line or a whitespace char
|
||||
// \_______________ __________Capturing group of 1 or more non-whitespace chars
|
||||
// _|__ _|_ followed by the end of line
|
||||
// / \/ \
|
||||
const MATCH_REGEX = /(^|\s)(\S+)$/;
|
||||
|
||||
class TabComplete {
|
||||
|
||||
constructor(opts) {
|
||||
opts.startingWordSuffix = opts.startingWordSuffix || "";
|
||||
opts.wordSuffix = opts.wordSuffix || "";
|
||||
opts.allowLooping = opts.allowLooping || false;
|
||||
opts.autoEnterTabComplete = opts.autoEnterTabComplete || false;
|
||||
opts.onClickCompletes = opts.onClickCompletes || false;
|
||||
this.opts = opts;
|
||||
this.completing = false;
|
||||
this.list = []; // full set of tab-completable things
|
||||
this.matchedList = []; // subset of completable things to loop over
|
||||
this.currentIndex = 0; // index in matchedList currently
|
||||
this.originalText = null; // original input text when tab was first hit
|
||||
this.textArea = opts.textArea; // DOMElement
|
||||
this.isFirstWord = false; // true if you tab-complete on the first word
|
||||
this.enterTabCompleteTimerId = null;
|
||||
this.inPassiveMode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Entry[]} completeList
|
||||
*/
|
||||
setCompletionList(completeList) {
|
||||
this.list = completeList;
|
||||
if (this.opts.onClickCompletes) {
|
||||
// assign onClick listeners for each entry to complete the text
|
||||
this.list.forEach((l) => {
|
||||
l.onClick = () => {
|
||||
this.completeTo(l.getText());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DOMElement}
|
||||
*/
|
||||
setTextArea(textArea) {
|
||||
this.textArea = textArea;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isTabCompleting() {
|
||||
// actually have things to tab over
|
||||
return this.completing && this.matchedList.length > 1;
|
||||
}
|
||||
|
||||
stopTabCompleting() {
|
||||
this.completing = false;
|
||||
this.currentIndex = 0;
|
||||
this._notifyStateChange();
|
||||
}
|
||||
|
||||
startTabCompleting() {
|
||||
this.completing = true;
|
||||
this.currentIndex = 0;
|
||||
this._calculateCompletions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an auto-complete with the given word. This terminates the tab-complete.
|
||||
* @param {string} someVal
|
||||
*/
|
||||
completeTo(someVal) {
|
||||
this.textArea.value = this._replaceWith(someVal, true);
|
||||
this.stopTabCompleting();
|
||||
// keep focus on the text area
|
||||
this.textArea.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} numAheadToPeek Return *up to* this many elements.
|
||||
* @return {Entry[]}
|
||||
*/
|
||||
peek(numAheadToPeek) {
|
||||
if (this.matchedList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
var peekList = [];
|
||||
|
||||
// return the current match item and then one with an index higher, and
|
||||
// so on until we've reached the requested limit. If we hit the end of
|
||||
// the list of options we're done.
|
||||
for (var i = 0; i < numAheadToPeek; i++) {
|
||||
var nextIndex;
|
||||
if (this.opts.allowLooping) {
|
||||
nextIndex = (this.currentIndex + i) % this.matchedList.length;
|
||||
}
|
||||
else {
|
||||
nextIndex = this.currentIndex + i;
|
||||
if (nextIndex === this.matchedList.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
peekList.push(this.matchedList[nextIndex]);
|
||||
}
|
||||
// console.log("Peek list(%s): %s", numAheadToPeek, JSON.stringify(peekList));
|
||||
return peekList;
|
||||
}
|
||||
|
||||
handleTabPress(passive, shiftKey) {
|
||||
var wasInPassiveMode = this.inPassiveMode && !passive;
|
||||
this.inPassiveMode = passive;
|
||||
|
||||
if (!this.completing) {
|
||||
this.startTabCompleting();
|
||||
}
|
||||
|
||||
if (shiftKey) {
|
||||
this.nextMatchedEntry(-1);
|
||||
}
|
||||
else {
|
||||
// if we were in passive mode we got out of sync by incrementing the
|
||||
// index to show the peek view but not set the text area. Therefore,
|
||||
// we want to set the *current* index rather than the *next* index.
|
||||
this.nextMatchedEntry(wasInPassiveMode ? 0 : 1);
|
||||
}
|
||||
this._notifyStateChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DOMEvent} e
|
||||
*/
|
||||
onKeyDown(ev) {
|
||||
if (!this.textArea) {
|
||||
console.error("onKeyDown called before a <textarea> was set!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.keyCode !== KEY_TAB) {
|
||||
// pressing any key (except shift, windows, cmd (OSX) and ctrl/alt combinations)
|
||||
// aborts the current tab completion
|
||||
if (this.completing && ev.keyCode !== KEY_SHIFT &&
|
||||
!ev.metaKey && !ev.ctrlKey && !ev.altKey && ev.keyCode !== KEY_WINDOWS) {
|
||||
// they're resuming typing; reset tab complete state vars.
|
||||
this.stopTabCompleting();
|
||||
}
|
||||
|
||||
|
||||
// explicitly pressing any key except tab removes passive mode. Tab doesn't remove
|
||||
// passive mode because handleTabPress needs to know when passive mode is toggling
|
||||
// off so it can resync the textarea/peek list. If tab did remove passive mode then
|
||||
// handleTabPress would never be able to tell when passive mode toggled off.
|
||||
this.inPassiveMode = false;
|
||||
|
||||
// pressing any key at all (except tab) restarts the automatic tab-complete timer
|
||||
if (this.opts.autoEnterTabComplete) {
|
||||
clearTimeout(this.enterTabCompleteTimerId);
|
||||
this.enterTabCompleteTimerId = setTimeout(() => {
|
||||
if (!this.completing) {
|
||||
this.handleTabPress(true, false);
|
||||
}
|
||||
}, DELAY_TIME_MS);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// tab key has been pressed at this point
|
||||
this.handleTabPress(false, ev.shiftKey)
|
||||
|
||||
// prevent the default TAB operation (typically focus shifting)
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the textarea to the next value in the matched list.
|
||||
* @param {Number} offset Offset to apply *before* setting the next value.
|
||||
*/
|
||||
nextMatchedEntry(offset) {
|
||||
if (this.matchedList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// work out the new index, wrapping if necessary.
|
||||
this.currentIndex += offset;
|
||||
if (this.currentIndex >= this.matchedList.length) {
|
||||
this.currentIndex = 0;
|
||||
}
|
||||
else if (this.currentIndex < 0) {
|
||||
this.currentIndex = this.matchedList.length - 1;
|
||||
}
|
||||
var isTransitioningToOriginalText = (
|
||||
// impossible to transition if they've never hit tab
|
||||
!this.inPassiveMode && this.currentIndex === 0
|
||||
);
|
||||
|
||||
if (!this.inPassiveMode) {
|
||||
// set textarea to this new value
|
||||
this.textArea.value = this._replaceWith(
|
||||
this.matchedList[this.currentIndex].text,
|
||||
this.currentIndex !== 0 // don't suffix the original text!
|
||||
);
|
||||
}
|
||||
|
||||
// visual display to the user that we looped - TODO: This should be configurable
|
||||
if (isTransitioningToOriginalText) {
|
||||
this.textArea.style["background-color"] = "#faa";
|
||||
setTimeout(() => { // yay for lexical 'this'!
|
||||
this.textArea.style["background-color"] = "";
|
||||
}, 150);
|
||||
|
||||
if (!this.opts.allowLooping) {
|
||||
this.stopTabCompleting();
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.textArea.style["background-color"] = ""; // cancel blinks TODO: required?
|
||||
}
|
||||
}
|
||||
|
||||
_replaceWith(newVal, includeSuffix) {
|
||||
// The regex to replace the input matches a character of whitespace AND
|
||||
// the partial word. If we just use string.replace() with the regex it will
|
||||
// replace the partial word AND the character of whitespace. We want to
|
||||
// preserve whatever that character is (\n, \t, etc) so find out what it is now.
|
||||
var boundaryChar;
|
||||
var res = MATCH_REGEX.exec(this.originalText);
|
||||
if (res) {
|
||||
boundaryChar = res[1]; // the first captured group
|
||||
}
|
||||
if (boundaryChar === undefined) {
|
||||
console.warn("Failed to find boundary char on text: '%s'", this.originalText);
|
||||
boundaryChar = "";
|
||||
}
|
||||
|
||||
var replacementText = (
|
||||
boundaryChar + newVal + (
|
||||
includeSuffix ?
|
||||
(this.isFirstWord ? this.opts.startingWordSuffix : this.opts.wordSuffix) :
|
||||
""
|
||||
)
|
||||
);
|
||||
return this.originalText.replace(MATCH_REGEX, function() {
|
||||
return replacementText; // function form to avoid `$` special-casing
|
||||
});
|
||||
}
|
||||
|
||||
_calculateCompletions() {
|
||||
this.originalText = this.textArea.value; // cache starting text
|
||||
|
||||
// grab the partial word from the text which we'll be tab-completing
|
||||
var res = MATCH_REGEX.exec(this.originalText);
|
||||
if (!res) {
|
||||
this.matchedList = [];
|
||||
return;
|
||||
}
|
||||
// ES6 destructuring; ignore first element (the complete match)
|
||||
var [ , boundaryGroup, partialGroup] = res;
|
||||
this.isFirstWord = partialGroup.length === this.originalText.length;
|
||||
|
||||
this.matchedList = [
|
||||
new Entry(partialGroup) // first entry is always the original partial
|
||||
];
|
||||
|
||||
// find matching entries in the set of entries given to us
|
||||
this.list.forEach((entry) => {
|
||||
if (entry.text.toLowerCase().indexOf(partialGroup.toLowerCase()) === 0) {
|
||||
this.matchedList.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
// console.log("_calculateCompletions => %s", JSON.stringify(this.matchedList));
|
||||
}
|
||||
|
||||
_notifyStateChange() {
|
||||
if (this.opts.onStateChange) {
|
||||
this.opts.onStateChange(this.completing);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TabComplete;
|
101
src/TabCompleteEntries.js
Normal file
101
src/TabCompleteEntries.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
Copyright 2015 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
var React = require("react");
|
||||
var sdk = require("./index");
|
||||
|
||||
class Entry {
|
||||
constructor(text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string} The text to display in this entry.
|
||||
*/
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ReactClass} Raw JSX
|
||||
*/
|
||||
getImageJsx() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {?string} The unique key= prop for React dedupe
|
||||
*/
|
||||
getKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this entry is clicked.
|
||||
*/
|
||||
onClick() {
|
||||
// NOP
|
||||
}
|
||||
}
|
||||
|
||||
class MemberEntry extends Entry {
|
||||
constructor(member) {
|
||||
super(member.name || member.userId);
|
||||
this.member = member;
|
||||
}
|
||||
|
||||
getImageJsx() {
|
||||
var MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
return (
|
||||
<MemberAvatar member={this.member} width={24} height={24} />
|
||||
);
|
||||
}
|
||||
|
||||
getKey() {
|
||||
return this.member.userId;
|
||||
}
|
||||
}
|
||||
|
||||
MemberEntry.fromMemberList = function(members) {
|
||||
return members.sort(function(a, b) {
|
||||
var userA = a.user;
|
||||
var userB = b.user;
|
||||
if (userA && !userB) {
|
||||
return -1; // a comes first
|
||||
}
|
||||
else if (!userA && userB) {
|
||||
return 1; // b comes first
|
||||
}
|
||||
else if (!userA && !userB) {
|
||||
return 0; // don't care
|
||||
}
|
||||
else { // both User objects exist
|
||||
if (userA.lastActiveAgo < userB.lastActiveAgo) {
|
||||
return -1; // a comes first
|
||||
}
|
||||
else if (userA.lastActiveAgo > userB.lastActiveAgo) {
|
||||
return 1; // b comes first
|
||||
}
|
||||
else {
|
||||
return 0; // same last active ago
|
||||
}
|
||||
}
|
||||
}).map(function(m) {
|
||||
return new MemberEntry(m);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.Entry = Entry;
|
||||
module.exports.MemberEntry = MemberEntry;
|
|
@ -9,7 +9,17 @@ function textForMemberEvent(ev) {
|
|||
) : "";
|
||||
switch (ev.getContent().membership) {
|
||||
case 'invite':
|
||||
return senderName + " invited " + targetName + ".";
|
||||
var threePidContent = ev.getContent().third_party_invite;
|
||||
if (threePidContent) {
|
||||
// TODO: When we have third_party_invite.display_name we should
|
||||
// do this as "$displayname received the invitation from $sender"
|
||||
// or equiv
|
||||
return targetName + " received an invitation from " + senderName +
|
||||
".";
|
||||
}
|
||||
else {
|
||||
return senderName + " invited " + targetName + ".";
|
||||
}
|
||||
case 'ban':
|
||||
return senderName + " banned " + targetName + "." + reason;
|
||||
case 'join':
|
||||
|
@ -101,6 +111,12 @@ function textForCallInviteEvent(event) {
|
|||
return senderName + " placed a " + type + " call." + supported;
|
||||
};
|
||||
|
||||
function textForThreePidInviteEvent(event) {
|
||||
var senderName = event.sender ? event.sender.name : event.getSender();
|
||||
return senderName + " sent an invitation to " + event.getContent().display_name +
|
||||
" to join the room.";
|
||||
};
|
||||
|
||||
var handlers = {
|
||||
'm.room.message': textForMessageEvent,
|
||||
'm.room.name': textForRoomNameEvent,
|
||||
|
@ -109,6 +125,7 @@ var handlers = {
|
|||
'm.call.invite': textForCallInviteEvent,
|
||||
'm.call.answer': textForCallAnswerEvent,
|
||||
'm.call.hangup': textForCallHangupEvent,
|
||||
'm.room.third_party_invite': textForThreePidInviteEvent
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
30
src/UnreadStatus.js
Normal file
30
src/UnreadStatus.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
};
|
67
src/UserSettingsStore.js
Normal file
67
src/UserSettingsStore.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
Copyright 2015 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
var Notifier = require("./Notifier");
|
||||
|
||||
/*
|
||||
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
|
||||
loadProfileInfo: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
return cli.getProfileInfo(cli.credentials.userId);
|
||||
},
|
||||
|
||||
saveDisplayName: function(newDisplayname) {
|
||||
return MatrixClientPeg.get().setDisplayName(newDisplayname);
|
||||
},
|
||||
|
||||
loadThreePids: function() {
|
||||
return MatrixClientPeg.get().getThreePids();
|
||||
},
|
||||
|
||||
saveThreePids: function(threePids) {
|
||||
// TODO
|
||||
},
|
||||
|
||||
getEnableNotifications: function() {
|
||||
return Notifier.isEnabled();
|
||||
},
|
||||
|
||||
setEnableNotifications: function(enable) {
|
||||
if (!Notifier.supportsDesktopNotifications()) {
|
||||
return;
|
||||
}
|
||||
Notifier.setEnabled(enable);
|
||||
},
|
||||
|
||||
changePassword: function(old_password, new_password) {
|
||||
var cli = MatrixClientPeg.get();
|
||||
|
||||
var authDict = {
|
||||
type: 'm.login.password',
|
||||
user: cli.credentials.userId,
|
||||
password: old_password
|
||||
};
|
||||
|
||||
return cli.setPassword(authDict, new_password);
|
||||
},
|
||||
};
|
|
@ -28,6 +28,8 @@ module.exports.components['structures.login.PostRegistration'] = require('./comp
|
|||
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.ScrollPanel'] = require('./components/structures/ScrollPanel');
|
||||
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');
|
||||
|
@ -64,6 +66,7 @@ module.exports.components['views.rooms.RoomHeader'] = require('./components/view
|
|||
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.rooms.TabCompleteBar'] = require('./components/views/rooms/TabCompleteBar');
|
||||
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');
|
||||
|
|
|
@ -251,13 +251,15 @@ module.exports = React.createClass({
|
|||
var UserSelector = sdk.getComponent("elements.UserSelector");
|
||||
var RoomHeader = sdk.getComponent("rooms.RoomHeader");
|
||||
|
||||
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, '');
|
||||
|
||||
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 />
|
||||
<RoomAlias ref="alias" alias={this.state.alias} homeserver={ domain } onChange={this.onAliasChanged}/> <br />
|
||||
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
|
||||
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
|
||||
<div>
|
||||
|
|
|
@ -29,6 +29,7 @@ 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");
|
||||
|
@ -41,7 +42,8 @@ module.exports = React.createClass({
|
|||
ConferenceHandler: React.PropTypes.any,
|
||||
onNewScreen: React.PropTypes.func,
|
||||
registrationUrl: React.PropTypes.string,
|
||||
enableGuest: React.PropTypes.bool
|
||||
enableGuest: React.PropTypes.bool,
|
||||
startingQueryParams: React.PropTypes.object
|
||||
},
|
||||
|
||||
PageTypes: {
|
||||
|
@ -75,6 +77,12 @@ module.exports = React.createClass({
|
|||
return s;
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
startingQueryParams: {}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._autoRegisterAsGuest = false;
|
||||
if (this.props.enableGuest) {
|
||||
|
@ -94,6 +102,9 @@ module.exports = React.createClass({
|
|||
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);
|
||||
|
||||
|
@ -246,28 +257,38 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
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;
|
||||
|
@ -284,11 +305,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
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(
|
||||
|
@ -296,11 +313,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
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':
|
||||
|
@ -324,21 +337,15 @@ module.exports = React.createClass({
|
|||
});
|
||||
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':
|
||||
|
@ -367,6 +374,58 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_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) {
|
||||
credentials.guest = Boolean(credentials.guest);
|
||||
console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest);
|
||||
|
@ -385,7 +444,10 @@ module.exports = React.createClass({
|
|||
startMatrixClient: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
var self = this;
|
||||
cli.on('sync', function(state) {
|
||||
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;
|
||||
|
@ -434,7 +496,9 @@ module.exports = React.createClass({
|
|||
Notifier.start();
|
||||
UserActivity.start();
|
||||
Presence.start();
|
||||
cli.startClient();
|
||||
cli.startClient({
|
||||
pendingEventOrdering: "end"
|
||||
});
|
||||
},
|
||||
|
||||
onKeyDown: function(ev) {
|
||||
|
@ -610,6 +674,22 @@ module.exports = React.createClass({
|
|||
this.showScreen("settings");
|
||||
},
|
||||
|
||||
onUserSettingsClose: function() {
|
||||
// XXX: use browser history instead to find the previous room?
|
||||
if (this.state.currentRoom) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.currentRoom,
|
||||
});
|
||||
}
|
||||
else {
|
||||
dis.dispatch({
|
||||
action: 'view_indexed_room',
|
||||
roomIndex: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
var RoomView = sdk.getComponent('structures.RoomView');
|
||||
|
@ -634,6 +714,7 @@ module.exports = React.createClass({
|
|||
case this.PageTypes.RoomView:
|
||||
page_element = (
|
||||
<RoomView
|
||||
ref="roomView"
|
||||
roomId={this.state.currentRoom}
|
||||
key={this.state.currentRoom}
|
||||
ConferenceHandler={this.props.ConferenceHandler} />
|
||||
|
@ -641,7 +722,7 @@ module.exports = React.createClass({
|
|||
right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} />
|
||||
break;
|
||||
case this.PageTypes.UserSettings:
|
||||
page_element = <UserSettings />
|
||||
page_element = <UserSettings onClose={this.onUserSettingsClose} />
|
||||
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
|
||||
break;
|
||||
case this.PageTypes.CreateRoom:
|
||||
|
@ -702,6 +783,7 @@ module.exports = React.createClass({
|
|||
clientSecret={this.state.register_client_secret}
|
||||
sessionId={this.state.register_session_id}
|
||||
idSid={this.state.register_id_sid}
|
||||
email={this.props.startingQueryParams.email}
|
||||
hsUrl={this.props.config.default_hs_url}
|
||||
isUrl={this.props.config.default_is_url}
|
||||
registrationUrl={this.props.registrationUrl}
|
||||
|
|
File diff suppressed because it is too large
Load diff
288
src/components/structures/ScrollPanel.js
Normal file
288
src/components/structures/ScrollPanel.js
Normal file
|
@ -0,0 +1,288 @@
|
|||
/*
|
||||
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");
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
|
||||
var DEBUG_SCROLL = false;
|
||||
|
||||
/* This component implements an intelligent scrolling list.
|
||||
*
|
||||
* It wraps a list of <li> children; when items are added to the start or end
|
||||
* of the list, the scroll position is updated so that the user still sees the
|
||||
* same position in the list.
|
||||
*
|
||||
* It also provides a hook which allows parents to provide more list elements
|
||||
* when we get close to the start or end of the list.
|
||||
*
|
||||
* We don't save the absolute scroll offset, because that would be affected by
|
||||
* window width, zoom level, amount of scrollback, etc. Instead we save an
|
||||
* identifier for the last fully-visible message, and the number of pixels the
|
||||
* window was scrolled below it - which is hopefully be near enough.
|
||||
*
|
||||
* Each child element should have a 'data-scroll-token'. This token is used to
|
||||
* serialise the scroll state, and returned as the 'lastDisplayedScrollToken'
|
||||
* attribute by getScrollState().
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ScrollPanel',
|
||||
|
||||
propTypes: {
|
||||
/* stickyBottom: if set to true, then once the user hits the bottom of
|
||||
* the list, any new children added to the list will cause the list to
|
||||
* scroll down to show the new element, rather than preserving the
|
||||
* existing view.
|
||||
*/
|
||||
stickyBottom: React.PropTypes.bool,
|
||||
|
||||
/* onFillRequest(backwards): a callback which is called on scroll when
|
||||
* the user nears the start (backwards = true) or end (backwards =
|
||||
* false) of the list
|
||||
*/
|
||||
onFillRequest: React.PropTypes.func,
|
||||
|
||||
/* onScroll: a callback which is called whenever any scroll happens.
|
||||
*/
|
||||
onScroll: React.PropTypes.func,
|
||||
|
||||
/* className: classnames to add to the top-level div
|
||||
*/
|
||||
className: React.PropTypes.string,
|
||||
|
||||
/* style: styles to add to the top-level div
|
||||
*/
|
||||
style: React.PropTypes.object,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
stickyBottom: true,
|
||||
onFillRequest: function(backwards) {},
|
||||
onScroll: function() {},
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.resetScrollState();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
// after adding event tiles, we may need to tweak the scroll (either to
|
||||
// keep at the bottom of the timeline, or to maintain the view after
|
||||
// adding events to the top).
|
||||
this._restoreSavedScrollState();
|
||||
},
|
||||
|
||||
onScroll: function(ev) {
|
||||
var sn = this._getScrollNode();
|
||||
if (DEBUG_SCROLL) console.log("Scroll event: offset now:", sn.scrollTop, "recentEventScroll:", this.recentEventScroll);
|
||||
|
||||
// Sometimes we see attempts to write to scrollTop essentially being
|
||||
// ignored. (Or rather, it is successfully written, but on the next
|
||||
// scroll event, it's been reset again).
|
||||
//
|
||||
// This was observed on Chrome 47, when scrolling using the trackpad in OS
|
||||
// X Yosemite. Can't reproduce on El Capitan. Our theory is that this is
|
||||
// due to Chrome not being able to cope with the scroll offset being reset
|
||||
// while a two-finger drag is in progress.
|
||||
//
|
||||
// By way of a workaround, we detect this situation and just keep
|
||||
// resetting scrollTop until we see the scroll node have the right
|
||||
// value.
|
||||
if (this.recentEventScroll !== undefined) {
|
||||
if(sn.scrollTop < this.recentEventScroll-200) {
|
||||
console.log("Working around vector-im/vector-web#528");
|
||||
this._restoreSavedScrollState();
|
||||
return;
|
||||
}
|
||||
this.recentEventScroll = undefined;
|
||||
}
|
||||
|
||||
this.scrollState = this._calculateScrollState();
|
||||
if (DEBUG_SCROLL) console.log("Saved scroll state", this.scrollState);
|
||||
|
||||
this.props.onScroll(ev);
|
||||
|
||||
this.checkFillState();
|
||||
},
|
||||
|
||||
// return true if the content is fully scrolled down right now; else false.
|
||||
//
|
||||
// Note that if the content hasn't yet been fully populated, this may
|
||||
// spuriously return true even if the user wanted to be looking at earlier
|
||||
// content. So don't call it in render() cycles.
|
||||
isAtBottom: function() {
|
||||
var sn = this._getScrollNode();
|
||||
// + 1 here to avoid fractional pixel rounding errors
|
||||
return sn.scrollHeight - sn.scrollTop <= sn.clientHeight + 1;
|
||||
},
|
||||
|
||||
// check the scroll state and send out backfill requests if necessary.
|
||||
checkFillState: function() {
|
||||
var sn = this._getScrollNode();
|
||||
|
||||
if (sn.scrollTop < sn.clientHeight) {
|
||||
// there's less than a screenful of messages left - try to get some
|
||||
// more messages.
|
||||
this.props.onFillRequest(true);
|
||||
}
|
||||
},
|
||||
|
||||
// get the current scroll position of the room, so that it can be
|
||||
// restored later
|
||||
getScrollState: function() {
|
||||
return this.scrollState;
|
||||
},
|
||||
|
||||
/* reset the saved scroll state.
|
||||
*
|
||||
* This will cause the scroll to be reinitialised on the next update of the
|
||||
* child list.
|
||||
*
|
||||
* This is useful if the list is being replaced, and you don't want to
|
||||
* preserve scroll even if new children happen to have the same scroll
|
||||
* tokens as old ones.
|
||||
*/
|
||||
resetScrollState: function() {
|
||||
this.scrollState = null;
|
||||
},
|
||||
|
||||
scrollToTop: function() {
|
||||
this._getScrollNode().scrollTop = 0;
|
||||
if (DEBUG_SCROLL) console.log("Scrolled to top");
|
||||
},
|
||||
|
||||
scrollToBottom: function() {
|
||||
var scrollNode = this._getScrollNode();
|
||||
scrollNode.scrollTop = scrollNode.scrollHeight;
|
||||
if (DEBUG_SCROLL) console.log("Scrolled to bottom; offset now", scrollNode.scrollTop);
|
||||
},
|
||||
|
||||
// scroll the message list to the node with the given scrollToken. See
|
||||
// notes in _calculateScrollState on how this works.
|
||||
//
|
||||
// pixel_offset gives the number of pixels between the bottom of the node
|
||||
// and the bottom of the container.
|
||||
scrollToToken: function(scrollToken, pixelOffset) {
|
||||
/* find the dom node with the right scrolltoken */
|
||||
var node;
|
||||
var messages = this.refs.itemlist.children;
|
||||
for (var i = messages.length-1; i >= 0; --i) {
|
||||
var m = messages[i];
|
||||
if (!m.dataset.scrollToken) continue;
|
||||
if (m.dataset.scrollToken == scrollToken) {
|
||||
node = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
console.error("No node with scrollToken '"+scrollToken+"'");
|
||||
return;
|
||||
}
|
||||
|
||||
var scrollNode = this._getScrollNode();
|
||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||
if(scrollDelta != 0) {
|
||||
scrollNode.scrollTop += scrollDelta;
|
||||
|
||||
// see the comments in onMessageListScroll regarding recentEventScroll
|
||||
this.recentEventScroll = scrollNode.scrollTop;
|
||||
}
|
||||
|
||||
if (DEBUG_SCROLL) {
|
||||
console.log("Scrolled to token", node.dataset.scrollToken, "+", pixelOffset+":", scrollNode.scrollTop, "(delta: "+scrollDelta+")");
|
||||
console.log("recentEventScroll now "+this.recentEventScroll);
|
||||
}
|
||||
},
|
||||
|
||||
_calculateScrollState: function() {
|
||||
// Our scroll implementation is agnostic of the precise contents of the
|
||||
// message list (since it needs to work with both search results and
|
||||
// timelines). 'refs.messageList' is expected to be a DOM node with a
|
||||
// number of children, each of which may have a 'data-scroll-token'
|
||||
// attribute. It is this token which is stored as the
|
||||
// 'lastDisplayedScrollToken'.
|
||||
|
||||
var atBottom = this.isAtBottom();
|
||||
|
||||
var itemlist = this.refs.itemlist;
|
||||
var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||
var messages = itemlist.children;
|
||||
|
||||
for (var i = messages.length-1; i >= 0; --i) {
|
||||
var node = messages[i];
|
||||
if (!node.dataset.scrollToken) continue;
|
||||
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
if (boundingRect.bottom < wrapperRect.bottom) {
|
||||
return {
|
||||
atBottom: atBottom,
|
||||
lastDisplayedScrollToken: node.dataset.scrollToken,
|
||||
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apparently the entire timeline is below the viewport. Give up.
|
||||
return { atBottom: true };
|
||||
},
|
||||
|
||||
_restoreSavedScrollState: function() {
|
||||
var scrollState = this.scrollState;
|
||||
if (!scrollState || (this.props.stickyBottom && scrollState.atBottom)) {
|
||||
this.scrollToBottom();
|
||||
} else if (scrollState.lastDisplayedScrollToken) {
|
||||
this.scrollToToken(scrollState.lastDisplayedScrollToken,
|
||||
scrollState.pixelOffset);
|
||||
}
|
||||
},
|
||||
|
||||
/* get the DOM node which has the scrollTop property we care about for our
|
||||
* message panel.
|
||||
*/
|
||||
_getScrollNode: function() {
|
||||
var panel = ReactDOM.findDOMNode(this.refs.geminiPanel);
|
||||
|
||||
// If the gemini scrollbar is doing its thing, this will be a div within
|
||||
// the message panel (ie, the gemini container); otherwise it will be the
|
||||
// message panel itself.
|
||||
|
||||
if (panel.classList.contains('gm-prevented')) {
|
||||
return panel;
|
||||
} else {
|
||||
return panel.children[2]; // XXX: Fragile!
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// TODO: the classnames on the div and ol could do with being updated to
|
||||
// reflect the fact that we don't necessarily contain a list of messages.
|
||||
// it's not obvious why we have a separate div and ol anyway.
|
||||
return (<GeminiScrollbar autoshow={true} ref="geminiPanel" onScroll={ this.onScroll }
|
||||
className={this.props.className} style={this.props.style}>
|
||||
<div className="mx_RoomView_messageListWrapper">
|
||||
<ol ref="itemlist" className="mx_RoomView_MessageList" aria-live="polite">
|
||||
{this.props.children}
|
||||
</ol>
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
);
|
||||
},
|
||||
});
|
93
src/components/structures/UploadBar.js
Normal file
93
src/components/structures/UploadBar.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
Copyright 2015 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
var React = require('react');
|
||||
var ContentMessages = require('../../ContentMessages');
|
||||
var dis = require('../../dispatcher');
|
||||
var filesize = require('filesize');
|
||||
|
||||
module.exports = React.createClass({displayName: 'UploadBar',
|
||||
propTypes: {
|
||||
room: React.PropTypes.object
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
dis.register(this.onAction);
|
||||
this.mounted = true;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.mounted = false;
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
switch (payload.action) {
|
||||
case 'upload_progress':
|
||||
case 'upload_finished':
|
||||
case 'upload_failed':
|
||||
if (this.mounted) this.forceUpdate();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var uploads = ContentMessages.getCurrentUploads();
|
||||
if (uploads.length == 0) {
|
||||
return <div />
|
||||
}
|
||||
|
||||
var upload;
|
||||
for (var i = 0; i < uploads.length; ++i) {
|
||||
if (uploads[i].roomId == this.props.room.roomId) {
|
||||
upload = uploads[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!upload) {
|
||||
upload = uploads[0];
|
||||
}
|
||||
|
||||
var innerProgressStyle = {
|
||||
width: ((upload.loaded / (upload.total || 1)) * 100) + '%'
|
||||
};
|
||||
var uploadedSize = filesize(upload.loaded);
|
||||
var totalSize = filesize(upload.total);
|
||||
if (uploadedSize.replace(/^.* /,'') === totalSize.replace(/^.* /,'')) {
|
||||
uploadedSize = uploadedSize.replace(/ .*/, '');
|
||||
}
|
||||
|
||||
var others;
|
||||
if (uploads.length > 1) {
|
||||
others = 'and '+(uploads.length - 1) + ' other' + (uploads.length > 2 ? 's' : '');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UploadBar">
|
||||
<div className="mx_UploadBar_uploadProgressOuter">
|
||||
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
|
||||
</div>
|
||||
<img className="mx_UploadBar_uploadIcon" src="img/fileicon.png" width="17" height="22"/>
|
||||
<img className="mx_UploadBar_uploadCancel" src="img/cancel.svg" width="18" height="18"
|
||||
onClick={function() { ContentMessages.cancelUpload(upload.promise); }}
|
||||
/>
|
||||
<div className="mx_UploadBar_uploadBytes">
|
||||
{ uploadedSize } / { totalSize }
|
||||
</div>
|
||||
<div className="mx_UploadBar_uploadFilename">Uploading {upload.fileName}{others}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -17,14 +17,22 @@ var React = require('react');
|
|||
var sdk = require('../../index');
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var Modal = require('../../Modal');
|
||||
var dis = require("../../dispatcher");
|
||||
var q = require('q');
|
||||
var version = require('../../../package.json').version;
|
||||
var UserSettingsStore = require('../../UserSettingsStore');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'UserSettings',
|
||||
Phases: {
|
||||
Loading: "loading",
|
||||
Display: "display",
|
||||
|
||||
propTypes: {
|
||||
onClose: React.PropTypes.func
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onClose: function() {}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -32,131 +40,227 @@ module.exports = React.createClass({
|
|||
avatarUrl: null,
|
||||
threePids: [],
|
||||
clientVersion: version,
|
||||
phase: this.Phases.Loading,
|
||||
phase: "UserSettings.LOADING", // LOADING, DISPLAY
|
||||
};
|
||||
},
|
||||
|
||||
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); }
|
||||
);
|
||||
this._refreshFromServer();
|
||||
},
|
||||
|
||||
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);
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this._me = MatrixClientPeg.get().credentials.userId;
|
||||
},
|
||||
|
||||
addEmail: function() {
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
editDisplayName: function() {
|
||||
this.refs.displayname.edit();
|
||||
_refreshFromServer: function() {
|
||||
var self = this;
|
||||
q.all([
|
||||
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids()
|
||||
]).done(function(resps) {
|
||||
self.setState({
|
||||
avatarUrl: resps[0].avatar_url,
|
||||
threepids: resps[1].threepids,
|
||||
phase: "UserSettings.DISPLAY",
|
||||
});
|
||||
}, function(error) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Can't load user settings",
|
||||
description: error.toString()
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
changePassword: function() {
|
||||
var ChangePassword = sdk.getComponent('settings.ChangePassword');
|
||||
Modal.createDialog(ChangePassword);
|
||||
onAction: function(payload) {
|
||||
if (payload.action === "notifier_enabled") {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
onAvatarSelected: function(ev) {
|
||||
var self = this;
|
||||
var changeAvatar = this.refs.changeAvatar;
|
||||
if (!changeAvatar) {
|
||||
console.error("No ChangeAvatar found to upload image to!");
|
||||
return;
|
||||
}
|
||||
changeAvatar.onFileSelected(ev).done(function() {
|
||||
// dunno if the avatar changed, re-check it.
|
||||
self._refreshFromServer();
|
||||
}, function(err) {
|
||||
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: "Failed to set avatar. " + errMsg
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onLogoutClicked: function(ev) {
|
||||
var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt');
|
||||
this.logoutModal = Modal.createDialog(LogoutPrompt, {onCancel: this.onLogoutPromptCancel});
|
||||
this.logoutModal = Modal.createDialog(
|
||||
LogoutPrompt, {onCancel: this.onLogoutPromptCancel}
|
||||
);
|
||||
},
|
||||
|
||||
onPasswordChangeError: function(err) {
|
||||
var errMsg = err.error || "";
|
||||
if (err.httpStatus === 403) {
|
||||
errMsg = "Failed to change password. Is your password correct?";
|
||||
}
|
||||
else if (err.httpStatus) {
|
||||
errMsg += ` (HTTP status ${err.httpStatus})`;
|
||||
}
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Error",
|
||||
description: errMsg
|
||||
});
|
||||
},
|
||||
|
||||
onPasswordChanged: function() {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Success",
|
||||
description: `Your password was successfully changed. You will not
|
||||
receive push notifications on other devices until you
|
||||
log back in to them.`
|
||||
});
|
||||
},
|
||||
|
||||
onLogoutPromptCancel: function() {
|
||||
this.logoutModal.closeDialog();
|
||||
},
|
||||
|
||||
onAvatarDialogCancel: function() {
|
||||
this.avatarDialog.close();
|
||||
onEnableNotificationsChange: function(event) {
|
||||
UserSettingsStore.setEnableNotifications(event.target.checked);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
if (this.state.phase === this.Phases.Loading) {
|
||||
return <Loader />
|
||||
switch (this.state.phase) {
|
||||
case "UserSettings.LOADING":
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader />
|
||||
);
|
||||
case "UserSettings.DISPLAY":
|
||||
break; // quit the switch to return the common state
|
||||
default:
|
||||
throw new Error("Unknown state.phase => " + this.state.phase);
|
||||
}
|
||||
else if (this.state.phase === this.Phases.Display) {
|
||||
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
||||
var EnableNotificationsButton = sdk.getComponent('settings.EnableNotificationsButton');
|
||||
return (
|
||||
// can only get here if phase is UserSettings.DISPLAY
|
||||
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
|
||||
var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
|
||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
var avatarUrl = (
|
||||
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
||||
);
|
||||
|
||||
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
|
||||
<RoomHeader simpleHeader="Settings" />
|
||||
|
||||
<h2>Profile</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<div className="mx_UserSettings_profileTable">
|
||||
<div className="mx_UserSettings_profileTableRow">
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label htmlFor="displayName">Display name</label>
|
||||
</div>
|
||||
<div className="mx_UserSettings_Avatar_Edit" onClick={this.editAvatar}>
|
||||
Edit
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<ChangeDisplayName />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx_UserSettings_DisplayName">
|
||||
<ChangeDisplayName ref="displayname" />
|
||||
<div className="mx_UserSettings_DisplayName_Edit" onClick={this.editDisplayName}>
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
{this.state.threepids.map(function(val, pidIndex) {
|
||||
var id = "email-" + val.address;
|
||||
return (
|
||||
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label htmlFor={id}>Email</label>
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<input key={val.address} id={id} value={val.address} disabled />
|
||||
</div>
|
||||
</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 className="mx_UserSettings_avatarPicker">
|
||||
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
|
||||
<div className="mx_UserSettings_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput">
|
||||
<img src="img/upload.svg"
|
||||
alt="Upload avatar" title="Upload avatar"
|
||||
width="19" height="24" />
|
||||
</label>
|
||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
|
||||
</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>
|
||||
<h2>Account</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<ChangePassword
|
||||
className="mx_UserSettings_accountTable"
|
||||
rowClassName="mx_UserSettings_profileTableRow"
|
||||
rowLabelClassName="mx_UserSettings_profileLabelCell"
|
||||
rowInputClassName="mx_UserSettings_profileInputCell"
|
||||
buttonClassName="mx_UserSettings_button"
|
||||
onError={this.onPasswordChangeError}
|
||||
onFinished={this.onPasswordChanged} />
|
||||
</div>
|
||||
|
||||
<div className="mx_UserSettings_logout">
|
||||
<div className="mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
||||
Log out
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Notifications</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<div className="mx_UserSettings_notifTable">
|
||||
<div className="mx_UserSettings_notifTableRow">
|
||||
<div className="mx_UserSettings_notifInputCell">
|
||||
<input id="enableNotifications"
|
||||
ref="enableNotifications"
|
||||
type="checkbox"
|
||||
checked={ UserSettingsStore.getEnableNotifications() }
|
||||
onChange={ this.onEnableNotificationsChange } />
|
||||
</div>
|
||||
<div className="mx_UserSettings_notifLabelCell">
|
||||
<label htmlFor="enableNotifications">
|
||||
Enable desktop notifications
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Advanced</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<div className="mx_UserSettings_advanced">
|
||||
Logged in as {this._me}
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
Version {this.state.clientVersion}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -39,6 +39,7 @@ module.exports = React.createClass({
|
|||
idSid: React.PropTypes.string,
|
||||
hsUrl: React.PropTypes.string,
|
||||
isUrl: React.PropTypes.string,
|
||||
email: React.PropTypes.string,
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
@ -185,6 +186,7 @@ module.exports = React.createClass({
|
|||
registerStep = (
|
||||
<RegistrationForm
|
||||
showEmail={true}
|
||||
defaultEmail={this.props.email}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
onError={this.onFormValidationFailed}
|
||||
onRegisterClick={this.onFormSubmit} />
|
||||
|
|
|
@ -56,8 +56,9 @@ module.exports = React.createClass({
|
|||
|
||||
if (this.props.homeserver) {
|
||||
if (curr_val == "") {
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
target.value = "#:" + this.props.homeserver;
|
||||
target.value = "#:" + self.props.homeserver;
|
||||
target.setSelectionRange(1, 1);
|
||||
}, 0);
|
||||
} else {
|
||||
|
|
|
@ -113,6 +113,10 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onBlur: function() {
|
||||
this.cancelEdit();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var editable_el;
|
||||
|
||||
|
@ -125,7 +129,8 @@ module.exports = React.createClass({
|
|||
} 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/>
|
||||
<input type="text" defaultValue={this.state.value}
|
||||
onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur} placeholder={this.props.placeHolder} autoFocus/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
<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" />
|
||||
placeholder="Email or user name" autoFocus />
|
||||
<br />
|
||||
<input className="mx_Login_field" ref="pass" type="password"
|
||||
value={this.state.password} onChange={this.onPasswordChanged}
|
||||
|
|
|
@ -54,8 +54,8 @@ module.exports = React.createClass({
|
|||
|
||||
if (httpUrl) {
|
||||
return (
|
||||
<span className="mx_MFileTile">
|
||||
<div className="mx_MImageTile_download">
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
||||
<img src="img/download.png" width="10" height="12"/>
|
||||
Download {text}
|
||||
|
@ -65,7 +65,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
} else {
|
||||
var extra = text ? ': '+text : '';
|
||||
return <span className="mx_MFileTile">
|
||||
return <span className="mx_MFileBody">
|
||||
Invalid file{extra}
|
||||
</span>
|
||||
}
|
||||
|
|
|
@ -48,18 +48,23 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onClick: function(ev) {
|
||||
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");
|
||||
Modal.createDialog(ImageView, {
|
||||
var params = {
|
||||
src: httpUrl,
|
||||
width: content.info.w,
|
||||
height: content.info.h,
|
||||
mxEvent: this.props.mxEvent,
|
||||
}, "mx_Dialog_lightbox");
|
||||
mxEvent: this.props.mxEvent
|
||||
};
|
||||
|
||||
if (content.info) {
|
||||
params.width = content.info.w;
|
||||
params.height = content.info.h;
|
||||
}
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -104,14 +109,14 @@ module.exports = React.createClass({
|
|||
var thumbUrl = this._getThumbUrl();
|
||||
if (thumbUrl) {
|
||||
return (
|
||||
<span className="mx_MImageTile">
|
||||
<span className="mx_MImageBody">
|
||||
<a href={cli.mxcUrlToHttp(content.url)} onClick={ this.onClick }>
|
||||
<img className="mx_MImageTile_thumbnail" src={thumbUrl}
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl}
|
||||
alt={content.body} style={imgStyle}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
</a>
|
||||
<div className="mx_MImageTile_download">
|
||||
<div className="mx_MImageBody_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" })
|
||||
|
@ -121,13 +126,13 @@ module.exports = React.createClass({
|
|||
);
|
||||
} else if (content.body) {
|
||||
return (
|
||||
<span className="mx_MImageTile">
|
||||
<span className="mx_MImageBody">
|
||||
Image '{content.body}' cannot be displayed.
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_MImageTile">
|
||||
<span className="mx_MImageBody">
|
||||
This image cannot be displayed.
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -70,8 +70,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<span className="mx_MVideoTile">
|
||||
<video className="mx_MVideoTile" src={cli.mxcUrlToHttp(content.url)} alt={content.body}
|
||||
<span className="mx_MVideoBody">
|
||||
<video className="mx_MVideoBody" src={cli.mxcUrlToHttp(content.url)} alt={content.body}
|
||||
controls preload={preload} autoPlay="0"
|
||||
height={height} width={width} poster={poster}>
|
||||
</video>
|
||||
|
|
|
@ -47,6 +47,7 @@ module.exports = React.createClass({
|
|||
TileType = tileTypes[msgtype];
|
||||
}
|
||||
|
||||
return <TileType mxEvent={this.props.mxEvent} highlights={this.props.highlights} />;
|
||||
return <TileType mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
||||
onHighlightClick={this.props.onHighlightClick} />;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -49,25 +49,26 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
var mxEvent = this.props.mxEvent;
|
||||
var content = mxEvent.getContent();
|
||||
var body = HtmlUtils.bodyToHtml(content, this.props.highlights);
|
||||
var body = HtmlUtils.bodyToHtml(content, this.props.highlights,
|
||||
{onHighlightClick: this.props.onHighlightClick});
|
||||
|
||||
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">
|
||||
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
|
||||
* { name } { body }
|
||||
</span>
|
||||
);
|
||||
case "m.notice":
|
||||
return (
|
||||
<span ref="content" className="mx_MNoticeTile mx_MessageTile_content">
|
||||
<span ref="content" className="mx_MNoticeBody mx_EventTile_content">
|
||||
{ body }
|
||||
</span>
|
||||
);
|
||||
default: // including "m.text"
|
||||
return (
|
||||
<span ref="content" className="mx_MTextTile mx_MessageTile_content">
|
||||
<span ref="content" className="mx_MTextBody mx_EventTile_content">
|
||||
{ body }
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -34,7 +34,7 @@ module.exports = React.createClass({
|
|||
if (text == null || text.length == 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mx_EventAsTextTile">
|
||||
<div className="mx_TextualEvent">
|
||||
{TextForEvent.textForEvent(this.props.mxEvent)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -24,7 +24,7 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
var content = this.props.mxEvent.getContent();
|
||||
return (
|
||||
<span className="mx_UnknownMessageTile">
|
||||
<span className="mx_UnknownBody">
|
||||
{content.body}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -44,6 +44,7 @@ var eventTileTypes = {
|
|||
'm.call.hangup' : 'messages.TextualEvent',
|
||||
'm.room.name' : 'messages.TextualEvent',
|
||||
'm.room.topic' : 'messages.TextualEvent',
|
||||
'm.room.third_party_invite': 'messages.TextualEvent'
|
||||
};
|
||||
|
||||
var MAX_READ_AVATARS = 5;
|
||||
|
@ -73,6 +74,32 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: React.PropTypes.object.isRequired,
|
||||
|
||||
/* true if this is a continuation of the previous event (which has the
|
||||
* effect of not showing another avatar/displayname
|
||||
*/
|
||||
continuation: React.PropTypes.bool,
|
||||
|
||||
/* true if this is the last event in the timeline (which has the effect
|
||||
* of always showing the timestamp)
|
||||
*/
|
||||
last: React.PropTypes.bool,
|
||||
|
||||
/* true if this is search context (which has the effect of greying out
|
||||
* the text
|
||||
*/
|
||||
contextual: React.PropTypes.bool,
|
||||
|
||||
/* a list of words to highlight */
|
||||
highlights: React.PropTypes.array,
|
||||
|
||||
/* a function to be called when the highlight is clicked */
|
||||
onHighlightClick: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {menu: false, allReadAvatars: false};
|
||||
},
|
||||
|
@ -133,6 +160,9 @@ module.exports = React.createClass({
|
|||
|
||||
for (var i = 0; i < receipts.length; ++i) {
|
||||
var member = room.getMember(receipts[i].userId);
|
||||
if (!member) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Using react refs here would mean both getting Velociraptor to expose
|
||||
// them and making them scoped to the whole RoomView. Not impossible, but
|
||||
|
@ -279,7 +309,8 @@ module.exports = React.createClass({
|
|||
{ avatar }
|
||||
{ sender }
|
||||
<div className="mx_EventTile_line">
|
||||
<EventTileType mxEvent={this.props.mxEvent} highlights={this.props.highlights} />
|
||||
<EventTileType mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
||||
onHighlightClick={this.props.onHighlightClick} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -43,11 +43,16 @@ module.exports = React.createClass({
|
|||
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("dialogs.ErrorDialog");
|
||||
var roomId = this.props.member.roomId;
|
||||
|
@ -221,36 +226,10 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
// FIXME: this is horribly duplicated with MemberTile's onLeaveClick.
|
||||
// Not sure what the right solution to this is.
|
||||
onLeaveClick: function() {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var QuestionDialog = sdk.getComponent("dialogs.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("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()
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.props.member.roomId,
|
||||
});
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
@ -269,13 +248,13 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_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;
|
||||
}
|
||||
|
@ -286,7 +265,7 @@ module.exports = React.createClass({
|
|||
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()
|
||||
|
@ -377,7 +356,7 @@ module.exports = React.createClass({
|
|||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<img className="mx_MemberInfo_cancel" src="img/cancel-black.png" width="18" height="18" onClick={this.onCancel}/>
|
||||
<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>
|
||||
|
|
|
@ -31,33 +31,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onLeaveClick: function() {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.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("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()
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.props.member.roomId,
|
||||
});
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
var React = require("react");
|
||||
|
||||
var marked = require("marked");
|
||||
marked.setOptions({
|
||||
renderer: new marked.Renderer(),
|
||||
|
@ -25,9 +26,12 @@ marked.setOptions({
|
|||
smartLists: true,
|
||||
smartypants: false
|
||||
});
|
||||
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var SlashCommands = require("../../../SlashCommands");
|
||||
var Modal = require("../../../Modal");
|
||||
var CallHandler = require('../../../CallHandler');
|
||||
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
|
||||
var sdk = require('../../../index');
|
||||
|
||||
var dis = require("../../../dispatcher");
|
||||
|
@ -61,14 +65,13 @@ function mdownToHtml(mdown) {
|
|||
module.exports = React.createClass({
|
||||
displayName: 'MessageComposer',
|
||||
|
||||
propTypes: {
|
||||
tabComplete: React.PropTypes.any
|
||||
},
|
||||
|
||||
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
|
||||
|
@ -169,6 +172,9 @@ module.exports = React.createClass({
|
|||
this.props.room.roomId
|
||||
);
|
||||
this.resizeInput();
|
||||
if (this.props.tabComplete) {
|
||||
this.props.tabComplete.setTextArea(this.refs.textarea);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -194,13 +200,6 @@ module.exports = React.createClass({
|
|||
this.sentHistory.push(input);
|
||||
this.onEnter(ev);
|
||||
}
|
||||
else if (ev.keyCode === KeyCode.TAB) {
|
||||
var members = [];
|
||||
if (this.props.room) {
|
||||
members = this.props.room.getJoinedMembers();
|
||||
}
|
||||
this.onTab(ev, members);
|
||||
}
|
||||
else if (ev.keyCode === KeyCode.UP) {
|
||||
var input = this.refs.textarea.value;
|
||||
var offset = this.refs.textarea.selectionStart || 0;
|
||||
|
@ -219,10 +218,9 @@ module.exports = React.createClass({
|
|||
this.resizeInput();
|
||||
}
|
||||
}
|
||||
else if (ev.keyCode !== KeyCode.SHIFT && this.tabStruct.completing) {
|
||||
// they're resuming typing; reset tab complete state vars.
|
||||
this.tabStruct.completing = false;
|
||||
this.tabStruct.index = 0;
|
||||
|
||||
if (this.props.tabComplete) {
|
||||
this.props.tabComplete.onKeyDown(ev);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
@ -316,6 +314,9 @@ module.exports = React.createClass({
|
|||
if (isEmote) {
|
||||
contentText = contentText.substring(4);
|
||||
}
|
||||
else if (contentText[0] === '/') {
|
||||
contentText = contentText.substring(1);
|
||||
}
|
||||
|
||||
var htmlText;
|
||||
if (this.markdownEnabled && (htmlText = mdownToHtml(contentText)) !== contentText) {
|
||||
|
@ -343,104 +344,6 @@ module.exports = React.createClass({
|
|||
ev.preventDefault();
|
||||
},
|
||||
|
||||
onTab: function(ev, sortedMembers) {
|
||||
var textArea = this.refs.textarea;
|
||||
if (!this.tabStruct.completing) {
|
||||
this.tabStruct.completing = true;
|
||||
this.tabStruct.index = 0;
|
||||
// cache starting text
|
||||
this.tabStruct.original = textArea.value;
|
||||
}
|
||||
|
||||
// loop in the right direction
|
||||
if (ev.shiftKey) {
|
||||
this.tabStruct.index --;
|
||||
if (this.tabStruct.index < 0) {
|
||||
// wrap to the last search match, and fix up to a real index
|
||||
// value after we've matched.
|
||||
this.tabStruct.index = Number.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.tabStruct.index++;
|
||||
}
|
||||
|
||||
var searchIndex = 0;
|
||||
var targetIndex = this.tabStruct.index;
|
||||
var text = this.tabStruct.original;
|
||||
|
||||
var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text);
|
||||
// console.log("Searched in '%s' - got %s", text, search);
|
||||
if (targetIndex === 0) { // 0 is always the original text
|
||||
textArea.value = text;
|
||||
}
|
||||
else if (search && search[1]) {
|
||||
// console.log("search found: " + search+" from "+text);
|
||||
var expansion;
|
||||
|
||||
// FIXME: could do better than linear search here
|
||||
for (var i=0; i<sortedMembers.length; i++) {
|
||||
var member = sortedMembers[i];
|
||||
if (member.name && searchIndex < targetIndex) {
|
||||
if (member.name.toLowerCase().indexOf(search[1].toLowerCase()) === 0) {
|
||||
expansion = member.name;
|
||||
searchIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (searchIndex < targetIndex) { // then search raw mxids
|
||||
for (var i=0; i<sortedMembers.length; i++) {
|
||||
if (searchIndex >= targetIndex) {
|
||||
break;
|
||||
}
|
||||
var userId = sortedMembers[i].userId;
|
||||
// === 1 because mxids are @username
|
||||
if (userId.toLowerCase().indexOf(search[1].toLowerCase()) === 1) {
|
||||
expansion = userId;
|
||||
searchIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (searchIndex === targetIndex ||
|
||||
targetIndex === Number.MAX_VALUE) {
|
||||
// xchat-style tab complete, add a colon if tab
|
||||
// completing at the start of the text
|
||||
if (search[0].length === text.length) {
|
||||
expansion += ": ";
|
||||
}
|
||||
else {
|
||||
expansion += " ";
|
||||
}
|
||||
textArea.value = text.replace(
|
||||
/@?([a-zA-Z0-9_\-:\.]+)$/, expansion
|
||||
);
|
||||
// cancel blink
|
||||
textArea.style["background-color"] = "";
|
||||
if (targetIndex === Number.MAX_VALUE) {
|
||||
// wrap the index around to the last index found
|
||||
this.tabStruct.index = searchIndex;
|
||||
targetIndex = searchIndex;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// console.log("wrapped!");
|
||||
textArea.style["background-color"] = "#faa";
|
||||
setTimeout(function() {
|
||||
textArea.style["background-color"] = "";
|
||||
}, 150);
|
||||
textArea.value = text;
|
||||
this.tabStruct.index = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.tabStruct.index = 0;
|
||||
}
|
||||
// prevent the default TAB operation (typically focus shifting)
|
||||
ev.preventDefault();
|
||||
},
|
||||
|
||||
onTypingActivity: function() {
|
||||
this.isTyping = true;
|
||||
if (!this.userTypingTimer) {
|
||||
|
@ -524,6 +427,20 @@ module.exports = React.createClass({
|
|||
this.refs.uploadInput.value = null;
|
||||
},
|
||||
|
||||
onHangupClick: function() {
|
||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
//var call = CallHandler.getAnyActiveCall();
|
||||
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',
|
||||
|
@ -544,6 +461,27 @@ module.exports = React.createClass({
|
|||
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);
|
||||
//var call = CallHandler.getAnyActiveCall();
|
||||
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">
|
||||
|
@ -555,15 +493,12 @@ module.exports = React.createClass({
|
|||
<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.png" alt="Upload file" title="Upload file" width="17" height="22"/>
|
||||
<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>
|
||||
<div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick}>
|
||||
<img src="img/voice.png" alt="Voice call" title="Voice call" width="16" height="26"/>
|
||||
</div>
|
||||
<div className="mx_MessageComposer_videocall" onClick={this.onCallClick}>
|
||||
<img src="img/call.png" alt="Video call" title="Video call" width="28" height="20"/>
|
||||
</div>
|
||||
{ hangupButton }
|
||||
{ callButton }
|
||||
{ videoCallButton }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,15 +16,9 @@ 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 sdk = require('../../../index');
|
||||
var dis = require("../../../dispatcher");
|
||||
var CallHandler = require("../../../CallHandler");
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
|
||||
module.exports = React.createClass({
|
||||
|
@ -35,6 +29,8 @@ module.exports = React.createClass({
|
|||
editing: React.PropTypes.bool,
|
||||
onSettingsClick: React.PropTypes.func,
|
||||
onSaveClick: React.PropTypes.func,
|
||||
onSearchClick: React.PropTypes.func,
|
||||
onLeaveClick: React.PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -45,34 +41,6 @@ module.exports = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
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',
|
||||
|
@ -80,6 +48,7 @@ module.exports = React.createClass({
|
|||
room_id: this.props.room.roomId
|
||||
});
|
||||
},
|
||||
|
||||
onVoiceClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
|
@ -87,38 +56,6 @@ module.exports = React.createClass({
|
|||
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
|
||||
});
|
||||
},
|
||||
|
||||
onNameChange: function(new_name) {
|
||||
if (this.props.room.name != new_name && new_name) {
|
||||
|
@ -129,10 +66,6 @@ module.exports = React.createClass({
|
|||
getRoomName: function() {
|
||||
return this.refs.name_edit.value;
|
||||
},
|
||||
|
||||
onFullscreenClick: function() {
|
||||
dis.dispatch({action: 'video_fullscreen', fullscreen: true}, true);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var EditableText = sdk.getComponent("elements.EditableText");
|
||||
|
@ -140,53 +73,23 @@ module.exports = React.createClass({
|
|||
|
||||
var header;
|
||||
if (this.props.simpleHeader) {
|
||||
var cancel;
|
||||
if (this.props.onCancelClick) {
|
||||
cancel = <img className="mx_RoomHeader_simpleHeaderCancel" src="img/cancel-black.png" onClick={ this.props.onCancelClick } alt="Close" width="18" height="18"/>
|
||||
}
|
||||
header =
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_simpleHeader">
|
||||
{ this.props.simpleHeader }
|
||||
{ cancel }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
|
||||
var call_buttons;
|
||||
if (this.state && this.state.call_state != 'ended') {
|
||||
//var muteVideoButton;
|
||||
var activeCall = (
|
||||
CallHandler.getCallForRoom(this.props.room.roomId)
|
||||
);
|
||||
/*
|
||||
if (activeCall && activeCall.type === "video") {
|
||||
muteVideoButton = (
|
||||
<div className="mx_RoomHeader_textButton mx_RoomHeader_voipButton"
|
||||
onClick={this.onMuteVideoClick}>
|
||||
{
|
||||
(activeCall.isLocalVideoMuted() ?
|
||||
"Unmute" : "Mute") + " video"
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{muteVideoButton}
|
||||
<div className="mx_RoomHeader_textButton mx_RoomHeader_voipButton"
|
||||
onClick={this.onMuteAudioClick}>
|
||||
{
|
||||
(activeCall && activeCall.isMicrophoneMuted() ?
|
||||
"Unmute" : "Mute") + " audio"
|
||||
}
|
||||
</div>
|
||||
*/
|
||||
|
||||
call_buttons = (
|
||||
<div className="mx_RoomHeader_textButton"
|
||||
onClick={this.onHangupClick}>
|
||||
End call
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var name = null;
|
||||
var searchStatus = null;
|
||||
var topic_el = null;
|
||||
var cancel_button = null;
|
||||
var save_button = null;
|
||||
|
@ -203,11 +106,20 @@ module.exports = React.createClass({
|
|||
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;
|
||||
// don't display the search count until the search completes and
|
||||
// gives us a non-null searchCount.
|
||||
if (this.props.searchInfo && this.props.searchInfo.searchCount !== null) {
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus"> (~{ this.props.searchInfo.searchCount } results)</div>;
|
||||
}
|
||||
|
||||
name =
|
||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||
<div className="mx_RoomHeader_nametext">{ this.props.room.name }</div>
|
||||
<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.png" width="12" height="12"/>
|
||||
<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>;
|
||||
|
@ -220,23 +132,22 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
var zoom_button, video_button, voice_button;
|
||||
if (activeCall) {
|
||||
if (activeCall.type == "video") {
|
||||
zoom_button = (
|
||||
<div className="mx_RoomHeader_button" onClick={this.onFullscreenClick}>
|
||||
<img src="img/zoom.png" title="Fullscreen" alt="Fullscreen" width="32" height="32" style={{ 'marginTop': '-5px' }}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
video_button =
|
||||
<div className="mx_RoomHeader_button mx_RoomHeader_video" onClick={activeCall && activeCall.type === "video" ? this.onMuteVideoClick : this.onVideoClick}>
|
||||
<img src="img/video.png" title="Video call" alt="Video call" width="32" height="32" style={{ 'marginTop': '-8px' }}/>
|
||||
</div>;
|
||||
voice_button =
|
||||
<div className="mx_RoomHeader_button mx_RoomHeader_voice" onClick={activeCall ? this.onMuteAudioClick : this.onVoiceClick}>
|
||||
<img src="img/voip.png" title="VoIP call" alt="VoIP call" width="32" height="32" style={{ 'marginTop': '-8px' }}/>
|
||||
</div>;
|
||||
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>;
|
||||
}
|
||||
|
||||
var forget_button;
|
||||
if (this.props.onForgetClick) {
|
||||
forget_button =
|
||||
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton">
|
||||
<img src="img/leave.svg" title="Forget room" alt="Forget room"
|
||||
width="26" height="20" onClick={this.props.onForgetClick}/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
header =
|
||||
|
@ -250,15 +161,13 @@ module.exports = React.createClass({
|
|||
{ topic_el }
|
||||
</div>
|
||||
</div>
|
||||
{call_buttons}
|
||||
{cancel_button}
|
||||
{save_button}
|
||||
<div className="mx_RoomHeader_rightRow">
|
||||
{ video_button }
|
||||
{ voice_button }
|
||||
{ zoom_button }
|
||||
{ forget_button }
|
||||
{ leave_button }
|
||||
<div className="mx_RoomHeader_button">
|
||||
<img src="img/search.png" title="Search" alt="Search" width="21" height="19" onClick={this.props.onSearchClick}/>
|
||||
<img src="img/search.svg" title="Search" alt="Search" width="21" height="19" onClick={this.props.onSearchClick}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,9 @@ var React = require("react");
|
|||
var ReactDOM = require("react-dom");
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var CallHandler = require('../../../CallHandler');
|
||||
var RoomListSorter = require("../../../RoomListSorter");
|
||||
var UnreadStatus = require('../../../UnreadStatus');
|
||||
var dis = require("../../../dispatcher");
|
||||
var sdk = require('../../../index');
|
||||
|
||||
|
@ -37,13 +39,16 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
activityMap: null,
|
||||
isLoadingLeftRooms: false,
|
||||
lists: {},
|
||||
incomingCall: null,
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
cli.on("Room", this.onRoom);
|
||||
cli.on("deleteRoom", this.onDeleteRoom);
|
||||
cli.on("Room.timeline", this.onRoomTimeline);
|
||||
cli.on("Room.name", this.onRoomName);
|
||||
cli.on("Room.tags", this.onRoomTags);
|
||||
|
@ -65,7 +70,21 @@ module.exports = React.createClass({
|
|||
this.tooltip = payload.tooltip;
|
||||
this._repositionTooltip();
|
||||
if (this.tooltip) this.tooltip.style.display = 'block';
|
||||
break
|
||||
break;
|
||||
case 'call_state':
|
||||
var call = CallHandler.getCall(payload.room_id);
|
||||
if (call && call.call_state === 'ringing') {
|
||||
this.setState({
|
||||
incomingCall: call
|
||||
});
|
||||
this._repositionIncomingCallBox(undefined, true);
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
incomingCall: null
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -87,7 +106,26 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onRoom: function(room) {
|
||||
this.refreshRoomList();
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onDeleteRoom: function(roomId) {
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onArchivedHeaderClick: function(isHidden) {
|
||||
if (!isHidden) {
|
||||
var self = this;
|
||||
this.setState({ isLoadingLeftRooms: true });
|
||||
// we don't care about the response since it comes down via "Room"
|
||||
// events.
|
||||
MatrixClientPeg.get().syncLeftRooms().catch(function(err) {
|
||||
console.error("Failed to sync left rooms: %s", err);
|
||||
console.error(err);
|
||||
}).finally(function() {
|
||||
self.setState({ isLoadingLeftRooms: false });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
||||
|
@ -98,47 +136,85 @@ module.exports = React.createClass({
|
|||
room.roomId != this.props.selectedRoom &&
|
||||
ev.getSender() != MatrixClientPeg.get().credentials.userId)
|
||||
{
|
||||
// don't mark rooms as unread for just member changes
|
||||
if (ev.getType() != "m.room.member") {
|
||||
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) {
|
||||
if ((actions && actions.tweaks && actions.tweaks.highlight) ||
|
||||
(me && me.membership == "invite"))
|
||||
{
|
||||
hl = 2;
|
||||
}
|
||||
}
|
||||
|
||||
var newState = this.getRoomLists();
|
||||
if (hl > 0) {
|
||||
var newState = this.getRoomLists();
|
||||
|
||||
// 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);
|
||||
}
|
||||
// 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();
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onRoomTags: function(event, room) {
|
||||
this.refreshRoomList();
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
setTimeout(this.refreshRoomList, 0);
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onRoomMemberName: function(ev, member) {
|
||||
setTimeout(this.refreshRoomList, 0);
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
_delayedRefreshRoomList: function() {
|
||||
// There can be 1000s of JS SDK events when rooms are initially synced;
|
||||
// we don't want to do lots of work rendering until things have settled.
|
||||
// Therefore, keep a 1s refresh buffer which will refresh the room list
|
||||
// at MOST once every 1s to prevent thrashing.
|
||||
var MAX_REFRESH_INTERVAL_MS = 1000;
|
||||
var self = this;
|
||||
|
||||
if (!self._lastRefreshRoomListTs) {
|
||||
self.refreshRoomList(); // first refresh evar
|
||||
}
|
||||
else {
|
||||
var timeWaitedMs = Date.now() - self._lastRefreshRoomListTs;
|
||||
if (timeWaitedMs > MAX_REFRESH_INTERVAL_MS) {
|
||||
clearTimeout(self._refreshRoomListTimerId);
|
||||
self._refreshRoomListTimerId = null;
|
||||
self.refreshRoomList(); // refreshed more than MAX_REFRESH_INTERVAL_MS ago
|
||||
}
|
||||
else {
|
||||
// refreshed less than MAX_REFRESH_INTERVAL_MS ago, wait the difference
|
||||
// if we aren't already waiting. If we are waiting then NOP, it will
|
||||
// fire soon, promise!
|
||||
if (!self._refreshRoomListTimerId) {
|
||||
self._refreshRoomListTimerId = setTimeout(function() {
|
||||
self.refreshRoomList();
|
||||
}, 10 + MAX_REFRESH_INTERVAL_MS - timeWaitedMs); // 10 is a buffer amount
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
refreshRoomList: function() {
|
||||
// console.log("DEBUG: Refresh room list delta=%s ms",
|
||||
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
|
||||
// );
|
||||
|
||||
// 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
|
||||
|
@ -146,27 +222,31 @@ module.exports = React.createClass({
|
|||
// us re-rendering all the sublists every time anything changes anywhere
|
||||
// in the state of the client.
|
||||
this.setState(this.getRoomLists());
|
||||
this._lastRefreshRoomListTs = Date.now();
|
||||
},
|
||||
|
||||
getRoomLists: function() {
|
||||
var self = this;
|
||||
var s = { lists: {} };
|
||||
|
||||
s.lists["m.invite"] = [];
|
||||
s.lists["im.vector.fake.invite"] = [];
|
||||
s.lists["m.favourite"] = [];
|
||||
s.lists["m.recent"] = [];
|
||||
s.lists["im.vector.fake.recent"] = [];
|
||||
s.lists["m.lowpriority"] = [];
|
||||
s.lists["m.archived"] = [];
|
||||
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["m.invite"].push(room);
|
||||
s.lists["im.vector.fake.invite"].push(room);
|
||||
}
|
||||
else if (me && me.membership === "leave") {
|
||||
s.lists["im.vector.fake.archived"].push(room);
|
||||
}
|
||||
else {
|
||||
var shouldShowRoom = (
|
||||
me && (me.membership == "join")
|
||||
me && (me.membership == "join" || me.membership === "ban")
|
||||
);
|
||||
|
||||
// hiding conf rooms only ever toggles shouldShowRoom to false
|
||||
|
@ -178,7 +258,7 @@ module.exports = React.createClass({
|
|||
return m.userId !== me.userId
|
||||
})[0];
|
||||
var ConfHandler = self.props.ConferenceHandler;
|
||||
if (ConfHandler && ConfHandler.isConferenceUser(otherMember)) {
|
||||
if (ConfHandler && ConfHandler.isConferenceUser(otherMember.userId)) {
|
||||
// console.log("Hiding conference 1:1 room %s", room.roomId);
|
||||
shouldShowRoom = false;
|
||||
}
|
||||
|
@ -195,23 +275,71 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
else {
|
||||
s.lists["m.recent"].push(room);
|
||||
s.lists["im.vector.fake.recent"].push(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//console.log("calculated new roomLists; m.recent = " + s.lists["m.recent"]);
|
||||
//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;
|
||||
},
|
||||
|
||||
_getScrollNode: function() {
|
||||
var panel = ReactDOM.findDOMNode(this);
|
||||
if (!panel) return null;
|
||||
|
||||
if (panel.classList.contains('gm-prevented')) {
|
||||
return panel;
|
||||
} else {
|
||||
return panel.children[2]; // XXX: Fragile!
|
||||
}
|
||||
},
|
||||
|
||||
_repositionTooltips: function(e) {
|
||||
this._repositionTooltip(e);
|
||||
this._repositionIncomingCallBox(e, false);
|
||||
},
|
||||
|
||||
_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";
|
||||
this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
|
||||
}
|
||||
},
|
||||
|
||||
_repositionIncomingCallBox: function(e, firstTime) {
|
||||
var incomingCallBox = document.getElementById("incomingCallBox");
|
||||
if (incomingCallBox && incomingCallBox.parentElement) {
|
||||
var scroll = this._getScrollNode();
|
||||
var top = (scroll.offsetTop + incomingCallBox.parentElement.offsetTop - scroll.scrollTop);
|
||||
|
||||
if (firstTime) {
|
||||
// scroll to make sure the callbox is on the screen...
|
||||
if (top < 10) { // 10px of vertical margin at top of screen
|
||||
scroll.scrollTop = incomingCallBox.parentElement.offsetTop - 10;
|
||||
}
|
||||
else if (top > scroll.clientHeight - incomingCallBox.offsetHeight + 50) {
|
||||
scroll.scrollTop = incomingCallBox.parentElement.offsetTop - scroll.offsetHeight + incomingCallBox.offsetHeight - 50;
|
||||
}
|
||||
// recalculate top in case we clipped it.
|
||||
top = (scroll.offsetTop + incomingCallBox.parentElement.offsetTop - scroll.scrollTop);
|
||||
}
|
||||
else {
|
||||
// stop the box from scrolling off the screen
|
||||
if (top < 10) {
|
||||
top = 10;
|
||||
}
|
||||
else if (top > scroll.clientHeight - incomingCallBox.offsetHeight + 50) {
|
||||
top = scroll.clientHeight - incomingCallBox.offsetHeight + 50;
|
||||
}
|
||||
}
|
||||
|
||||
incomingCallBox.style.top = top + "px";
|
||||
incomingCallBox.style.left = scroll.offsetLeft + scroll.offsetWidth + "px";
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -230,16 +358,17 @@ module.exports = React.createClass({
|
|||
var self = this;
|
||||
|
||||
return (
|
||||
<GeminiScrollbar className="mx_RoomList_scrollbar" autoshow={true} onScroll={self._repositionTooltip}>
|
||||
<GeminiScrollbar className="mx_RoomList_scrollbar" autoshow={true} onScroll={ self._repositionTooltips }>
|
||||
<div className="mx_RoomList">
|
||||
{ expandButton }
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.invite'] }
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.invite'] }
|
||||
label="Invites"
|
||||
editable={ false }
|
||||
order="recent"
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.favourite'] }
|
||||
|
@ -250,19 +379,21 @@ module.exports = React.createClass({
|
|||
order="manual"
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.recent'] }
|
||||
label="Conversations"
|
||||
<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 }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
{ Object.keys(self.state.lists).map(function(tagName) {
|
||||
if (!tagName.match(/^m\.(invite|favourite|recent|lowpriority|archived)$/)) {
|
||||
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|archived))$/)) {
|
||||
return <RoomSubList list={ self.state.lists[tagName] }
|
||||
key={ tagName }
|
||||
label={ tagName }
|
||||
|
@ -272,6 +403,7 @@ module.exports = React.createClass({
|
|||
order="manual"
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
}
|
||||
|
@ -283,21 +415,25 @@ module.exports = React.createClass({
|
|||
verb="demote"
|
||||
editable={ true }
|
||||
order="recent"
|
||||
bottommost={ self.state.lists['m.archived'].length === 0 }
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.archived'] }
|
||||
<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 } />
|
||||
collapsed={ self.props.collapsed }
|
||||
alwaysShowHeader={ true }
|
||||
startAsHidden={ true }
|
||||
showSpinner={ self.state.isLoadingLeftRooms }
|
||||
onHeaderClick= { self.onArchivedHeaderClick }
|
||||
incomingCall={ self.state.incomingCall } />
|
||||
</div>
|
||||
</GeminiScrollbar>
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -38,6 +38,7 @@ module.exports = React.createClass({
|
|||
highlight: React.PropTypes.bool.isRequired,
|
||||
isInvite: React.PropTypes.bool.isRequired,
|
||||
roomSubList: React.PropTypes.object.isRequired,
|
||||
incomingCall: React.PropTypes.object,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -70,14 +71,9 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
||||
});
|
||||
|
||||
var name;
|
||||
if (this.props.isInvite) {
|
||||
name = this.props.room.getMember(myUserId).events.member.getSender();
|
||||
}
|
||||
else {
|
||||
// XXX: We should never display raw room IDs, but sometimes the room name js sdk gives is undefined
|
||||
name = this.props.room.name || this.props.room.roomId;
|
||||
}
|
||||
// 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;
|
||||
|
@ -110,6 +106,12 @@ module.exports = React.createClass({
|
|||
label = <RoomTooltip room={this.props.room}/>;
|
||||
}
|
||||
|
||||
var incomingCallBox;
|
||||
if (this.props.incomingCall) {
|
||||
var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||
incomingCallBox = <IncomingCallBox incomingCall={ this.props.incomingCall }/>;
|
||||
}
|
||||
|
||||
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
|
||||
// These props are injected by React DnD,
|
||||
|
@ -125,6 +127,7 @@ module.exports = React.createClass({
|
|||
{ badge }
|
||||
</div>
|
||||
{ label }
|
||||
{ incomingCallBox }
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
|
46
src/components/views/rooms/TabCompleteBar.js
Normal file
46
src/components/views/rooms/TabCompleteBar.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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: 'TabCompleteBar',
|
||||
|
||||
propTypes: {
|
||||
entries: React.PropTypes.array.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_TabCompleteBar">
|
||||
{this.props.entries.map(function(entry, i) {
|
||||
return (
|
||||
<div key={entry.getKey() || i + ""} className="mx_TabCompleteBar_item"
|
||||
onClick={entry.onClick.bind(entry)} >
|
||||
{entry.getImageJsx()}
|
||||
<span className="mx_TabCompleteBar_text">
|
||||
{entry.getText()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -23,6 +23,9 @@ module.exports = React.createClass({
|
|||
propTypes: {
|
||||
initialAvatarUrl: React.PropTypes.string,
|
||||
room: React.PropTypes.object,
|
||||
// if false, you need to call changeAvatar.onFileSelected yourself.
|
||||
showUploadSection: React.PropTypes.bool,
|
||||
className: React.PropTypes.string
|
||||
},
|
||||
|
||||
Phases: {
|
||||
|
@ -31,6 +34,13 @@ module.exports = React.createClass({
|
|||
Error: "error",
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
showUploadSection: true,
|
||||
className: "mx_Dialog_content" // FIXME - shouldn't be this by default
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
avatarUrl: this.props.initialAvatarUrl,
|
||||
|
@ -55,7 +65,7 @@ module.exports = React.createClass({
|
|||
phase: this.Phases.Uploading
|
||||
});
|
||||
var self = this;
|
||||
MatrixClientPeg.get().uploadContent(file).then(function(url) {
|
||||
var httpPromise = MatrixClientPeg.get().uploadContent(file).then(function(url) {
|
||||
newUrl = url;
|
||||
if (self.props.room) {
|
||||
return MatrixClientPeg.get().sendStateEvent(
|
||||
|
@ -67,7 +77,9 @@ module.exports = React.createClass({
|
|||
} else {
|
||||
return MatrixClientPeg.get().setAvatarUrl(url);
|
||||
}
|
||||
}).done(function() {
|
||||
});
|
||||
|
||||
httpPromise.done(function() {
|
||||
self.setState({
|
||||
phase: self.Phases.Display,
|
||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl)
|
||||
|
@ -78,11 +90,13 @@ module.exports = React.createClass({
|
|||
});
|
||||
self.onError(error);
|
||||
});
|
||||
|
||||
return httpPromise;
|
||||
},
|
||||
|
||||
onFileSelected: function(ev) {
|
||||
this.avatarSet = true;
|
||||
this.setAvatarFromFile(ev.target.files[0]);
|
||||
return this.setAvatarFromFile(ev.target.files[0]);
|
||||
},
|
||||
|
||||
onError: function(error) {
|
||||
|
@ -106,19 +120,26 @@ module.exports = React.createClass({
|
|||
avatarImg = <img src={this.state.avatarUrl} style={style} />;
|
||||
}
|
||||
|
||||
var uploadSection;
|
||||
if (this.props.showUploadSection) {
|
||||
uploadSection = (
|
||||
<div className={this.props.className}>
|
||||
Upload new:
|
||||
<input type="file" onChange={this.onFileSelected}/>
|
||||
{this.state.errorText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (this.state.phase) {
|
||||
case this.Phases.Display:
|
||||
case this.Phases.Error:
|
||||
return (
|
||||
<div>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className={this.props.className}>
|
||||
{avatarImg}
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
Upload new:
|
||||
<input type="file" onChange={this.onFileSelected}/>
|
||||
{this.state.errorText}
|
||||
</div>
|
||||
{uploadSection}
|
||||
</div>
|
||||
);
|
||||
case this.Phases.Uploading:
|
||||
|
|
|
@ -98,7 +98,9 @@ module.exports = React.createClass({
|
|||
} 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}/>
|
||||
<EditableText ref="displayname_edit" initialValue={this.state.displayName}
|
||||
label="Click to set display name."
|
||||
onValueChanged={this.onValueChanged} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,30 +18,47 @@ limitations under the License.
|
|||
|
||||
var React = require('react');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var sdk = require("../../../index");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ChangePassword',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func,
|
||||
onError: React.PropTypes.func,
|
||||
onCheckPassword: React.PropTypes.func,
|
||||
rowClassName: React.PropTypes.string,
|
||||
rowLabelClassName: React.PropTypes.string,
|
||||
rowInputClassName: React.PropTypes.string,
|
||||
buttonClassName: React.PropTypes.string
|
||||
},
|
||||
|
||||
Phases: {
|
||||
Edit: "edit",
|
||||
Uploading: "uploading",
|
||||
Error: "error",
|
||||
Success: "Success"
|
||||
Error: "error"
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onFinished: function() {},
|
||||
onError: function() {},
|
||||
onCheckPassword: function(oldPass, newPass, confirmPass) {
|
||||
if (newPass !== confirmPass) {
|
||||
return {
|
||||
error: "New passwords don't match."
|
||||
};
|
||||
} else if (!newPass || newPass.length === 0) {
|
||||
return {
|
||||
error: "Passwords can't be empty"
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
phase: this.Phases.Edit,
|
||||
errorString: ''
|
||||
phase: this.Phases.Edit
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -55,60 +72,72 @@ module.exports = React.createClass({
|
|||
};
|
||||
|
||||
this.setState({
|
||||
phase: this.Phases.Uploading,
|
||||
errorString: '',
|
||||
})
|
||||
|
||||
var d = cli.setPassword(authDict, new_password);
|
||||
phase: this.Phases.Uploading
|
||||
});
|
||||
|
||||
var self = this;
|
||||
d.then(function() {
|
||||
self.setState({
|
||||
phase: self.Phases.Success,
|
||||
errorString: '',
|
||||
})
|
||||
cli.setPassword(authDict, new_password).then(function() {
|
||||
self.props.onFinished();
|
||||
}, function(err) {
|
||||
self.props.onError(err);
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
phase: self.Phases.Error,
|
||||
errorString: err.toString()
|
||||
})
|
||||
});
|
||||
phase: self.Phases.Edit
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
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 {
|
||||
var err = this.props.onCheckPassword(
|
||||
old_password, new_password, confirm_password
|
||||
);
|
||||
if (err) {
|
||||
this.props.onError(err);
|
||||
}
|
||||
else {
|
||||
this.changePassword(old_password, new_password);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var rowClassName = this.props.rowClassName;
|
||||
var rowLabelClassName = this.props.rowLabelClassName;
|
||||
var rowInputClassName = this.props.rowInputClassName
|
||||
var buttonClassName = this.props.buttonClassName;
|
||||
|
||||
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 className={this.props.className}>
|
||||
<div className={rowClassName}>
|
||||
<div className={rowLabelClassName}>
|
||||
<label htmlFor="passwordold">Current password</label>
|
||||
</div>
|
||||
<div className={rowInputClassName}>
|
||||
<input id="passwordold" type="password" ref="old_input" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onClickChange}>Change Password</button>
|
||||
<button onClick={this.props.onFinished}>Cancel</button>
|
||||
<div className={rowClassName}>
|
||||
<div className={rowLabelClassName}>
|
||||
<label htmlFor="password1">New password</label>
|
||||
</div>
|
||||
<div className={rowInputClassName}>
|
||||
<input id="password1" type="password" ref="new_input" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={rowClassName}>
|
||||
<div className={rowLabelClassName}>
|
||||
<label htmlFor="password2">Confirm password</label>
|
||||
</div>
|
||||
<div className={rowInputClassName}>
|
||||
<input id="password2" type="password" ref="confirm_input" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={buttonClassName} onClick={this.onClickChange}>
|
||||
Change Password
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -119,17 +148,6 @@ module.exports = React.createClass({
|
|||
<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>
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -35,19 +35,13 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this._trackedRoom = null;
|
||||
if (this.props.room) {
|
||||
this._trackedRoom = this.props.room;
|
||||
this.showCall(this._trackedRoom.roomId);
|
||||
this.showCall(this.props.room.roomId);
|
||||
}
|
||||
else {
|
||||
// XXX: why would we ever not have a this.props.room?
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +75,7 @@ module.exports = React.createClass({
|
|||
// and for the voice stream of screen captures
|
||||
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
||||
}
|
||||
if (call && call.type === "video" && call.state !== 'ended') {
|
||||
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
|
||||
// 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 = (
|
||||
|
|
|
@ -21,87 +21,29 @@ 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
|
||||
room_id: this.props.incomingCall.roomId
|
||||
});
|
||||
},
|
||||
|
||||
onRejectClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
room_id: this.state.incomingCall.roomId
|
||||
room_id: this.props.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;
|
||||
var room = this.props.incomingCall ? MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId) : null;
|
||||
var caller = room ? room.name : "unknown";
|
||||
return (
|
||||
<div className="mx_IncomingCallBox">
|
||||
{audioBlock}
|
||||
<div className="mx_IncomingCallBox" id="incomingCallBox">
|
||||
<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 }
|
||||
Incoming { this.props.incomingCall ? this.props.incomingCall.type : '' } call from { caller }
|
||||
</div>
|
||||
<div className="mx_IncomingCallBox_buttons">
|
||||
<div className="mx_IncomingCallBox_buttons_cell">
|
||||
|
|
Loading…
Reference in a new issue