Merge remote-tracking branch 'origin/notif_sync' into unread_sync
This commit is contained in:
commit
ba51c68844
81 changed files with 1788 additions and 778 deletions
2
header
2
header
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -35,10 +35,10 @@ module.exports = {
|
||||||
return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
|
||||||
}
|
}
|
||||||
else if (now.getFullYear() === date.getFullYear()) {
|
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 {
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
113
src/HtmlUtils.js
113
src/HtmlUtils.js
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -48,8 +48,15 @@ var sanitizeHtmlParams = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
class Highlighter {
|
||||||
_applyHighlights: function(safeSnippet, highlights, html, k) {
|
constructor(html, highlightClass, onHighlightClick) {
|
||||||
|
this.html = html;
|
||||||
|
this.highlightClass = highlightClass;
|
||||||
|
this.onHighlightClick = onHighlightClick;
|
||||||
|
this._key = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyHighlights(safeSnippet, highlights) {
|
||||||
var lastOffset = 0;
|
var lastOffset = 0;
|
||||||
var offset;
|
var offset;
|
||||||
var nodes = [];
|
var nodes = [];
|
||||||
|
@ -61,77 +68,97 @@ module.exports = {
|
||||||
// If and when this happens, we'll probably have to split his method in two between
|
// If and when this happens, we'll probably have to split his method in two between
|
||||||
// HTML and plain-text highlighting.
|
// HTML and plain-text highlighting.
|
||||||
|
|
||||||
var safeHighlight = html ? sanitizeHtml(highlights[0], sanitizeHtmlParams) : highlights[0];
|
var safeHighlight = this.html ? sanitizeHtml(highlights[0], sanitizeHtmlParams) : highlights[0];
|
||||||
while ((offset = safeSnippet.indexOf(safeHighlight, lastOffset)) >= 0) {
|
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
|
||||||
// handle preamble
|
// handle preamble
|
||||||
if (offset > lastOffset) {
|
if (offset > lastOffset) {
|
||||||
nodes = nodes.concat(this._applySubHighlightsInRange(safeSnippet, lastOffset, offset, highlights, html, k));
|
var subSnippet = safeSnippet.substring(lastOffset, offset);
|
||||||
k += nodes.length;
|
nodes = nodes.concat(this._applySubHighlights(subSnippet, highlights));
|
||||||
}
|
}
|
||||||
|
|
||||||
// do highlight
|
// do highlight
|
||||||
if (html) {
|
nodes.push(this._createSpan(safeHighlight, true));
|
||||||
nodes.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeHighlight }} className="mx_MessageTile_searchHighlight" />);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
nodes.push(<span key={ k++ } className="mx_MessageTile_searchHighlight">{ safeHighlight }</span>);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastOffset = offset + safeHighlight.length;
|
lastOffset = offset + safeHighlight.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle postamble
|
// handle postamble
|
||||||
if (lastOffset != safeSnippet.length) {
|
if (lastOffset != safeSnippet.length) {
|
||||||
nodes = nodes.concat(this._applySubHighlightsInRange(safeSnippet, lastOffset, undefined, highlights, html, k));
|
var subSnippet = safeSnippet.substring(lastOffset, undefined);
|
||||||
k += nodes.length;
|
nodes = nodes.concat(this._applySubHighlights(subSnippet, highlights));
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
},
|
}
|
||||||
|
|
||||||
_applySubHighlightsInRange: function(safeSnippet, lastOffset, offset, highlights, html, k) {
|
_applySubHighlights(safeSnippet, highlights) {
|
||||||
var nodes = [];
|
|
||||||
if (highlights[1]) {
|
if (highlights[1]) {
|
||||||
// recurse into this range to check for the next set of highlight matches
|
// recurse into this range to check for the next set of highlight matches
|
||||||
var subnodes = this._applyHighlights( safeSnippet.substring(lastOffset, offset), highlights.slice(1), html, k );
|
return this.applyHighlights(safeSnippet, highlights.slice(1));
|
||||||
nodes = nodes.concat(subnodes);
|
|
||||||
k += subnodes.length;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// no more highlights to be found, just return the unhighlighted string
|
// no more highlights to be found, just return the unhighlighted string
|
||||||
if (html) {
|
return [this._createSpan(safeSnippet, false)];
|
||||||
nodes.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeSnippet.substring(lastOffset, offset) }} />);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 (highlight) {
|
||||||
|
spanProps.onClick = this.onHighlightClick;
|
||||||
|
spanProps.className = this.highlightClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.html) {
|
||||||
|
return (<span {...spanProps} dangerouslySetInnerHTML={{ __html: spanBody }} />);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
nodes.push(<span key={ k++ }>{ safeSnippet.substring(lastOffset, offset) }</span>);
|
return (<span {...spanProps}>{ spanBody }</span>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nodes;
|
}
|
||||||
},
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
bodyToHtml: function(content, highlights) {
|
|
||||||
var originalBody = content.body;
|
|
||||||
var body;
|
var body;
|
||||||
var k = 0;
|
|
||||||
|
|
||||||
if (highlights && highlights.length > 0) {
|
if (highlights && highlights.length > 0) {
|
||||||
var bodyList = [];
|
var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
|
||||||
|
body = highlighter.applyHighlights(safeBody, highlights);
|
||||||
if (content.format === "org.matrix.custom.html") {
|
|
||||||
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
|
||||||
bodyList = this._applyHighlights(safeBody, highlights, true, k);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
bodyList = this._applyHighlights(originalBody, highlights, true, k);
|
if (isHtml) {
|
||||||
}
|
|
||||||
body = bodyList;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (content.format === "org.matrix.custom.html") {
|
|
||||||
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
|
||||||
body = <span className="markdown-body" dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
body = <span className="markdown-body" dangerouslySetInnerHTML={{ __html: safeBody }} />;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
body = originalBody;
|
body = safeBody;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -99,19 +99,24 @@ var commands = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find a room with this alias
|
// 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(
|
var foundRoom = MatrixTools.getRoomForAlias(
|
||||||
MatrixClientPeg.get().getRooms(),
|
MatrixClientPeg.get().getRooms(),
|
||||||
room_alias
|
room_alias
|
||||||
);
|
);
|
||||||
if (foundRoom) { // we've already joined this room, view it.
|
|
||||||
|
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({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: foundRoom.roomId
|
room_id: foundRoom.roomId
|
||||||
});
|
});
|
||||||
return success();
|
return success();
|
||||||
}
|
}
|
||||||
else {
|
}
|
||||||
// attempt to join this alias.
|
|
||||||
|
// otherwise attempt to join this alias.
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().joinRoom(room_alias).then(
|
MatrixClientPeg.get().joinRoom(room_alias).then(
|
||||||
function(room) {
|
function(room) {
|
||||||
|
@ -123,7 +128,6 @@ var commands = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return reject("Usage: /join <room_alias>");
|
return reject("Usage: /join <room_alias>");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
307
src/TabComplete.js
Normal file
307
src/TabComplete.js
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 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, 2016 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;
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
67
src/UserSettingsStore.js
Normal file
67
src/UserSettingsStore.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 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);
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -28,6 +28,7 @@ module.exports.components['structures.login.PostRegistration'] = require('./comp
|
||||||
module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
|
module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
|
||||||
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
|
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
|
||||||
module.exports.components['structures.RoomView'] = require('./components/structures/RoomView');
|
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.UploadBar'] = require('./components/structures/UploadBar');
|
||||||
module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings');
|
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.MemberAvatar'] = require('./components/views/avatars/MemberAvatar');
|
||||||
|
@ -65,6 +66,8 @@ 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.RoomList'] = require('./components/views/rooms/RoomList');
|
||||||
module.exports.components['views.rooms.RoomSettings'] = require('./components/views/rooms/RoomSettings');
|
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.RoomTile'] = require('./components/views/rooms/RoomTile');
|
||||||
|
module.exports.components['views.rooms.SearchResultTile'] = require('./components/views/rooms/SearchResultTile');
|
||||||
|
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.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar');
|
||||||
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
|
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
|
||||||
module.exports.components['views.settings.ChangePassword'] = require('./components/views/settings/ChangePassword');
|
module.exports.components['views.settings.ChangePassword'] = require('./components/views/settings/ChangePassword');
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -251,13 +251,15 @@ module.exports = React.createClass({
|
||||||
var UserSelector = sdk.getComponent("elements.UserSelector");
|
var UserSelector = sdk.getComponent("elements.UserSelector");
|
||||||
var RoomHeader = sdk.getComponent("rooms.RoomHeader");
|
var RoomHeader = sdk.getComponent("rooms.RoomHeader");
|
||||||
|
|
||||||
|
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CreateRoom">
|
<div className="mx_CreateRoom">
|
||||||
<RoomHeader simpleHeader="Create room" />
|
<RoomHeader simpleHeader="Create room" />
|
||||||
<div className="mx_CreateRoom_body">
|
<div className="mx_CreateRoom_body">
|
||||||
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder="Name"/> <br />
|
<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 />
|
<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 />
|
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged}/> <br />
|
||||||
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
|
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset}/> <br />
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -639,12 +639,30 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var rooms = MatrixClientPeg.get().getRooms();
|
var rooms = MatrixClientPeg.get().getRooms();
|
||||||
for (var i = 0; i < rooms.length; ++i) {
|
for (var i = 0; i < rooms.length; ++i) {
|
||||||
|
if (rooms[i].unread_notification_count) {
|
||||||
notifCount += rooms[i].unread_notification_count;
|
notifCount += rooms[i].unread_notification_count;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.favicon.badge(notifCount);
|
this.favicon.badge(notifCount);
|
||||||
document.title = (notifCount > 0 ? "["+notifCount+"] " : "")+"Vector";
|
document.title = (notifCount > 0 ? "["+notifCount+"] " : "")+"Vector";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
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() {
|
render: function() {
|
||||||
var LeftPanel = sdk.getComponent('structures.LeftPanel');
|
var LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||||
var RoomView = sdk.getComponent('structures.RoomView');
|
var RoomView = sdk.getComponent('structures.RoomView');
|
||||||
|
@ -677,7 +695,7 @@ module.exports = React.createClass({
|
||||||
right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} />
|
right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} />
|
||||||
break;
|
break;
|
||||||
case this.PageTypes.UserSettings:
|
case this.PageTypes.UserSettings:
|
||||||
page_element = <UserSettings />
|
page_element = <UserSettings onClose={this.onUserSettingsClose} />
|
||||||
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
|
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
|
||||||
break;
|
break;
|
||||||
case this.PageTypes.CreateRoom:
|
case this.PageTypes.CreateRoom:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -23,7 +23,6 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
|
||||||
var q = require("q");
|
var q = require("q");
|
||||||
var classNames = require("classnames");
|
var classNames = require("classnames");
|
||||||
var Matrix = require("matrix-js-sdk");
|
var Matrix = require("matrix-js-sdk");
|
||||||
|
@ -34,6 +33,8 @@ var WhoIsTyping = require("../../WhoIsTyping");
|
||||||
var Modal = require("../../Modal");
|
var Modal = require("../../Modal");
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
var CallHandler = require('../../CallHandler');
|
var CallHandler = require('../../CallHandler');
|
||||||
|
var TabComplete = require("../../TabComplete");
|
||||||
|
var MemberEntry = require("../../TabCompleteEntries").MemberEntry;
|
||||||
var Resend = require("../../Resend");
|
var Resend = require("../../Resend");
|
||||||
var dis = require("../../dispatcher");
|
var dis = require("../../dispatcher");
|
||||||
|
|
||||||
|
@ -42,6 +43,13 @@ var INITIAL_SIZE = 20;
|
||||||
|
|
||||||
var DEBUG_SCROLL = false;
|
var DEBUG_SCROLL = false;
|
||||||
|
|
||||||
|
if (DEBUG_SCROLL) {
|
||||||
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
|
var debuglog = console.log.bind(console);
|
||||||
|
} else {
|
||||||
|
var debuglog = function () {};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomView',
|
displayName: 'RoomView',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -49,13 +57,6 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
/* properties in RoomView objects include:
|
/* properties in RoomView objects include:
|
||||||
*
|
|
||||||
* savedScrollState: the current scroll position in the backlog. Response
|
|
||||||
* from _calculateScrollState. Updated on scroll events.
|
|
||||||
*
|
|
||||||
* savedSearchScrollState: similar to savedScrollState, but specific to the
|
|
||||||
* search results (we need to preserve savedScrollState when search
|
|
||||||
* results are visible)
|
|
||||||
*
|
*
|
||||||
* eventNodes: a map from event id to DOM node representing that event
|
* eventNodes: a map from event id to DOM node representing that event
|
||||||
*/
|
*/
|
||||||
|
@ -84,7 +85,18 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||||
this.savedScrollState = {atBottom: true};
|
// xchat-style tab complete, add a colon if tab
|
||||||
|
// completing at the start of the text
|
||||||
|
this.tabComplete = new TabComplete({
|
||||||
|
startingWordSuffix: ": ",
|
||||||
|
wordSuffix: " ",
|
||||||
|
allowLooping: false,
|
||||||
|
autoEnterTabComplete: true,
|
||||||
|
onClickCompletes: true,
|
||||||
|
onStateChange: (isCompleting) => {
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -168,23 +180,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// get the DOM node which has the scrollTop property we care about for our
|
|
||||||
// message panel.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
_getScrollNode: function() {
|
|
||||||
var panel = ReactDOM.findDOMNode(this.refs.messagePanel);
|
|
||||||
if (!panel) return null;
|
|
||||||
|
|
||||||
if (panel.classList.contains('gm-prevented')) {
|
|
||||||
return panel;
|
|
||||||
} else {
|
|
||||||
return panel.children[2]; // XXX: Fragile!
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onSyncStateChange: function(state, prevState) {
|
onSyncStateChange: function(state, prevState) {
|
||||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||||
return;
|
return;
|
||||||
|
@ -218,7 +213,7 @@ module.exports = React.createClass({
|
||||||
if (!toStartOfTimeline &&
|
if (!toStartOfTimeline &&
|
||||||
(ev.getSender() !== MatrixClientPeg.get().credentials.userId)) {
|
(ev.getSender() !== MatrixClientPeg.get().credentials.userId)) {
|
||||||
// update unread count when scrolled up
|
// update unread count when scrolled up
|
||||||
if (!this.state.searchResults && this.savedScrollState.atBottom) {
|
if (!this.state.searchResults && this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) {
|
||||||
currentUnread = 0;
|
currentUnread = 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -251,6 +246,11 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomStateMember: function(ev, state, member) {
|
onRoomStateMember: function(ev, state, member) {
|
||||||
|
if (member.roomId === this.props.roomId) {
|
||||||
|
// a member state changed in this room, refresh the tab complete list
|
||||||
|
this._updateTabCompleteList(this.state.room);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.props.ConferenceHandler) {
|
if (!this.props.ConferenceHandler) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -313,6 +313,17 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
window.addEventListener('resize', this.onResize);
|
window.addEventListener('resize', this.onResize);
|
||||||
this.onResize();
|
this.onResize();
|
||||||
|
|
||||||
|
this._updateTabCompleteList(this.state.room);
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateTabCompleteList: function(room) {
|
||||||
|
if (!room || !this.tabComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.tabComplete.setCompletionList(
|
||||||
|
MemberEntry.fromMemberList(room.getJoinedMembers())
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_initialiseMessagePanel: function() {
|
_initialiseMessagePanel: function() {
|
||||||
|
@ -326,7 +337,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
this.sendReadReceipt();
|
this.sendReadReceipt();
|
||||||
this.fillSpace();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
|
@ -338,64 +348,52 @@ module.exports = React.createClass({
|
||||||
if (!this.refs.messagePanel.initialised) {
|
if (!this.refs.messagePanel.initialised) {
|
||||||
this._initialiseMessagePanel();
|
this._initialiseMessagePanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_paginateCompleted: function() {
|
_paginateCompleted: function() {
|
||||||
if (DEBUG_SCROLL) console.log("paginate complete");
|
debuglog("paginate complete");
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
room: MatrixClientPeg.get().getRoom(this.props.roomId)
|
room: MatrixClientPeg.get().getRoom(this.props.roomId)
|
||||||
});
|
});
|
||||||
|
|
||||||
// we might not have got enough results from the pagination
|
|
||||||
// request, so give fillSpace() a chance to set off another.
|
|
||||||
this.setState({paginating: false});
|
this.setState({paginating: false});
|
||||||
|
},
|
||||||
|
|
||||||
if (!this.state.searchResults) {
|
onSearchResultsFillRequest: function(backwards) {
|
||||||
this.fillSpace();
|
if (!backwards)
|
||||||
|
return q(false);
|
||||||
|
|
||||||
|
if (this.state.searchResults.next_batch) {
|
||||||
|
debuglog("requesting more search results");
|
||||||
|
var searchPromise = MatrixClientPeg.get().backPaginateRoomEventsSearch(
|
||||||
|
this.state.searchResults);
|
||||||
|
return this._handleSearchResult(searchPromise);
|
||||||
|
} else {
|
||||||
|
debuglog("no more search results");
|
||||||
|
return q(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// check the scroll position, and if we need to, set off a pagination
|
// set off a pagination request.
|
||||||
// request.
|
onMessageListFillRequest: function(backwards) {
|
||||||
fillSpace: function() {
|
if (!backwards)
|
||||||
if (!this.refs.messagePanel) return;
|
return q(false);
|
||||||
var messageWrapperScroll = this._getScrollNode();
|
|
||||||
if (messageWrapperScroll.scrollTop > messageWrapperScroll.clientHeight) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// there's less than a screenful of messages left - try to get some
|
|
||||||
// more messages.
|
|
||||||
|
|
||||||
if (this.state.searchResults) {
|
|
||||||
if (this.nextSearchBatch) {
|
|
||||||
if (DEBUG_SCROLL) console.log("requesting more search results");
|
|
||||||
this._getSearchBatch(this.state.searchTerm,
|
|
||||||
this.state.searchScope);
|
|
||||||
} else {
|
|
||||||
if (DEBUG_SCROLL) console.log("no more search results");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Either wind back the message cap (if there are enough events in the
|
// Either wind back the message cap (if there are enough events in the
|
||||||
// timeline to do so), or fire off a pagination request.
|
// timeline to do so), or fire off a pagination request.
|
||||||
|
|
||||||
if (this.state.messageCap < this.state.room.timeline.length) {
|
if (this.state.messageCap < this.state.room.timeline.length) {
|
||||||
var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length);
|
var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length);
|
||||||
if (DEBUG_SCROLL) console.log("winding back message cap to", cap);
|
debuglog("winding back message cap to", cap);
|
||||||
this.setState({messageCap: cap});
|
this.setState({messageCap: cap});
|
||||||
|
return q(true);
|
||||||
} else if(this.state.room.oldState.paginationToken) {
|
} else if(this.state.room.oldState.paginationToken) {
|
||||||
var cap = this.state.messageCap + PAGINATE_SIZE;
|
var cap = this.state.messageCap + PAGINATE_SIZE;
|
||||||
if (DEBUG_SCROLL) console.log("starting paginate to cap", cap);
|
debuglog("starting paginate to cap", cap);
|
||||||
this.setState({messageCap: cap, paginating: true});
|
this.setState({messageCap: cap, paginating: true});
|
||||||
MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(this._paginateCompleted).done();
|
return MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).
|
||||||
|
finally(this._paginateCompleted).then(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -431,45 +429,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessageListScroll: function(ev) {
|
onMessageListScroll: function(ev) {
|
||||||
var sn = this._getScrollNode();
|
if (this.state.numUnreadMessages != 0 &&
|
||||||
if (DEBUG_SCROLL) console.log("Scroll event: offset now:", sn.scrollTop, "recentEventScroll:", this.recentEventScroll);
|
this.refs.messagePanel.isAtBottom()) {
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.refs.messagePanel) {
|
|
||||||
if (this.state.searchResults) {
|
|
||||||
this.savedSearchScrollState = this._calculateScrollState();
|
|
||||||
if (DEBUG_SCROLL) console.log("Saved search scroll state", this.savedSearchScrollState);
|
|
||||||
} else {
|
|
||||||
this.savedScrollState = this._calculateScrollState();
|
|
||||||
if (DEBUG_SCROLL) console.log("Saved scroll state", this.savedScrollState);
|
|
||||||
if (this.savedScrollState.atBottom && this.state.numUnreadMessages != 0) {
|
|
||||||
this.setState({numUnreadMessages: 0});
|
this.setState({numUnreadMessages: 0});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!this.state.paginating && !this.state.searchInProgress) {
|
|
||||||
this.fillSpace();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onDragOver: function(ev) {
|
onDragOver: function(ev) {
|
||||||
|
@ -524,71 +487,78 @@ module.exports = React.createClass({
|
||||||
this.setState({
|
this.setState({
|
||||||
searchTerm: term,
|
searchTerm: term,
|
||||||
searchScope: scope,
|
searchScope: scope,
|
||||||
searchResults: [],
|
searchResults: {},
|
||||||
searchHighlights: [],
|
searchHighlights: [],
|
||||||
searchCount: null,
|
|
||||||
searchCanPaginate: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.savedSearchScrollState = {atBottom: true};
|
// if we already have a search panel, we need to tell it to forget
|
||||||
this.nextSearchBatch = null;
|
// about its scroll state.
|
||||||
this._getSearchBatch(term, scope);
|
if (this.refs.searchResultsPanel) {
|
||||||
|
this.refs.searchResultsPanel.resetScrollState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that we don't end up showing results from
|
||||||
|
// an aborted search by keeping a unique id.
|
||||||
|
//
|
||||||
|
// todo: should cancel any previous search requests.
|
||||||
|
this.searchId = new Date().getTime();
|
||||||
|
|
||||||
|
var filter;
|
||||||
|
if (scope === "Room") {
|
||||||
|
filter = {
|
||||||
|
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :(
|
||||||
|
rooms: [
|
||||||
|
this.props.roomId
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
debuglog("sending search request");
|
||||||
|
|
||||||
|
var searchPromise = MatrixClientPeg.get().searchRoomEvents({
|
||||||
|
filter: filter,
|
||||||
|
term: term,
|
||||||
|
});
|
||||||
|
this._handleSearchResult(searchPromise).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
// fire off a request for a batch of search results
|
_handleSearchResult: function(searchPromise) {
|
||||||
_getSearchBatch: function(term, scope) {
|
var self = this;
|
||||||
|
|
||||||
|
// keep a record of the current search id, so that if the search terms
|
||||||
|
// change before we get a response, we can ignore the results.
|
||||||
|
var localSearchId = this.searchId;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
searchInProgress: true,
|
searchInProgress: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// make sure that we don't end up merging results from
|
return searchPromise.then(function(results) {
|
||||||
// different searches by keeping a unique id.
|
debuglog("search complete");
|
||||||
//
|
if (!self.state.searching || self.searchId != localSearchId) {
|
||||||
// todo: should cancel any previous search requests.
|
|
||||||
var searchId = this.searchId = new Date().getTime();
|
|
||||||
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (DEBUG_SCROLL) console.log("sending search request");
|
|
||||||
MatrixClientPeg.get().search({ body: this._getSearchCondition(term, scope),
|
|
||||||
next_batch: this.nextSearchBatch })
|
|
||||||
.then(function(data) {
|
|
||||||
if (DEBUG_SCROLL) console.log("search complete");
|
|
||||||
if (!self.state.searching || self.searchId != searchId) {
|
|
||||||
console.error("Discarding stale search results");
|
console.error("Discarding stale search results");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var results = data.search_categories.room_events;
|
// postgres on synapse returns us precise details of the strings
|
||||||
|
// which actually got matched for highlighting.
|
||||||
|
//
|
||||||
|
// In either case, we want to highlight the literal search term
|
||||||
|
// whether it was used by the search engine or not.
|
||||||
|
|
||||||
// postgres on synapse returns us precise details of the
|
var highlights = results.highlights;
|
||||||
// strings which actually got matched for highlighting.
|
if (highlights.indexOf(self.state.searchTerm) < 0) {
|
||||||
|
highlights = highlights.concat(self.state.searchTerm);
|
||||||
// combine the highlight list with our existing list; build an object
|
|
||||||
// to avoid O(N^2) fail
|
|
||||||
var highlights = {};
|
|
||||||
results.highlights.forEach(function(hl) { highlights[hl] = 1; });
|
|
||||||
self.state.searchHighlights.forEach(function(hl) { highlights[hl] = 1; });
|
|
||||||
|
|
||||||
// turn it back into an ordered list. For overlapping highlights,
|
|
||||||
// favour longer (more specific) terms first
|
|
||||||
highlights = Object.keys(highlights).sort(function(a, b) { b.length - a.length });
|
|
||||||
|
|
||||||
// sqlite doesn't give us any highlights, so just try to highlight the literal search term
|
|
||||||
if (highlights.length == 0) {
|
|
||||||
highlights = [ term ];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// append the new results to our existing results
|
// For overlapping highlights,
|
||||||
var events = self.state.searchResults.concat(results.results);
|
// favour longer (more specific) terms first
|
||||||
|
highlights = highlights.sort(function(a, b) { b.length - a.length });
|
||||||
|
|
||||||
self.setState({
|
self.setState({
|
||||||
searchHighlights: highlights,
|
searchHighlights: highlights,
|
||||||
searchResults: events,
|
searchResults: results,
|
||||||
searchCount: results.count,
|
|
||||||
searchCanPaginate: !!(results.next_batch),
|
|
||||||
});
|
});
|
||||||
self.nextSearchBatch = results.next_batch;
|
|
||||||
}, function(error) {
|
}, function(error) {
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
@ -599,56 +569,27 @@ module.exports = React.createClass({
|
||||||
self.setState({
|
self.setState({
|
||||||
searchInProgress: false
|
searchInProgress: false
|
||||||
});
|
});
|
||||||
}).done();
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_getSearchCondition: function(term, scope) {
|
|
||||||
var filter;
|
|
||||||
|
|
||||||
if (scope === "Room") {
|
getSearchResultTiles: function() {
|
||||||
filter = {
|
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :(
|
var SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
|
||||||
rooms: [
|
|
||||||
this.props.roomId
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
search_categories: {
|
|
||||||
room_events: {
|
|
||||||
search_term: term,
|
|
||||||
filter: filter,
|
|
||||||
order_by: "recent",
|
|
||||||
event_context: {
|
|
||||||
before_limit: 1,
|
|
||||||
after_limit: 1,
|
|
||||||
include_profile: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getEventTiles: function() {
|
|
||||||
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
var ret = [];
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (this.state.searchResults)
|
|
||||||
{
|
|
||||||
// XXX: todo: merge overlapping results somehow?
|
// XXX: todo: merge overlapping results somehow?
|
||||||
// XXX: why doesn't searching on name work?
|
// XXX: why doesn't searching on name work?
|
||||||
|
|
||||||
var lastRoomId;
|
if (this.state.searchResults.results === undefined) {
|
||||||
|
// awaiting results
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.searchCanPaginate === false) {
|
var ret = [];
|
||||||
if (this.state.searchResults.length == 0) {
|
|
||||||
|
if (!this.state.searchResults.next_batch) {
|
||||||
|
if (this.state.searchResults.results.length == 0) {
|
||||||
ret.push(<li key="search-top-marker">
|
ret.push(<li key="search-top-marker">
|
||||||
<h2 className="mx_RoomView_topMarker">No results</h2>
|
<h2 className="mx_RoomView_topMarker">No results</h2>
|
||||||
</li>
|
</li>
|
||||||
|
@ -661,9 +602,12 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = this.state.searchResults.length - 1; i >= 0; i--) {
|
var lastRoomId;
|
||||||
var result = this.state.searchResults[i];
|
|
||||||
var mxEv = new Matrix.MatrixEvent(result.result);
|
for (var i = this.state.searchResults.results.length - 1; i >= 0; i--) {
|
||||||
|
var result = this.state.searchResults.results[i];
|
||||||
|
|
||||||
|
var mxEv = result.context.getEvent();
|
||||||
|
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
// XXX: can this ever happen? It will make the result count
|
// XXX: can this ever happen? It will make the result count
|
||||||
|
@ -671,37 +615,38 @@ module.exports = React.createClass({
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var eventId = mxEv.getId();
|
if (this.state.searchScope === 'All') {
|
||||||
|
var roomId = mxEv.getRoomId();
|
||||||
if (self.state.searchScope === 'All') {
|
|
||||||
var roomId = result.result.room_id;
|
|
||||||
if(roomId != lastRoomId) {
|
if(roomId != lastRoomId) {
|
||||||
ret.push(<li key={eventId + "-room"}><h1>Room: { cli.getRoom(roomId).name }</h1></li>);
|
var room = cli.getRoom(roomId);
|
||||||
|
|
||||||
|
// XXX: if we've left the room, we might not know about
|
||||||
|
// it. We should tell the js sdk to go and find out about
|
||||||
|
// it. But that's not an issue currently, as synapse only
|
||||||
|
// returns results for rooms we're joined to.
|
||||||
|
var roomName = room ? room.name : "Unknown room "+roomId;
|
||||||
|
|
||||||
|
ret.push(<li key={mxEv.getId() + "-room"}>
|
||||||
|
<h1>Room: { roomName }</h1>
|
||||||
|
</li>);
|
||||||
lastRoomId = roomId;
|
lastRoomId = roomId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ts1 = result.result.origin_server_ts;
|
ret.push(<SearchResultTile key={mxEv.getId()}
|
||||||
ret.push(<li key={ts1 + "-search"}><DateSeparator ts={ts1}/></li>); // Rank: {resultList[i].rank}
|
searchResult={result}
|
||||||
|
searchHighlights={this.state.searchHighlights}/>);
|
||||||
if (result.context.events_before[0]) {
|
|
||||||
var mxEv2 = new Matrix.MatrixEvent(result.context.events_before[0]);
|
|
||||||
if (EventTile.haveTileForEvent(mxEv2)) {
|
|
||||||
ret.push(<li key={eventId+"-1"} data-scroll-token={eventId+"-1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.push(<li key={eventId+"+0"} data-scroll-token={eventId+"+0"}><EventTile mxEvent={mxEv} highlights={self.state.searchHighlights}/></li>);
|
|
||||||
|
|
||||||
if (result.context.events_after[0]) {
|
|
||||||
var mxEv2 = new Matrix.MatrixEvent(result.context.events_after[0]);
|
|
||||||
if (EventTile.haveTileForEvent(mxEv2)) {
|
|
||||||
ret.push(<li key={eventId+"+1"} data-scroll-token={eventId+"+1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
getEventTiles: function() {
|
||||||
|
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
|
|
||||||
|
var ret = [];
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
|
|
||||||
|
|
||||||
var prevEvent = null; // the last event we showed
|
var prevEvent = null; // the last event we showed
|
||||||
|
@ -996,10 +941,9 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
scrollToBottom: function() {
|
scrollToBottom: function() {
|
||||||
var scrollNode = this._getScrollNode();
|
var messagePanel = this.refs.messagePanel;
|
||||||
if (!scrollNode) return;
|
if (!messagePanel) return;
|
||||||
scrollNode.scrollTop = scrollNode.scrollHeight;
|
messagePanel.scrollToBottom();
|
||||||
if (DEBUG_SCROLL) console.log("Scrolled to bottom; offset now", scrollNode.scrollTop);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// scroll the event view to put the given event at the bottom.
|
// scroll the event view to put the given event at the bottom.
|
||||||
|
@ -1007,6 +951,9 @@ module.exports = React.createClass({
|
||||||
// pixel_offset gives the number of pixels between the bottom of the event
|
// pixel_offset gives the number of pixels between the bottom of the event
|
||||||
// and the bottom of the container.
|
// and the bottom of the container.
|
||||||
scrollToEvent: function(eventId, pixelOffset) {
|
scrollToEvent: function(eventId, pixelOffset) {
|
||||||
|
var messagePanel = this.refs.messagePanel;
|
||||||
|
if (!messagePanel) return;
|
||||||
|
|
||||||
var idx = this._indexForEventId(eventId);
|
var idx = this._indexForEventId(eventId);
|
||||||
if (idx === null) {
|
if (idx === null) {
|
||||||
// we don't seem to have this event in our timeline. Presumably
|
// we don't seem to have this event in our timeline. Presumably
|
||||||
|
@ -1016,7 +963,7 @@ module.exports = React.createClass({
|
||||||
//
|
//
|
||||||
// for now, just scroll to the top of the buffer.
|
// for now, just scroll to the top of the buffer.
|
||||||
console.log("Refusing to scroll to unknown event "+eventId);
|
console.log("Refusing to scroll to unknown event "+eventId);
|
||||||
this._getScrollNode().scrollTop = 0;
|
messagePanel.scrollToTop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1036,117 +983,30 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// the scrollTokens on our DOM nodes are the event IDs, so we can pass
|
// the scrollTokens on our DOM nodes are the event IDs, so we can pass
|
||||||
// eventId directly into _scrollToToken.
|
// eventId directly into _scrollToToken.
|
||||||
this._scrollToToken(eventId, pixelOffset);
|
messagePanel.scrollToToken(eventId, pixelOffset);
|
||||||
},
|
|
||||||
|
|
||||||
_restoreSavedScrollState: function() {
|
|
||||||
var scrollState = this.state.searchResults ? this.savedSearchScrollState : this.savedScrollState;
|
|
||||||
if (!scrollState || scrollState.atBottom) {
|
|
||||||
this.scrollToBottom();
|
|
||||||
} else if (scrollState.lastDisplayedScrollToken) {
|
|
||||||
this._scrollToToken(scrollState.lastDisplayedScrollToken,
|
|
||||||
scrollState.pixelOffset);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_calculateScrollState: function() {
|
|
||||||
// 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
|
|
||||||
// will hopefully be near enough.
|
|
||||||
//
|
|
||||||
// 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 messageWrapperScroll = this._getScrollNode();
|
|
||||||
// + 1 here to avoid fractional pixel rounding errors
|
|
||||||
var atBottom = messageWrapperScroll.scrollHeight - messageWrapperScroll.scrollTop <= messageWrapperScroll.clientHeight + 1;
|
|
||||||
|
|
||||||
var messageWrapper = this.refs.messagePanel;
|
|
||||||
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
|
||||||
var messages = this.refs.messageList.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 };
|
|
||||||
},
|
|
||||||
|
|
||||||
// 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.messageList.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 messageWrapper = this.refs.messagePanel;
|
|
||||||
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// get the current scroll position of the room, so that it can be
|
// get the current scroll position of the room, so that it can be
|
||||||
// restored when we switch back to it
|
// restored when we switch back to it
|
||||||
getScrollState: function() {
|
getScrollState: function() {
|
||||||
return this.savedScrollState;
|
var messagePanel = this.refs.messagePanel;
|
||||||
|
if (!messagePanel) return null;
|
||||||
|
|
||||||
|
return messagePanel.getScrollState();
|
||||||
},
|
},
|
||||||
|
|
||||||
restoreScrollState: function(scrollState) {
|
restoreScrollState: function(scrollState) {
|
||||||
if (!this.refs.messagePanel) return;
|
var messagePanel = this.refs.messagePanel;
|
||||||
|
if (!messagePanel) return null;
|
||||||
|
|
||||||
if(scrollState.atBottom) {
|
if(scrollState.atBottom) {
|
||||||
// we were at the bottom before. Ideally we'd scroll to the
|
// we were at the bottom before. Ideally we'd scroll to the
|
||||||
// 'read-up-to' mark here.
|
// 'read-up-to' mark here.
|
||||||
|
messagePanel.scrollToBottom();
|
||||||
|
|
||||||
} else if (scrollState.lastDisplayedScrollToken) {
|
} else if (scrollState.lastDisplayedScrollToken) {
|
||||||
// we might need to backfill, so we call scrollToEvent rather than
|
// we might need to backfill, so we call scrollToEvent rather than
|
||||||
// _scrollToToken here. The scrollTokens on our DOM nodes are the
|
// scrollToToken here. The scrollTokens on our DOM nodes are the
|
||||||
// event IDs, so lastDisplayedScrollToken will be the event ID we need,
|
// event IDs, so lastDisplayedScrollToken will be the event ID we need,
|
||||||
// and we can pass it directly into scrollToEvent.
|
// and we can pass it directly into scrollToEvent.
|
||||||
this.scrollToEvent(scrollState.lastDisplayedScrollToken,
|
this.scrollToEvent(scrollState.lastDisplayedScrollToken,
|
||||||
|
@ -1212,6 +1072,7 @@ module.exports = React.createClass({
|
||||||
var CallView = sdk.getComponent("voip.CallView");
|
var CallView = sdk.getComponent("voip.CallView");
|
||||||
var RoomSettings = sdk.getComponent("rooms.RoomSettings");
|
var RoomSettings = sdk.getComponent("rooms.RoomSettings");
|
||||||
var SearchBar = sdk.getComponent("rooms.SearchBar");
|
var SearchBar = sdk.getComponent("rooms.SearchBar");
|
||||||
|
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||||
|
|
||||||
if (!this.state.room) {
|
if (!this.state.room) {
|
||||||
if (this.props.roomId) {
|
if (this.props.roomId) {
|
||||||
|
@ -1298,6 +1159,21 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
else if (this.tabComplete.isTabCompleting()) {
|
||||||
|
var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar');
|
||||||
|
statusBar = (
|
||||||
|
<div className="mx_RoomView_tabCompleteBar">
|
||||||
|
<div className="mx_RoomView_tabCompleteImage">...</div>
|
||||||
|
<div className="mx_RoomView_tabCompleteWrapper">
|
||||||
|
<TabCompleteBar entries={this.tabComplete.peek(6)} />
|
||||||
|
<div className="mx_RoomView_tabCompleteEol">
|
||||||
|
<img src="img/eol.svg" width="22" height="16" alt="->|"/>
|
||||||
|
Auto-complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
else if (this.state.hasUnsentMessages) {
|
else if (this.state.hasUnsentMessages) {
|
||||||
statusBar = (
|
statusBar = (
|
||||||
<div className="mx_RoomView_connectionLostBar">
|
<div className="mx_RoomView_connectionLostBar">
|
||||||
|
@ -1378,7 +1254,9 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
if (canSpeak) {
|
if (canSpeak) {
|
||||||
messageComposer =
|
messageComposer =
|
||||||
<MessageComposer room={this.state.room} roomView={this} uploadFile={this.uploadFile} callState={this.state.callState} />
|
<MessageComposer
|
||||||
|
room={this.state.room} roomView={this} uploadFile={this.uploadFile}
|
||||||
|
callState={this.state.callState} tabComplete={this.tabComplete} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Why aren't we storing the term/scope/count in this format
|
// TODO: Why aren't we storing the term/scope/count in this format
|
||||||
|
@ -1387,7 +1265,7 @@ module.exports = React.createClass({
|
||||||
searchInfo = {
|
searchInfo = {
|
||||||
searchTerm : this.state.searchTerm,
|
searchTerm : this.state.searchTerm,
|
||||||
searchScope : this.state.searchScope,
|
searchScope : this.state.searchScope,
|
||||||
searchCount : this.state.searchCount,
|
searchCount : this.state.searchResults.count,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1433,6 +1311,33 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// if we have search results, we keep the messagepanel (so that it preserves its
|
||||||
|
// scroll state), but hide it.
|
||||||
|
var searchResultsPanel;
|
||||||
|
var hideMessagePanel = false;
|
||||||
|
|
||||||
|
if (this.state.searchResults) {
|
||||||
|
searchResultsPanel = (
|
||||||
|
<ScrollPanel ref="searchResultsPanel" className="mx_RoomView_messagePanel"
|
||||||
|
onFillRequest={ this.onSearchResultsFillRequest }>
|
||||||
|
<li className={scrollheader_classes}></li>
|
||||||
|
{this.getSearchResultTiles()}
|
||||||
|
</ScrollPanel>
|
||||||
|
);
|
||||||
|
hideMessagePanel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var messagePanel = (
|
||||||
|
<ScrollPanel ref="messagePanel" className="mx_RoomView_messagePanel"
|
||||||
|
onScroll={ this.onMessageListScroll }
|
||||||
|
onFillRequest={ this.onMessageListFillRequest }
|
||||||
|
style={ hideMessagePanel ? { display: 'none' } : {} } >
|
||||||
|
<li className={scrollheader_classes}></li>
|
||||||
|
{this.getEventTiles()}
|
||||||
|
</ScrollPanel>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") }>
|
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") }>
|
||||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||||
|
@ -1453,15 +1358,8 @@ module.exports = React.createClass({
|
||||||
{ conferenceCallNotification }
|
{ conferenceCallNotification }
|
||||||
{ aux }
|
{ aux }
|
||||||
</div>
|
</div>
|
||||||
<GeminiScrollbar autoshow={true} ref="messagePanel" className="mx_RoomView_messagePanel" onScroll={ this.onMessageListScroll }>
|
{ messagePanel }
|
||||||
<div className="mx_RoomView_messageListWrapper">
|
{ searchResultsPanel }
|
||||||
<ol ref="messageList" className="mx_RoomView_MessageList" aria-live="polite">
|
|
||||||
<li className={scrollheader_classes}>
|
|
||||||
</li>
|
|
||||||
{this.getEventTiles()}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</GeminiScrollbar>
|
|
||||||
<div className="mx_RoomView_statusArea">
|
<div className="mx_RoomView_statusArea">
|
||||||
<div className="mx_RoomView_statusAreaBox">
|
<div className="mx_RoomView_statusAreaBox">
|
||||||
<div className="mx_RoomView_statusAreaBox_line"></div>
|
<div className="mx_RoomView_statusAreaBox_line"></div>
|
||||||
|
|
376
src/components/structures/ScrollPanel.js
Normal file
376
src/components/structures/ScrollPanel.js
Normal file
|
@ -0,0 +1,376 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 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 q = require("q");
|
||||||
|
|
||||||
|
var DEBUG_SCROLL = false;
|
||||||
|
|
||||||
|
if (DEBUG_SCROLL) {
|
||||||
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
|
var debuglog = console.log.bind(console);
|
||||||
|
} else {
|
||||||
|
var debuglog = function () {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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.
|
||||||
|
*
|
||||||
|
* This should return a promise; no more calls will be made until the
|
||||||
|
* promise completes.
|
||||||
|
*
|
||||||
|
* The promise should resolve to true if there is more data to be
|
||||||
|
* retrieved in this direction (in which case onFillRequest may be
|
||||||
|
* called again immediately), or false if there is no more data in this
|
||||||
|
* directon (at this time) - which will stop the pagination cycle until
|
||||||
|
* the user scrolls again.
|
||||||
|
*/
|
||||||
|
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) { return q(false); },
|
||||||
|
onScroll: function() {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._pendingFillRequests = {b: null, f: null};
|
||||||
|
this.resetScrollState();
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.checkFillState();
|
||||||
|
},
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// we also re-check the fill state, in case the paginate was inadequate
|
||||||
|
this.checkFillState();
|
||||||
|
},
|
||||||
|
|
||||||
|
onScroll: function(ev) {
|
||||||
|
var sn = this._getScrollNode();
|
||||||
|
debuglog("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();
|
||||||
|
debuglog("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 there is less than a screenful of messages above or below the
|
||||||
|
// viewport, try to get some more messages.
|
||||||
|
//
|
||||||
|
// scrollTop is the number of pixels between the top of the content and
|
||||||
|
// the top of the viewport.
|
||||||
|
//
|
||||||
|
// scrollHeight is the total height of the content.
|
||||||
|
//
|
||||||
|
// clientHeight is the height of the viewport (excluding borders,
|
||||||
|
// margins, and scrollbars).
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// .---------. - -
|
||||||
|
// | | | scrollTop |
|
||||||
|
// .-+---------+-. - - |
|
||||||
|
// | | | | | |
|
||||||
|
// | | | | | clientHeight | scrollHeight
|
||||||
|
// | | | | | |
|
||||||
|
// `-+---------+-' - |
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// `---------' -
|
||||||
|
//
|
||||||
|
|
||||||
|
if (sn.scrollTop < sn.clientHeight) {
|
||||||
|
// need to back-fill
|
||||||
|
this._maybeFill(true);
|
||||||
|
}
|
||||||
|
if (sn.scrollTop > sn.scrollHeight - sn.clientHeight * 2) {
|
||||||
|
// need to forward-fill
|
||||||
|
this._maybeFill(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// check if there is already a pending fill request. If not, set one off.
|
||||||
|
_maybeFill: function(backwards) {
|
||||||
|
var dir = backwards ? 'b' : 'f';
|
||||||
|
if (this._pendingFillRequests[dir]) {
|
||||||
|
debuglog("ScrollPanel: Already a "+dir+" fill in progress - not starting another");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debuglog("ScrollPanel: starting "+dir+" fill");
|
||||||
|
|
||||||
|
// onFillRequest can end up calling us recursively (via onScroll
|
||||||
|
// events) so make sure we set this before firing off the call. That
|
||||||
|
// does present the risk that we might not ever actually fire off the
|
||||||
|
// fill request, so wrap it in a try/catch.
|
||||||
|
this._pendingFillRequests[dir] = true;
|
||||||
|
var fillPromise;
|
||||||
|
try {
|
||||||
|
fillPromise = this.props.onFillRequest(backwards);
|
||||||
|
} catch (e) {
|
||||||
|
this._pendingFillRequests[dir] = false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
q.finally(fillPromise, () => {
|
||||||
|
debuglog("ScrollPanel: "+dir+" fill complete");
|
||||||
|
this._pendingFillRequests[dir] = false;
|
||||||
|
}).then((hasMoreResults) => {
|
||||||
|
if (hasMoreResults) {
|
||||||
|
// further pagination requests have been disabled until now, so
|
||||||
|
// it's time to check the fill state again in case the pagination
|
||||||
|
// was insufficient.
|
||||||
|
this.checkFillState();
|
||||||
|
}
|
||||||
|
}).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
debuglog("Scrolled to top");
|
||||||
|
},
|
||||||
|
|
||||||
|
scrollToBottom: function() {
|
||||||
|
var scrollNode = this._getScrollNode();
|
||||||
|
scrollNode.scrollTop = scrollNode.scrollHeight;
|
||||||
|
debuglog("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;
|
||||||
|
}
|
||||||
|
|
||||||
|
debuglog("Scrolled to token", node.dataset.scrollToken, "+",
|
||||||
|
pixelOffset+":", scrollNode.scrollTop,
|
||||||
|
"(delta: "+scrollDelta+")");
|
||||||
|
debuglog("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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -17,14 +17,22 @@ var React = require('react');
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
var Modal = require('../../Modal');
|
var Modal = require('../../Modal');
|
||||||
|
var dis = require("../../dispatcher");
|
||||||
var q = require('q');
|
var q = require('q');
|
||||||
var version = require('../../../package.json').version;
|
var version = require('../../../package.json').version;
|
||||||
|
var UserSettingsStore = require('../../UserSettingsStore');
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'UserSettings',
|
displayName: 'UserSettings',
|
||||||
Phases: {
|
|
||||||
Loading: "loading",
|
propTypes: {
|
||||||
Display: "display",
|
onClose: React.PropTypes.func
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
onClose: function() {}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -32,131 +40,227 @@ module.exports = React.createClass({
|
||||||
avatarUrl: null,
|
avatarUrl: null,
|
||||||
threePids: [],
|
threePids: [],
|
||||||
clientVersion: version,
|
clientVersion: version,
|
||||||
phase: this.Phases.Loading,
|
phase: "UserSettings.LOADING", // LOADING, DISPLAY
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var cli = MatrixClientPeg.get();
|
this._refreshFromServer();
|
||||||
|
},
|
||||||
|
|
||||||
var profile_d = cli.getProfileInfo(cli.credentials.userId);
|
componentDidMount: function() {
|
||||||
var threepid_d = cli.getThreePids();
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
this._me = MatrixClientPeg.get().credentials.userId;
|
||||||
|
},
|
||||||
|
|
||||||
q.all([profile_d, threepid_d]).then(
|
componentWillUnmount: function() {
|
||||||
function(resps) {
|
dis.unregister(this.dispatcherRef);
|
||||||
|
},
|
||||||
|
|
||||||
|
_refreshFromServer: function() {
|
||||||
|
var self = this;
|
||||||
|
q.all([
|
||||||
|
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids()
|
||||||
|
]).done(function(resps) {
|
||||||
self.setState({
|
self.setState({
|
||||||
avatarUrl: resps[0].avatar_url,
|
avatarUrl: resps[0].avatar_url,
|
||||||
threepids: resps[1].threepids,
|
threepids: resps[1].threepids,
|
||||||
phase: self.Phases.Display,
|
phase: "UserSettings.DISPLAY",
|
||||||
|
});
|
||||||
|
}, function(error) {
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Can't load user settings",
|
||||||
|
description: error.toString()
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function(err) { console.err(err); }
|
|
||||||
);
|
onAction: function(payload) {
|
||||||
|
if (payload.action === "notifier_enabled") {
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
editAvatar: function() {
|
onAvatarSelected: function(ev) {
|
||||||
var url = MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl);
|
var self = this;
|
||||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
var changeAvatar = this.refs.changeAvatar;
|
||||||
var avatarDialog = (
|
if (!changeAvatar) {
|
||||||
<div>
|
console.error("No ChangeAvatar found to upload image to!");
|
||||||
<ChangeAvatar initialAvatarUrl={url} />
|
return;
|
||||||
<div className="mx_Dialog_buttons">
|
}
|
||||||
<button onClick={this.onAvatarDialogCancel}>Cancel</button>
|
changeAvatar.onFileSelected(ev).done(function() {
|
||||||
</div>
|
// dunno if the avatar changed, re-check it.
|
||||||
</div>
|
self._refreshFromServer();
|
||||||
);
|
}, function(err) {
|
||||||
this.avatarDialog = Modal.createDialogWithElement(avatarDialog);
|
var errMsg = (typeof err === "string") ? err : (err.error || "");
|
||||||
},
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
addEmail: function() {
|
title: "Error",
|
||||||
|
description: "Failed to set avatar. " + errMsg
|
||||||
},
|
});
|
||||||
|
});
|
||||||
editDisplayName: function() {
|
|
||||||
this.refs.displayname.edit();
|
|
||||||
},
|
|
||||||
|
|
||||||
changePassword: function() {
|
|
||||||
var ChangePassword = sdk.getComponent('settings.ChangePassword');
|
|
||||||
Modal.createDialog(ChangePassword);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onLogoutClicked: function(ev) {
|
onLogoutClicked: function(ev) {
|
||||||
var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt');
|
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() {
|
onLogoutPromptCancel: function() {
|
||||||
this.logoutModal.closeDialog();
|
this.logoutModal.closeDialog();
|
||||||
},
|
},
|
||||||
|
|
||||||
onAvatarDialogCancel: function() {
|
onEnableNotificationsChange: function(event) {
|
||||||
this.avatarDialog.close();
|
UserSettingsStore.setEnableNotifications(event.target.checked);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
switch (this.state.phase) {
|
||||||
|
case "UserSettings.LOADING":
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
if (this.state.phase === this.Phases.Loading) {
|
return (
|
||||||
return <Loader />
|
<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) {
|
// can only get here if phase is UserSettings.DISPLAY
|
||||||
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
|
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||||
var EnableNotificationsButton = sdk.getComponent('settings.EnableNotificationsButton');
|
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 (
|
return (
|
||||||
<div className="mx_UserSettings">
|
<div className="mx_UserSettings">
|
||||||
<div className="mx_UserSettings_User">
|
<RoomHeader simpleHeader="Settings" />
|
||||||
<h1>User Settings</h1>
|
|
||||||
<hr/>
|
<h2>Profile</h2>
|
||||||
<div className="mx_UserSettings_User_Inner">
|
|
||||||
<div className="mx_UserSettings_Avatar">
|
<div className="mx_UserSettings_section">
|
||||||
<div className="mx_UserSettings_Avatar_Text">
|
<div className="mx_UserSettings_profileTable">
|
||||||
Profile Photo
|
<div className="mx_UserSettings_profileTableRow">
|
||||||
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
|
<label htmlFor="displayName">Display name</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_Avatar_Edit" onClick={this.editAvatar}>
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
Edit
|
<ChangeDisplayName />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_UserSettings_DisplayName">
|
{this.state.threepids.map(function(val, pidIndex) {
|
||||||
<ChangeDisplayName ref="displayname" />
|
var id = "email-" + val.address;
|
||||||
<div className="mx_UserSettings_DisplayName_Edit" onClick={this.editDisplayName}>
|
return (
|
||||||
Edit
|
<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>
|
||||||
|
);
|
||||||
<div className="mx_UserSettings_3pids">
|
|
||||||
{this.state.threepids.map(function(val) {
|
|
||||||
return <div key={val.address}>{val.address}</div>;
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_UserSettings_Add3pid" onClick={this.addEmail}>
|
<div className="mx_UserSettings_avatarPicker">
|
||||||
Add email
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_UserSettings_Global">
|
<h2>Account</h2>
|
||||||
<h1>Global Settings</h1>
|
|
||||||
<hr/>
|
<div className="mx_UserSettings_section">
|
||||||
<div className="mx_UserSettings_Global_Inner">
|
<ChangePassword
|
||||||
<div className="mx_UserSettings_ChangePassword" onClick={this.changePassword}>
|
className="mx_UserSettings_accountTable"
|
||||||
Change Password
|
rowClassName="mx_UserSettings_profileTableRow"
|
||||||
|
rowLabelClassName="mx_UserSettings_profileLabelCell"
|
||||||
|
rowInputClassName="mx_UserSettings_profileInputCell"
|
||||||
|
buttonClassName="mx_UserSettings_button"
|
||||||
|
onError={this.onPasswordChangeError}
|
||||||
|
onFinished={this.onPasswordChanged} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_ClientVersion">
|
|
||||||
|
<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}
|
Version {this.state.clientVersion}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserSettings_EnableNotifications">
|
|
||||||
<EnableNotificationsButton />
|
|
||||||
</div>
|
|
||||||
<div className="mx_UserSettings_Logout">
|
|
||||||
<button onClick={this.onLogoutClicked}>Sign Out</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -56,8 +56,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (this.props.homeserver) {
|
if (this.props.homeserver) {
|
||||||
if (curr_val == "") {
|
if (curr_val == "") {
|
||||||
|
var self = this;
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
target.value = "#:" + this.props.homeserver;
|
target.value = "#:" + self.props.homeserver;
|
||||||
target.setSelectionRange(1, 1);
|
target.setSelectionRange(1, 1);
|
||||||
}, 0);
|
}, 0);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -113,6 +113,10 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onBlur: function() {
|
||||||
|
this.cancelEdit();
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var editable_el;
|
var editable_el;
|
||||||
|
|
||||||
|
@ -125,7 +129,8 @@ module.exports = React.createClass({
|
||||||
} else if (this.state.phase == this.Phases.Edit) {
|
} else if (this.state.phase == this.Phases.Edit) {
|
||||||
editable_el = (
|
editable_el = (
|
||||||
<div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -54,8 +54,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (httpUrl) {
|
if (httpUrl) {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileTile">
|
<span className="mx_MFileBody">
|
||||||
<div className="mx_MImageTile_download">
|
<div className="mx_MImageBody_download">
|
||||||
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
||||||
<img src="img/download.png" width="10" height="12"/>
|
<img src="img/download.png" width="10" height="12"/>
|
||||||
Download {text}
|
Download {text}
|
||||||
|
@ -65,7 +65,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
var extra = text ? ': '+text : '';
|
var extra = text ? ': '+text : '';
|
||||||
return <span className="mx_MFileTile">
|
return <span className="mx_MFileBody">
|
||||||
Invalid file{extra}
|
Invalid file{extra}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -109,14 +109,14 @@ module.exports = React.createClass({
|
||||||
var thumbUrl = this._getThumbUrl();
|
var thumbUrl = this._getThumbUrl();
|
||||||
if (thumbUrl) {
|
if (thumbUrl) {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MImageTile">
|
<span className="mx_MImageBody">
|
||||||
<a href={cli.mxcUrlToHttp(content.url)} onClick={ this.onClick }>
|
<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}
|
alt={content.body} style={imgStyle}
|
||||||
onMouseEnter={this.onImageEnter}
|
onMouseEnter={this.onImageEnter}
|
||||||
onMouseLeave={this.onImageLeave} />
|
onMouseLeave={this.onImageLeave} />
|
||||||
</a>
|
</a>
|
||||||
<div className="mx_MImageTile_download">
|
<div className="mx_MImageBody_download">
|
||||||
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
||||||
<img src="img/download.png" width="10" height="12"/>
|
<img src="img/download.png" width="10" height="12"/>
|
||||||
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
|
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
|
||||||
|
@ -126,13 +126,13 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
} else if (content.body) {
|
} else if (content.body) {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MImageTile">
|
<span className="mx_MImageBody">
|
||||||
Image '{content.body}' cannot be displayed.
|
Image '{content.body}' cannot be displayed.
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MImageTile">
|
<span className="mx_MImageBody">
|
||||||
This image cannot be displayed.
|
This image cannot be displayed.
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -70,8 +70,8 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="mx_MVideoTile">
|
<span className="mx_MVideoBody">
|
||||||
<video className="mx_MVideoTile" src={cli.mxcUrlToHttp(content.url)} alt={content.body}
|
<video className="mx_MVideoBody" src={cli.mxcUrlToHttp(content.url)} alt={content.body}
|
||||||
controls preload={preload} autoPlay="0"
|
controls preload={preload} autoPlay="0"
|
||||||
height={height} width={width} poster={poster}>
|
height={height} width={width} poster={poster}>
|
||||||
</video>
|
</video>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -47,6 +47,7 @@ module.exports = React.createClass({
|
||||||
TileType = tileTypes[msgtype];
|
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} />;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -49,25 +49,26 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
var mxEvent = this.props.mxEvent;
|
var mxEvent = this.props.mxEvent;
|
||||||
var content = mxEvent.getContent();
|
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) {
|
switch (content.msgtype) {
|
||||||
case "m.emote":
|
case "m.emote":
|
||||||
var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
|
||||||
return (
|
return (
|
||||||
<span ref="content" className="mx_MEmoteTile mx_MessageTile_content">
|
<span ref="content" className="mx_MEmoteBody mx_EventTile_content">
|
||||||
* { name } { body }
|
* { name } { body }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case "m.notice":
|
case "m.notice":
|
||||||
return (
|
return (
|
||||||
<span ref="content" className="mx_MNoticeTile mx_MessageTile_content">
|
<span ref="content" className="mx_MNoticeBody mx_EventTile_content">
|
||||||
{ body }
|
{ body }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
default: // including "m.text"
|
default: // including "m.text"
|
||||||
return (
|
return (
|
||||||
<span ref="content" className="mx_MTextTile mx_MessageTile_content">
|
<span ref="content" className="mx_MTextBody mx_EventTile_content">
|
||||||
{ body }
|
{ body }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -34,7 +34,7 @@ module.exports = React.createClass({
|
||||||
if (text == null || text.length == 0) return null;
|
if (text == null || text.length == 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_EventAsTextTile">
|
<div className="mx_TextualEvent">
|
||||||
{TextForEvent.textForEvent(this.props.mxEvent)}
|
{TextForEvent.textForEvent(this.props.mxEvent)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -24,7 +24,7 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
var content = this.props.mxEvent.getContent();
|
var content = this.props.mxEvent.getContent();
|
||||||
return (
|
return (
|
||||||
<span className="mx_UnknownMessageTile">
|
<span className="mx_UnknownBody">
|
||||||
{content.body}
|
{content.body}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -74,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() {
|
getInitialState: function() {
|
||||||
return {menu: false, allReadAvatars: false};
|
return {menu: false, allReadAvatars: false};
|
||||||
},
|
},
|
||||||
|
@ -134,6 +160,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
for (var i = 0; i < receipts.length; ++i) {
|
for (var i = 0; i < receipts.length; ++i) {
|
||||||
var member = room.getMember(receipts[i].userId);
|
var member = room.getMember(receipts[i].userId);
|
||||||
|
if (!member) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Using react refs here would mean both getting Velociraptor to expose
|
// Using react refs here would mean both getting Velociraptor to expose
|
||||||
// them and making them scoped to the whole RoomView. Not impossible, but
|
// them and making them scoped to the whole RoomView. Not impossible, but
|
||||||
|
@ -280,7 +309,8 @@ module.exports = React.createClass({
|
||||||
{ avatar }
|
{ avatar }
|
||||||
{ sender }
|
{ sender }
|
||||||
<div className="mx_EventTile_line">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -254,7 +254,7 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.onPopulateInvite}>
|
<form onSubmit={this.onPopulateInvite}>
|
||||||
<input className="mx_MemberList_invite" ref="invite" placeholder="Invite another user"/>
|
<input className="mx_MemberList_invite" ref="invite" placeholder="Invite user (email)"/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -31,6 +31,7 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var SlashCommands = require("../../../SlashCommands");
|
var SlashCommands = require("../../../SlashCommands");
|
||||||
var Modal = require("../../../Modal");
|
var Modal = require("../../../Modal");
|
||||||
var CallHandler = require('../../../CallHandler');
|
var CallHandler = require('../../../CallHandler');
|
||||||
|
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
|
@ -64,14 +65,13 @@ function mdownToHtml(mdown) {
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MessageComposer',
|
displayName: 'MessageComposer',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
tabComplete: React.PropTypes.any
|
||||||
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this.oldScrollHeight = 0;
|
this.oldScrollHeight = 0;
|
||||||
this.markdownEnabled = MARKDOWN_ENABLED;
|
this.markdownEnabled = MARKDOWN_ENABLED;
|
||||||
this.tabStruct = {
|
|
||||||
completing: false,
|
|
||||||
original: null,
|
|
||||||
index: 0
|
|
||||||
};
|
|
||||||
var self = this;
|
var self = this;
|
||||||
this.sentHistory = {
|
this.sentHistory = {
|
||||||
// The list of typed messages. Index 0 is more recent
|
// The list of typed messages. Index 0 is more recent
|
||||||
|
@ -172,6 +172,9 @@ module.exports = React.createClass({
|
||||||
this.props.room.roomId
|
this.props.room.roomId
|
||||||
);
|
);
|
||||||
this.resizeInput();
|
this.resizeInput();
|
||||||
|
if (this.props.tabComplete) {
|
||||||
|
this.props.tabComplete.setTextArea(this.refs.textarea);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -197,13 +200,6 @@ module.exports = React.createClass({
|
||||||
this.sentHistory.push(input);
|
this.sentHistory.push(input);
|
||||||
this.onEnter(ev);
|
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) {
|
else if (ev.keyCode === KeyCode.UP) {
|
||||||
var input = this.refs.textarea.value;
|
var input = this.refs.textarea.value;
|
||||||
var offset = this.refs.textarea.selectionStart || 0;
|
var offset = this.refs.textarea.selectionStart || 0;
|
||||||
|
@ -222,10 +218,9 @@ module.exports = React.createClass({
|
||||||
this.resizeInput();
|
this.resizeInput();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (ev.keyCode !== KeyCode.SHIFT && this.tabStruct.completing) {
|
|
||||||
// they're resuming typing; reset tab complete state vars.
|
if (this.props.tabComplete) {
|
||||||
this.tabStruct.completing = false;
|
this.props.tabComplete.onKeyDown(ev);
|
||||||
this.tabStruct.index = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -349,104 +344,6 @@ module.exports = React.createClass({
|
||||||
ev.preventDefault();
|
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() {
|
onTypingActivity: function() {
|
||||||
this.isTyping = true;
|
this.isTyping = true;
|
||||||
if (!this.userTypingTimer) {
|
if (!this.userTypingTimer) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -73,10 +73,15 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var header;
|
var header;
|
||||||
if (this.props.simpleHeader) {
|
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 =
|
header =
|
||||||
<div className="mx_RoomHeader_wrapper">
|
<div className="mx_RoomHeader_wrapper">
|
||||||
<div className="mx_RoomHeader_simpleHeader">
|
<div className="mx_RoomHeader_simpleHeader">
|
||||||
{ this.props.simpleHeader }
|
{ this.props.simpleHeader }
|
||||||
|
{ cancel }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -104,8 +109,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var searchStatus;
|
var searchStatus;
|
||||||
// don't display the search count until the search completes and
|
// don't display the search count until the search completes and
|
||||||
// gives us a non-null searchCount.
|
// gives us a valid (possibly zero) searchCount.
|
||||||
if (this.props.searchInfo && this.props.searchInfo.searchCount !== null) {
|
if (this.props.searchInfo && this.props.searchInfo.searchCount !== undefined && this.props.searchInfo.searchCount !== null) {
|
||||||
searchStatus = <div className="mx_RoomHeader_searchStatus"> (~{ this.props.searchInfo.searchCount } results)</div>;
|
searchStatus = <div className="mx_RoomHeader_searchStatus"> (~{ this.props.searchInfo.searchCount } results)</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -127,8 +127,16 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomReceipt: function(receiptEvent, room) {
|
onRoomReceipt: function(receiptEvent, room) {
|
||||||
// because if we read a message it will affect notification / unread counts
|
// because if we read a notification, it will affect notification count
|
||||||
|
// only bother updating if there's a receipt from us
|
||||||
|
var receiptKeys = Object.keys(receiptEvent.getContent());
|
||||||
|
for (var i = 0; i < receiptKeys.length; ++i) {
|
||||||
|
var rcpt = receiptEvent.getContent()[receiptKeys[i]];
|
||||||
|
if (rcpt['m.read'] && rcpt['m.read'][MatrixClientPeg.get().credentials.userId]) {
|
||||||
this.refreshRoomList();
|
this.refreshRoomList();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomName: function(room) {
|
onRoomName: function(room) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
64
src/components/views/rooms/SearchResultTile.js
Normal file
64
src/components/views/rooms/SearchResultTile.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'SearchResult',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// a matrix-js-sdk SearchResult containing the details of this result
|
||||||
|
searchResult: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
// a list of strings to be highlighted in the results
|
||||||
|
searchHighlights: React.PropTypes.array,
|
||||||
|
|
||||||
|
// callback to be called when the user selects this result
|
||||||
|
onSelect: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
|
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
|
var result = this.props.searchResult;
|
||||||
|
var mxEv = result.context.getEvent();
|
||||||
|
var eventId = mxEv.getId();
|
||||||
|
|
||||||
|
var ts1 = mxEv.getTs();
|
||||||
|
var ret = [<DateSeparator key={ts1 + "-search"} ts={ts1}/>];
|
||||||
|
|
||||||
|
var timeline = result.context.getTimeline();
|
||||||
|
for (var j = 0; j < timeline.length; j++) {
|
||||||
|
var ev = timeline[j];
|
||||||
|
var highlights;
|
||||||
|
var contextual = (j != result.context.getOurEventIndex());
|
||||||
|
if (!contextual) {
|
||||||
|
highlights = this.props.searchHighlights;
|
||||||
|
}
|
||||||
|
if (EventTile.haveTileForEvent(ev)) {
|
||||||
|
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
|
||||||
|
onHighlightClick={this.props.onSelect}/>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li data-scroll-token={eventId+"+"+j}>
|
||||||
|
{ret}
|
||||||
|
</li>);
|
||||||
|
},
|
||||||
|
});
|
46
src/components/views/rooms/TabCompleteBar.js
Normal file
46
src/components/views/rooms/TabCompleteBar.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -23,6 +23,9 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
initialAvatarUrl: React.PropTypes.string,
|
initialAvatarUrl: React.PropTypes.string,
|
||||||
room: React.PropTypes.object,
|
room: React.PropTypes.object,
|
||||||
|
// if false, you need to call changeAvatar.onFileSelected yourself.
|
||||||
|
showUploadSection: React.PropTypes.bool,
|
||||||
|
className: React.PropTypes.string
|
||||||
},
|
},
|
||||||
|
|
||||||
Phases: {
|
Phases: {
|
||||||
|
@ -31,6 +34,13 @@ module.exports = React.createClass({
|
||||||
Error: "error",
|
Error: "error",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
showUploadSection: true,
|
||||||
|
className: "mx_Dialog_content" // FIXME - shouldn't be this by default
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
avatarUrl: this.props.initialAvatarUrl,
|
avatarUrl: this.props.initialAvatarUrl,
|
||||||
|
@ -55,7 +65,7 @@ module.exports = React.createClass({
|
||||||
phase: this.Phases.Uploading
|
phase: this.Phases.Uploading
|
||||||
});
|
});
|
||||||
var self = this;
|
var self = this;
|
||||||
MatrixClientPeg.get().uploadContent(file).then(function(url) {
|
var httpPromise = MatrixClientPeg.get().uploadContent(file).then(function(url) {
|
||||||
newUrl = url;
|
newUrl = url;
|
||||||
if (self.props.room) {
|
if (self.props.room) {
|
||||||
return MatrixClientPeg.get().sendStateEvent(
|
return MatrixClientPeg.get().sendStateEvent(
|
||||||
|
@ -67,7 +77,9 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
return MatrixClientPeg.get().setAvatarUrl(url);
|
return MatrixClientPeg.get().setAvatarUrl(url);
|
||||||
}
|
}
|
||||||
}).done(function() {
|
});
|
||||||
|
|
||||||
|
httpPromise.done(function() {
|
||||||
self.setState({
|
self.setState({
|
||||||
phase: self.Phases.Display,
|
phase: self.Phases.Display,
|
||||||
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl)
|
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl)
|
||||||
|
@ -78,11 +90,13 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
self.onError(error);
|
self.onError(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return httpPromise;
|
||||||
},
|
},
|
||||||
|
|
||||||
onFileSelected: function(ev) {
|
onFileSelected: function(ev) {
|
||||||
this.avatarSet = true;
|
this.avatarSet = true;
|
||||||
this.setAvatarFromFile(ev.target.files[0]);
|
return this.setAvatarFromFile(ev.target.files[0]);
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: function(error) {
|
onError: function(error) {
|
||||||
|
@ -106,19 +120,26 @@ module.exports = React.createClass({
|
||||||
avatarImg = <img src={this.state.avatarUrl} style={style} />;
|
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) {
|
switch (this.state.phase) {
|
||||||
case this.Phases.Display:
|
case this.Phases.Display:
|
||||||
case this.Phases.Error:
|
case this.Phases.Error:
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_Dialog_content">
|
<div className={this.props.className}>
|
||||||
{avatarImg}
|
{avatarImg}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_content">
|
{uploadSection}
|
||||||
Upload new:
|
|
||||||
<input type="file" onChange={this.onFileSelected}/>
|
|
||||||
{this.state.errorText}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case this.Phases.Uploading:
|
case this.Phases.Uploading:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -98,7 +98,9 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
var EditableText = sdk.getComponent('elements.EditableText');
|
var EditableText = sdk.getComponent('elements.EditableText');
|
||||||
return (
|
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} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -18,30 +18,47 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
var sdk = require("../../../index");
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'ChangePassword',
|
displayName: 'ChangePassword',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onFinished: React.PropTypes.func,
|
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: {
|
Phases: {
|
||||||
Edit: "edit",
|
Edit: "edit",
|
||||||
Uploading: "uploading",
|
Uploading: "uploading",
|
||||||
Error: "error",
|
Error: "error"
|
||||||
Success: "Success"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
onFinished: function() {},
|
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() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
phase: this.Phases.Edit,
|
phase: this.Phases.Edit
|
||||||
errorString: ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -55,60 +72,72 @@ module.exports = React.createClass({
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: this.Phases.Uploading,
|
phase: this.Phases.Uploading
|
||||||
errorString: '',
|
});
|
||||||
})
|
|
||||||
|
|
||||||
var d = cli.setPassword(authDict, new_password);
|
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
d.then(function() {
|
cli.setPassword(authDict, new_password).then(function() {
|
||||||
self.setState({
|
self.props.onFinished();
|
||||||
phase: self.Phases.Success,
|
|
||||||
errorString: '',
|
|
||||||
})
|
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
|
self.props.onError(err);
|
||||||
|
}).finally(function() {
|
||||||
self.setState({
|
self.setState({
|
||||||
phase: self.Phases.Error,
|
phase: self.Phases.Edit
|
||||||
errorString: err.toString()
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
}).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
onClickChange: function() {
|
onClickChange: function() {
|
||||||
var old_password = this.refs.old_input.value;
|
var old_password = this.refs.old_input.value;
|
||||||
var new_password = this.refs.new_input.value;
|
var new_password = this.refs.new_input.value;
|
||||||
var confirm_password = this.refs.confirm_input.value;
|
var confirm_password = this.refs.confirm_input.value;
|
||||||
if (new_password != confirm_password) {
|
var err = this.props.onCheckPassword(
|
||||||
this.setState({
|
old_password, new_password, confirm_password
|
||||||
state: this.Phases.Error,
|
);
|
||||||
errorString: "Passwords don't match"
|
if (err) {
|
||||||
});
|
this.props.onError(err);
|
||||||
} else if (new_password == '' || old_password == '') {
|
}
|
||||||
this.setState({
|
else {
|
||||||
state: this.Phases.Error,
|
|
||||||
errorString: "Passwords can't be empty"
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.changePassword(old_password, new_password);
|
this.changePassword(old_password, new_password);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
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) {
|
switch (this.state.phase) {
|
||||||
case this.Phases.Edit:
|
case this.Phases.Edit:
|
||||||
case this.Phases.Error:
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={this.props.className}>
|
||||||
<div className="mx_Dialog_content">
|
<div className={rowClassName}>
|
||||||
<div>{this.state.errorString}</div>
|
<div className={rowLabelClassName}>
|
||||||
<div><label>Old password <input type="password" ref="old_input"/></label></div>
|
<label htmlFor="passwordold">Current password</label>
|
||||||
<div><label>New password <input type="password" ref="new_input"/></label></div>
|
|
||||||
<div><label>Confirm password <input type="password" ref="confirm_input"/></label></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className={rowInputClassName}>
|
||||||
<button onClick={this.onClickChange}>Change Password</button>
|
<input id="passwordold" type="password" ref="old_input" />
|
||||||
<button onClick={this.props.onFinished}>Cancel</button>
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -119,17 +148,6 @@ module.exports = React.createClass({
|
||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</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>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -25,8 +25,12 @@ var dis = require('../../../dispatcher');
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'VideoView',
|
displayName: 'VideoView',
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentDidMount: function() {
|
||||||
dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
},
|
},
|
||||||
|
|
||||||
getRemoteVideoElement: function() {
|
getRemoteVideoElement: function() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue