Merge branch 'rav/roomview_works' into rav/read_marker
Conflicts: src/components/structures/MessagePanel.js src/components/structures/TimelinePanel.js
This commit is contained in:
commit
029f47d91c
25 changed files with 520 additions and 245 deletions
|
@ -92,6 +92,7 @@ class ContentMessages {
|
||||||
this.inprogress.push(upload);
|
this.inprogress.push(upload);
|
||||||
dis.dispatch({action: 'upload_started'});
|
dis.dispatch({action: 'upload_started'});
|
||||||
|
|
||||||
|
var error;
|
||||||
var self = this;
|
var self = this;
|
||||||
return def.promise.then(function() {
|
return def.promise.then(function() {
|
||||||
upload.promise = matrixClient.uploadContent(file);
|
upload.promise = matrixClient.uploadContent(file);
|
||||||
|
@ -103,11 +104,10 @@ class ContentMessages {
|
||||||
dis.dispatch({action: 'upload_progress', upload: upload});
|
dis.dispatch({action: 'upload_progress', upload: upload});
|
||||||
}
|
}
|
||||||
}).then(function(url) {
|
}).then(function(url) {
|
||||||
dis.dispatch({action: 'upload_finished', upload: upload});
|
|
||||||
content.url = url;
|
content.url = url;
|
||||||
return matrixClient.sendMessage(roomId, content);
|
return matrixClient.sendMessage(roomId, content);
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
dis.dispatch({action: 'upload_failed', upload: upload});
|
error = err;
|
||||||
if (!upload.canceled) {
|
if (!upload.canceled) {
|
||||||
var desc = "The file '"+upload.fileName+"' failed to upload.";
|
var desc = "The file '"+upload.fileName+"' failed to upload.";
|
||||||
if (err.http_status == 413) {
|
if (err.http_status == 413) {
|
||||||
|
@ -128,6 +128,12 @@ class ContentMessages {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (error) {
|
||||||
|
dis.dispatch({action: 'upload_failed', upload: upload});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dis.dispatch({action: 'upload_finished', upload: upload});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
103
src/HtmlUtils.js
103
src/HtmlUtils.js
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ReactDOMServer = require('react-dom/server')
|
|
||||||
var sanitizeHtml = require('sanitize-html');
|
var sanitizeHtml = require('sanitize-html');
|
||||||
var highlight = require('highlight.js');
|
var highlight = require('highlight.js');
|
||||||
|
|
||||||
|
@ -50,14 +49,23 @@ var sanitizeHtmlParams = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class Highlighter {
|
class BaseHighlighter {
|
||||||
constructor(html, highlightClass, onHighlightClick) {
|
constructor(highlightClass, highlightLink) {
|
||||||
this.html = html;
|
|
||||||
this.highlightClass = highlightClass;
|
this.highlightClass = highlightClass;
|
||||||
this.onHighlightClick = onHighlightClick;
|
this.highlightLink = highlightLink;
|
||||||
this._key = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* apply the highlights to a section of text
|
||||||
|
*
|
||||||
|
* @param {string} safeSnippet The snippet of text to apply the highlights
|
||||||
|
* to.
|
||||||
|
* @param {string[]} safeHighlights A list of substrings to highlight,
|
||||||
|
* sorted by descending length.
|
||||||
|
*
|
||||||
|
* returns a list of results (strings for HtmlHighligher, react nodes for
|
||||||
|
* TextHighlighter).
|
||||||
|
*/
|
||||||
applyHighlights(safeSnippet, safeHighlights) {
|
applyHighlights(safeSnippet, safeHighlights) {
|
||||||
var lastOffset = 0;
|
var lastOffset = 0;
|
||||||
var offset;
|
var offset;
|
||||||
|
@ -71,10 +79,12 @@ class Highlighter {
|
||||||
nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
|
nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
|
||||||
}
|
}
|
||||||
|
|
||||||
// do highlight
|
// do highlight. use the original string rather than safeHighlight
|
||||||
nodes.push(this._createSpan(safeHighlight, true));
|
// to preserve the original casing.
|
||||||
|
var endOffset = offset + safeHighlight.length;
|
||||||
|
nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true));
|
||||||
|
|
||||||
lastOffset = offset + safeHighlight.length;
|
lastOffset = endOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle postamble
|
// handle postamble
|
||||||
|
@ -92,31 +102,62 @@ class Highlighter {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// no more highlights to be found, just return the unhighlighted string
|
// no more highlights to be found, just return the unhighlighted string
|
||||||
return [this._createSpan(safeSnippet, false)];
|
return [this._processSnippet(safeSnippet, false)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HtmlHighlighter extends BaseHighlighter {
|
||||||
|
/* highlight the given snippet if required
|
||||||
|
*
|
||||||
|
* snippet: content of the span; must have been sanitised
|
||||||
|
* highlight: true to highlight as a search match
|
||||||
|
*
|
||||||
|
* returns an HTML string
|
||||||
|
*/
|
||||||
|
_processSnippet(snippet, highlight) {
|
||||||
|
if (!highlight) {
|
||||||
|
// nothing required here
|
||||||
|
return snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
var span = "<span class=\""+this.highlightClass+"\">"
|
||||||
|
+ snippet + "</span>";
|
||||||
|
|
||||||
|
if (this.highlightLink) {
|
||||||
|
span = "<a href=\""+encodeURI(this.highlightLink)+"\">"
|
||||||
|
+span+"</a>";
|
||||||
|
}
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextHighlighter extends BaseHighlighter {
|
||||||
|
constructor(highlightClass, highlightLink) {
|
||||||
|
super(highlightClass, highlightLink);
|
||||||
|
this._key = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* create a <span> node to hold the given content
|
/* create a <span> node to hold the given content
|
||||||
*
|
*
|
||||||
* spanBody: content of the span. If html, must have been sanitised
|
* snippet: content of the span
|
||||||
* highlight: true to highlight as a search match
|
* highlight: true to highlight as a search match
|
||||||
|
*
|
||||||
|
* returns a React node
|
||||||
*/
|
*/
|
||||||
_createSpan(spanBody, highlight) {
|
_processSnippet(snippet, highlight) {
|
||||||
var spanProps = {
|
var key = this._key++;
|
||||||
key: this._key++,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (highlight) {
|
var node =
|
||||||
spanProps.onClick = this.onHighlightClick;
|
<span key={key} className={highlight ? this.highlightClass : null }>
|
||||||
spanProps.className = this.highlightClass;
|
{ snippet }
|
||||||
|
</span>;
|
||||||
|
|
||||||
|
if (highlight && this.highlightLink) {
|
||||||
|
node = <a key={key} href={this.highlightLink}>{node}</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.html) {
|
return node;
|
||||||
return (<span {...spanProps} dangerouslySetInnerHTML={{ __html: spanBody }} />);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return (<span {...spanProps}>{ spanBody }</span>);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,8 +169,7 @@ module.exports = {
|
||||||
*
|
*
|
||||||
* highlights: optional list of words to highlight, ordered by longest word first
|
* highlights: optional list of words to highlight, ordered by longest word first
|
||||||
*
|
*
|
||||||
* opts.onHighlightClick: optional callback function to be called when a
|
* opts.highlightLink: optional href to add to highlights
|
||||||
* highlighted word is clicked
|
|
||||||
*/
|
*/
|
||||||
bodyToHtml: function(content, highlights, opts) {
|
bodyToHtml: function(content, highlights, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
@ -144,18 +184,13 @@ module.exports = {
|
||||||
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
|
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
|
||||||
try {
|
try {
|
||||||
if (highlights && highlights.length > 0) {
|
if (highlights && highlights.length > 0) {
|
||||||
var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
|
var highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
||||||
var safeHighlights = highlights.map(function(highlight) {
|
var safeHighlights = highlights.map(function(highlight) {
|
||||||
return sanitizeHtml(highlight, sanitizeHtmlParams);
|
return sanitizeHtml(highlight, sanitizeHtmlParams);
|
||||||
});
|
});
|
||||||
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
|
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
|
||||||
sanitizeHtmlParams.textFilter = function(safeText) {
|
sanitizeHtmlParams.textFilter = function(safeText) {
|
||||||
return highlighter.applyHighlights(safeText, safeHighlights).map(function(span) {
|
return highlighter.applyHighlights(safeText, safeHighlights).join('');
|
||||||
// XXX: rather clunky conversion from the react nodes returned by applyHighlights
|
|
||||||
// (which need to be nodes for the non-html highlighting case), to convert them
|
|
||||||
// back into raw HTML given that's what sanitize-html works in terms of.
|
|
||||||
return ReactDOMServer.renderToString(span);
|
|
||||||
}).join('');
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
|
||||||
|
@ -167,7 +202,7 @@ module.exports = {
|
||||||
} else {
|
} else {
|
||||||
safeBody = content.body;
|
safeBody = content.body;
|
||||||
if (highlights && highlights.length > 0) {
|
if (highlights && highlights.length > 0) {
|
||||||
var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
|
var highlighter = new TextHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
||||||
return highlighter.applyHighlights(safeBody, highlights);
|
return highlighter.applyHighlights(safeBody, highlights);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -182,6 +182,9 @@ var Notifier = {
|
||||||
if (state === "PREPARED" || state === "SYNCING") {
|
if (state === "PREPARED" || state === "SYNCING") {
|
||||||
this.isPrepared = true;
|
this.isPrepared = true;
|
||||||
}
|
}
|
||||||
|
else if (state === "STOPPED" || state === "ERROR") {
|
||||||
|
this.isPrepared = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
||||||
|
|
|
@ -64,6 +64,7 @@ var cssAttrs = [
|
||||||
"borderColor",
|
"borderColor",
|
||||||
"borderTopColor",
|
"borderTopColor",
|
||||||
"borderBottomColor",
|
"borderBottomColor",
|
||||||
|
"borderLeftColor",
|
||||||
];
|
];
|
||||||
|
|
||||||
var svgAttrs = [
|
var svgAttrs = [
|
||||||
|
|
|
@ -175,7 +175,7 @@ module.exports = React.createClass({
|
||||||
guest: true
|
guest: true
|
||||||
});
|
});
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error(err.data);
|
console.error("Failed to register as guest: " + err + " " + err.data);
|
||||||
self._setAutoRegisterAsGuest(false);
|
self._setAutoRegisterAsGuest(false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -970,7 +970,9 @@ module.exports = React.createClass({
|
||||||
onRegisterClick={this.onRegisterClick}
|
onRegisterClick={this.onRegisterClick}
|
||||||
homeserverUrl={this.props.config.default_hs_url}
|
homeserverUrl={this.props.config.default_hs_url}
|
||||||
identityServerUrl={this.props.config.default_is_url}
|
identityServerUrl={this.props.config.default_is_url}
|
||||||
onForgotPasswordClick={this.onForgotPasswordClick} />
|
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||||
|
onLoginAsGuestClick={this.props.enableGuest && this.props.config && this.props.config.default_hs_url ? this._registerAsGuest: undefined}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,11 +51,6 @@ module.exports = React.createClass({
|
||||||
// for more details.
|
// for more details.
|
||||||
stickyBottom: React.PropTypes.bool,
|
stickyBottom: React.PropTypes.bool,
|
||||||
|
|
||||||
// callback to determine if a user is the magic freeswitch conference
|
|
||||||
// user. Takes one parameter, which is a user id. Should return true if
|
|
||||||
// the user is the conference user.
|
|
||||||
isConferenceUser: React.PropTypes.func,
|
|
||||||
|
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: React.PropTypes.func,
|
onScroll: React.PropTypes.func,
|
||||||
|
|
||||||
|
@ -163,54 +158,20 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
this.eventNodes = {};
|
this.eventNodes = {};
|
||||||
|
|
||||||
// we do two passes over the events list; first of all, we figure out
|
var i;
|
||||||
// which events we want to show, and where the read markers fit into
|
|
||||||
// the list; then we actually create the event tiles. This allows us to
|
|
||||||
// behave slightly differently for the last event in the list.
|
|
||||||
//
|
|
||||||
// (Arguably we could do this when the events are added to this.props,
|
|
||||||
// but that would make it trickier to keep in sync with the read marker, given
|
|
||||||
// the read marker isn't necessarily on an event which we will show).
|
|
||||||
//
|
|
||||||
var eventsToShow = [];
|
|
||||||
|
|
||||||
// the index in 'eventsToShow' of the event *before* which we put the
|
// first figure out which is the last event in the list which we're
|
||||||
// read marker or its ghost. (Note that it may be equal to
|
// actually going to show; this allows us to behave slightly
|
||||||
// eventsToShow.length, which means it would be at the end of the timeline)
|
// differently for the last event in the list.
|
||||||
var ghostIndex, readMarkerIndex;
|
for (i = this.props.events.length-1; i >= 0; i--) {
|
||||||
|
|
||||||
for (var i = 0; i < this.props.events.length; i++) {
|
|
||||||
var mxEv = this.props.events[i];
|
var mxEv = this.props.events[i];
|
||||||
var wantTile = true;
|
|
||||||
|
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
wantTile = false;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.isConferenceUser && mxEv.getType() === "m.room.member") {
|
break;
|
||||||
if (this.props.isConferenceUser(mxEv.getSender()) ||
|
|
||||||
this.props.isConferenceUser(mxEv.getStateKey())) {
|
|
||||||
wantTile = false; // suppress conf user join/parts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wantTile) {
|
|
||||||
eventsToShow.push(mxEv);
|
|
||||||
}
|
|
||||||
|
|
||||||
var eventId = mxEv.getId();
|
|
||||||
if (eventId == this.props.readMarkerEventId) {
|
|
||||||
readMarkerIndex = eventsToShow.length;
|
|
||||||
} else if (eventId == this.currentReadMarkerEventId && !this.currentGhostEventId) {
|
|
||||||
// there is currently a read-up-to marker at this point, but no
|
|
||||||
// more. Show an animation of it disappearing.
|
|
||||||
ghostIndex = eventsToShow.length;
|
|
||||||
this.currentGhostEventId = eventId;
|
|
||||||
} else if (eventId == this.currentGhostEventId) {
|
|
||||||
// if we're showing an animation, continue to show it.
|
|
||||||
ghostIndex = eventsToShow.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
var lastShownEventIndex = i;
|
||||||
|
|
||||||
var ret = [];
|
var ret = [];
|
||||||
|
|
||||||
|
@ -219,42 +180,54 @@ module.exports = React.createClass({
|
||||||
// assume there is no read marker until proven otherwise
|
// assume there is no read marker until proven otherwise
|
||||||
var readMarkerVisible = false;
|
var readMarkerVisible = false;
|
||||||
|
|
||||||
for (var i = 0; i < eventsToShow.length; i++) {
|
for (i = 0; i < this.props.events.length; i++) {
|
||||||
var mxEv = eventsToShow[i];
|
var mxEv = this.props.events[i];
|
||||||
var wantTile = true;
|
var wantTile = true;
|
||||||
|
var eventId = mxEv.getId();
|
||||||
|
|
||||||
// insert the read marker if appropriate.
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
if (i == readMarkerIndex) {
|
wantTile = false;
|
||||||
var visible = this.props.readMarkerVisible;
|
|
||||||
|
|
||||||
// XXX is this still needed?
|
|
||||||
// suppress the read marker if the next event is sent by us; this
|
|
||||||
// is a nonsensical and temporary situation caused by the delay between
|
|
||||||
// us sending a message and receiving the synthesized receipt.
|
|
||||||
if (mxEv.sender && mxEv.sender.userId == this.props.ourUserId) {
|
|
||||||
visible = false;
|
|
||||||
}
|
|
||||||
ret.push(this._getReadMarkerTile(visible));
|
|
||||||
readMarkerVisible = visible;
|
|
||||||
} else if (i == ghostIndex) {
|
|
||||||
ret.push(this._getReadMarkerGhostTile());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var last = false;
|
var last = (i == lastShownEventIndex);
|
||||||
if (i == eventsToShow.length - 1) {
|
|
||||||
last = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the tiles for this event
|
if (wantTile) {
|
||||||
ret.push(this._getTilesForEvent(prevEvent, mxEv, last));
|
ret.push(this._getTilesForEvent(prevEvent, mxEv, last));
|
||||||
prevEvent = mxEv;
|
} else if (!mxEv.status) {
|
||||||
|
// if we aren't showing the event, put in a dummy scroll token anyway, so
|
||||||
|
// that we can scroll to the right place.
|
||||||
|
ret.push(<li key={eventId} data-scroll-token={eventId}/>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventId == this.props.readMarkerEventId) {
|
||||||
|
var visible = this.props.readMarkerVisible;
|
||||||
|
|
||||||
// if the read marker comes at the end of the timeline, we don't want
|
// if the read marker comes at the end of the timeline, we don't want
|
||||||
// to show it, but we still want to create the <li/> for it so that the
|
// to show it, but we still want to create the <li/> for it so that the
|
||||||
// algorithms which depend on its position on the screen aren't confused.
|
// algorithms which depend on its position on the screen aren't confused.
|
||||||
if (i == readMarkerIndex) {
|
if (i >= lastShownEventIndex) {
|
||||||
ret.push(this._getReadMarkerTile(false));
|
visible = false;
|
||||||
|
} else {
|
||||||
|
// XXX is this still needed?
|
||||||
|
// suppress the read marker if the next event is sent by us; this
|
||||||
|
// is a nonsensical and temporary situation caused by the delay between
|
||||||
|
// us sending a message and receiving the synthesized receipt.
|
||||||
|
var nextEvent = this.props.events[i+1];
|
||||||
|
if (nextEvent.sender && nextEvent.sender.userId == this.props.ourUserId) {
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret.push(this._getReadMarkerTile(visible));
|
||||||
|
readMarkerVisible = visible;
|
||||||
|
} else if (eventId == this.currentReadMarkerEventId && !this.currentGhostEventId) {
|
||||||
|
// there is currently a read-up-to marker at this point, but no
|
||||||
|
// more. Show an animation of it disappearing.
|
||||||
|
ret.push(this._getReadMarkerGhostTile());
|
||||||
|
this.currentGhostEventId = eventId;
|
||||||
|
} else if (eventId == this.currentGhostEventId) {
|
||||||
|
// if we're showing an animation, continue to show it.
|
||||||
|
ret.push(this._getReadMarkerGhostTile());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentReadMarkerEventId = readMarkerVisible ? this.props.readMarkerEventId : null;
|
this.currentReadMarkerEventId = readMarkerVisible ? this.props.readMarkerEventId : null;
|
||||||
|
@ -298,7 +271,8 @@ module.exports = React.createClass({
|
||||||
ref={this._collectEventNode.bind(this, eventId)}
|
ref={this._collectEventNode.bind(this, eventId)}
|
||||||
data-scroll-token={scrollToken}>
|
data-scroll-token={scrollToken}>
|
||||||
<EventTile mxEvent={mxEv} continuation={continuation}
|
<EventTile mxEvent={mxEv} continuation={continuation}
|
||||||
last={last} isSelectedEvent={highlight}/>
|
last={last} isSelectedEvent={highlight}
|
||||||
|
onImageLoad={this._onImageLoad} />
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -353,6 +327,16 @@ module.exports = React.createClass({
|
||||||
this.eventNodes[eventId] = node;
|
this.eventNodes[eventId] = node;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// once images in the events load, make the scrollPanel check the
|
||||||
|
// scroll offsets.
|
||||||
|
_onImageLoad: function() {
|
||||||
|
var scrollPanel = this.refs.messagePanel;
|
||||||
|
if (scrollPanel) {
|
||||||
|
scrollPanel.checkScroll();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -51,6 +51,11 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// callback for when the user clicks on the 'scroll to bottom' button
|
// callback for when the user clicks on the 'scroll to bottom' button
|
||||||
onScrollToBottomClick: React.PropTypes.func,
|
onScrollToBottomClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
// callback for when we do something that changes the size of the
|
||||||
|
// status bar. This is used to trigger a re-layout in the parent
|
||||||
|
// component.
|
||||||
|
onResize: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -63,8 +68,17 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentDidUpdate: function(prevProps, prevState) {
|
||||||
|
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
|
||||||
|
this.props.onResize();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
||||||
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange);
|
MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onSyncStateChange: function(state, prevState) {
|
onSyncStateChange: function(state, prevState) {
|
||||||
|
@ -76,7 +90,85 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
// determine if we need to call onResize
|
||||||
|
_checkForResize: function(prevProps, prevState) {
|
||||||
|
// figure out the old height and the new height of the status bar. We
|
||||||
|
// don't need the actual height - just whether it is likely to have
|
||||||
|
// changed - so we use '0' to indicate normal size, and other values to
|
||||||
|
// indicate other sizes.
|
||||||
|
var oldSize, newSize;
|
||||||
|
|
||||||
|
if (prevState.syncState === "ERROR") {
|
||||||
|
oldSize = 1;
|
||||||
|
} else if (prevProps.tabCompleteEntries) {
|
||||||
|
oldSize = 0;
|
||||||
|
} else if (prevProps.hasUnsentMessages) {
|
||||||
|
oldSize = 2;
|
||||||
|
} else {
|
||||||
|
oldSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.syncState === "ERROR") {
|
||||||
|
newSize = 1;
|
||||||
|
} else if (this.props.tabCompleteEntries) {
|
||||||
|
newSize = 0;
|
||||||
|
} else if (this.props.hasUnsentMessages) {
|
||||||
|
newSize = 2;
|
||||||
|
} else {
|
||||||
|
newSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSize != oldSize;
|
||||||
|
},
|
||||||
|
|
||||||
|
// return suitable content for the image on the left of the status bar.
|
||||||
|
//
|
||||||
|
// if wantPlaceholder is true, we include a "..." placeholder if
|
||||||
|
// there is nothing better to put in.
|
||||||
|
_getIndicator: function(wantPlaceholder) {
|
||||||
|
if (this.props.numUnreadMessages) {
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
||||||
|
onClick={ this.props.onScrollToBottomClick }>
|
||||||
|
<img src="img/newmessages.svg" width="24" height="24"
|
||||||
|
alt=""/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.props.atEndOfLiveTimeline) {
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomStatusBar_scrollDownIndicator"
|
||||||
|
onClick={ this.props.onScrollToBottomClick }>
|
||||||
|
<img src="img/scrolldown.svg" width="24" height="24"
|
||||||
|
alt="Scroll to bottom of page"
|
||||||
|
title="Scroll to bottom of page"/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.hasActiveCall) {
|
||||||
|
return (
|
||||||
|
<img src="img/sound-indicator.svg" width="23" height="20"/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.syncState === "ERROR") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wantPlaceholder) {
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomStatusBar_placeholderIndicator">...</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// return suitable content for the main (text) part of the status bar.
|
||||||
|
_getContent: function() {
|
||||||
var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar');
|
var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar');
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
|
@ -86,27 +178,24 @@ module.exports = React.createClass({
|
||||||
// a connection!
|
// a connection!
|
||||||
if (this.state.syncState === "ERROR") {
|
if (this.state.syncState === "ERROR") {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView_connectionLostBar">
|
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
||||||
<div className="mx_RoomView_connectionLostBar_textArea">
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
<div className="mx_RoomView_connectionLostBar_title">
|
|
||||||
Connectivity to the server has been lost.
|
Connectivity to the server has been lost.
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomView_connectionLostBar_desc">
|
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||||
Sent messages will be stored until your connection has returned.
|
Sent messages will be stored until your connection has returned.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.tabCompleteEntries) {
|
if (this.props.tabCompleteEntries) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView_tabCompleteBar">
|
<div className="mx_RoomStatusBar_tabCompleteBar">
|
||||||
<div className="mx_RoomView_tabCompleteImage">...</div>
|
<div className="mx_RoomStatusBar_tabCompleteWrapper">
|
||||||
<div className="mx_RoomView_tabCompleteWrapper">
|
|
||||||
<TabCompleteBar entries={this.props.tabCompleteEntries} />
|
<TabCompleteBar entries={this.props.tabCompleteEntries} />
|
||||||
<div className="mx_RoomView_tabCompleteEol" title="->|">
|
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
|
||||||
<TintableSvg src="img/eol.svg" width="22" height="16"/>
|
<TintableSvg src="img/eol.svg" width="22" height="16"/>
|
||||||
Auto-complete
|
Auto-complete
|
||||||
</div>
|
</div>
|
||||||
|
@ -117,20 +206,18 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (this.props.hasUnsentMessages) {
|
if (this.props.hasUnsentMessages) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView_connectionLostBar">
|
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
||||||
<div className="mx_RoomView_connectionLostBar_textArea">
|
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||||
<div className="mx_RoomView_connectionLostBar_title">
|
|
||||||
Some of your messages have not been sent.
|
Some of your messages have not been sent.
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomView_connectionLostBar_desc">
|
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||||
<a className="mx_RoomView_resend_link"
|
<a className="mx_RoomStatusBar_resend_link"
|
||||||
onClick={ this.props.onResendAllClick }>
|
onClick={ this.props.onResendAllClick }>
|
||||||
Resend all now
|
Resend all now
|
||||||
</a> or select individual messages to re-send.
|
</a> or select individual messages to re-send.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,8 +228,8 @@ module.exports = React.createClass({
|
||||||
(this.props.numUnreadMessages > 1 ? "s" : "");
|
(this.props.numUnreadMessages > 1 ? "s" : "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView_unreadMessagesBar" onClick={ this.props.onScrollToBottomClick }>
|
<div className="mx_RoomStatusBar_unreadMessagesBar"
|
||||||
<img src="img/newmessages.svg" width="24" height="24" alt=""/>
|
onClick={ this.props.onScrollToBottomClick }>
|
||||||
{unreadMsgs}
|
{unreadMsgs}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -151,30 +238,35 @@ module.exports = React.createClass({
|
||||||
var typingString = WhoIsTyping.whoIsTypingString(this.props.room);
|
var typingString = WhoIsTyping.whoIsTypingString(this.props.room);
|
||||||
if (typingString) {
|
if (typingString) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView_typingBar">
|
<div className="mx_RoomStatusBar_typingBar">
|
||||||
<div className="mx_RoomView_typingImage">...</div>
|
{typingString}
|
||||||
<span className="mx_RoomView_typingText">{typingString}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.props.atEndOfLiveTimeline) {
|
|
||||||
return (
|
|
||||||
<div className="mx_RoomView_scrollToBottomBar" onClick={ this.props.onScrollToBottomClick }>
|
|
||||||
<img src="img/scrolldown.svg" width="24" height="24" alt="Scroll to bottom of page" title="Scroll to bottom of page"/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.hasActiveCall) {
|
if (this.props.hasActiveCall) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView_callBar">
|
<div className="mx_RoomStatusBar_callBar">
|
||||||
<img src="img/sound-indicator.svg" width="23" height="20"/>
|
|
||||||
<b>Active call</b>
|
<b>Active call</b>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div />;
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var content = this._getContent();
|
||||||
|
var indicator = this._getIndicator(content !== null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomStatusBar">
|
||||||
|
<div className="mx_RoomStatusBar_indicator">
|
||||||
|
{indicator}
|
||||||
|
</div>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -420,14 +420,6 @@ module.exports = React.createClass({
|
||||||
window.addEventListener('resize', this.onResize);
|
window.addEventListener('resize', this.onResize);
|
||||||
this.onResize();
|
this.onResize();
|
||||||
|
|
||||||
if (this.refs.roomView) {
|
|
||||||
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
|
||||||
roomView.addEventListener('drop', this.onDrop);
|
|
||||||
roomView.addEventListener('dragover', this.onDragOver);
|
|
||||||
roomView.addEventListener('dragleave', this.onDragLeaveOrEnd);
|
|
||||||
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._updateTabCompleteList();
|
this._updateTabCompleteList();
|
||||||
|
|
||||||
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
||||||
|
@ -453,6 +445,18 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|
||||||
|
componentDidUpdate: function() {
|
||||||
|
if (this.refs.roomView) {
|
||||||
|
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||||
|
if (!roomView.ondrop) {
|
||||||
|
roomView.addEventListener('drop', this.onDrop);
|
||||||
|
roomView.addEventListener('dragover', this.onDragOver);
|
||||||
|
roomView.addEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||||
|
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onSearchResultsFillRequest: function(backwards) {
|
onSearchResultsFillRequest: function(backwards) {
|
||||||
if (!backwards)
|
if (!backwards)
|
||||||
return q(false);
|
return q(false);
|
||||||
|
@ -692,15 +696,6 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onSearchResultSelected: function(result) {
|
|
||||||
var event = result.context.getEvent();
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: event.getRoomId(),
|
|
||||||
event_id: event.getId(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getSearchResultTiles: function() {
|
getSearchResultTiles: function() {
|
||||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
var SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
|
var SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
|
||||||
|
@ -730,12 +725,22 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// once images in the search results load, make the scrollPanel check
|
||||||
|
// the scroll offsets.
|
||||||
|
var onImageLoad = () => {
|
||||||
|
var scrollPanel = this.refs.searchResultsPanel;
|
||||||
|
if (scrollPanel) {
|
||||||
|
scrollPanel.checkScroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var lastRoomId;
|
var lastRoomId;
|
||||||
|
|
||||||
for (var i = this.state.searchResults.results.length - 1; i >= 0; i--) {
|
for (var i = this.state.searchResults.results.length - 1; i >= 0; i--) {
|
||||||
var result = this.state.searchResults.results[i];
|
var result = this.state.searchResults.results[i];
|
||||||
|
|
||||||
var mxEv = result.context.getEvent();
|
var mxEv = result.context.getEvent();
|
||||||
|
var roomId = mxEv.getRoomId();
|
||||||
|
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
// XXX: can this ever happen? It will make the result count
|
// XXX: can this ever happen? It will make the result count
|
||||||
|
@ -744,7 +749,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.searchScope === 'All') {
|
if (this.state.searchScope === 'All') {
|
||||||
var roomId = mxEv.getRoomId();
|
|
||||||
if(roomId != lastRoomId) {
|
if(roomId != lastRoomId) {
|
||||||
var room = cli.getRoom(roomId);
|
var room = cli.getRoom(roomId);
|
||||||
|
|
||||||
|
@ -761,10 +765,13 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resultLink = "#/room/"+roomId+"/"+mxEv.getId();
|
||||||
|
|
||||||
ret.push(<SearchResultTile key={mxEv.getId()}
|
ret.push(<SearchResultTile key={mxEv.getId()}
|
||||||
searchResult={result}
|
searchResult={result}
|
||||||
searchHighlights={this.state.searchHighlights}
|
searchHighlights={this.state.searchHighlights}
|
||||||
onSelect={this._onSearchResultSelected.bind(this, result)}/>);
|
resultLink={resultLink}
|
||||||
|
onImageLoad={onImageLoad}/>);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
@ -843,11 +850,19 @@ module.exports = React.createClass({
|
||||||
self.setState({
|
self.setState({
|
||||||
rejecting: false
|
rejecting: false
|
||||||
});
|
});
|
||||||
}, function(err) {
|
}, function(error) {
|
||||||
console.error("Failed to reject invite: %s", err);
|
console.error("Failed to reject invite: %s", error);
|
||||||
|
|
||||||
|
var msg = error.message ? error.message : JSON.stringify(error);
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Failed to reject invite",
|
||||||
|
description: msg
|
||||||
|
});
|
||||||
|
|
||||||
self.setState({
|
self.setState({
|
||||||
rejecting: false,
|
rejecting: false,
|
||||||
rejectError: err
|
rejectError: error
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -969,10 +984,15 @@ module.exports = React.createClass({
|
||||||
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
||||||
|
|
||||||
if (this.refs.callView) {
|
if (this.refs.callView) {
|
||||||
|
var fullscreenElement =
|
||||||
|
(document.fullscreenElement ||
|
||||||
|
document.mozFullScreenElement ||
|
||||||
|
document.webkitFullscreenElement);
|
||||||
|
if (!fullscreenElement) {
|
||||||
var video = this.refs.callView.getVideoView().getRemoteVideoElement();
|
var video = this.refs.callView.getVideoView().getRemoteVideoElement();
|
||||||
|
|
||||||
video.style.maxHeight = auxPanelMaxHeight + "px";
|
video.style.maxHeight = auxPanelMaxHeight + "px";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// we need to do this for general auxPanels too
|
// we need to do this for general auxPanels too
|
||||||
if (this.refs.auxPanel) {
|
if (this.refs.auxPanel) {
|
||||||
|
@ -1015,10 +1035,16 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onCallViewResize: function() {
|
||||||
|
this.onChildResize();
|
||||||
|
this.onResize();
|
||||||
|
},
|
||||||
|
|
||||||
onChildResize: function() {
|
onChildResize: function() {
|
||||||
// When the video or the message composer resizes, the scroll panel
|
// When the video, status bar, or the message composer resizes, the
|
||||||
// also changes size. Work around GeminiScrollBar fail by telling it
|
// scroll panel also changes size. Work around GeminiScrollBar fail by
|
||||||
// about it. This also ensures that the scroll offset is updated.
|
// telling it about it. This also ensures that the scroll offset is
|
||||||
|
// updated.
|
||||||
if (this.refs.messagePanel) {
|
if (this.refs.messagePanel) {
|
||||||
this.refs.messagePanel.forceUpdate();
|
this.refs.messagePanel.forceUpdate();
|
||||||
}
|
}
|
||||||
|
@ -1055,7 +1081,6 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var joinErrorText = this.state.joinError ? "Failed to join room!" : "";
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView">
|
<div className="mx_RoomView">
|
||||||
<RoomHeader ref="header" room={this.state.room} simpleHeader="Join room"/>
|
<RoomHeader ref="header" room={this.state.room} simpleHeader="Join room"/>
|
||||||
|
@ -1064,7 +1089,6 @@ module.exports = React.createClass({
|
||||||
canJoin={ true } canPreview={ false }
|
canJoin={ true } canPreview={ false }
|
||||||
spinner={this.state.joining}
|
spinner={this.state.joining}
|
||||||
/>
|
/>
|
||||||
<div className="error">{joinErrorText}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomView_messagePanel"></div>
|
<div className="mx_RoomView_messagePanel"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1090,10 +1114,6 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
var inviteEvent = myMember.events.member;
|
var inviteEvent = myMember.events.member;
|
||||||
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
|
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
|
||||||
// XXX: Leaving this intentionally basic for now because invites are about to change totally
|
|
||||||
// FIXME: This comment is now outdated - what do we need to fix? ^
|
|
||||||
var joinErrorText = this.state.joinError ? "Failed to join room!" : "";
|
|
||||||
var rejectErrorText = this.state.rejectError ? "Failed to reject invite!" : "";
|
|
||||||
|
|
||||||
// We deliberately don't try to peek into invites, even if we have permission to peek
|
// We deliberately don't try to peek into invites, even if we have permission to peek
|
||||||
// as they could be a spam vector.
|
// as they could be a spam vector.
|
||||||
|
@ -1109,8 +1129,6 @@ module.exports = React.createClass({
|
||||||
canJoin={ true } canPreview={ false }
|
canJoin={ true } canPreview={ false }
|
||||||
spinner={this.state.joining}
|
spinner={this.state.joining}
|
||||||
/>
|
/>
|
||||||
<div className="error">{joinErrorText}</div>
|
|
||||||
<div className="error">{rejectErrorText}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomView_messagePanel"></div>
|
<div className="mx_RoomView_messagePanel"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1157,6 +1175,7 @@ module.exports = React.createClass({
|
||||||
hasActiveCall={inCall}
|
hasActiveCall={inCall}
|
||||||
onResendAllClick={this.onResendAllClick}
|
onResendAllClick={this.onResendAllClick}
|
||||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||||
|
onResize={this.onChildResize}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1295,9 +1314,6 @@ module.exports = React.createClass({
|
||||||
highlightedEventId={this.props.highlightedEventId}
|
highlightedEventId={this.props.highlightedEventId}
|
||||||
eventId={this.props.eventId}
|
eventId={this.props.eventId}
|
||||||
eventPixelOffset={this.props.eventPixelOffset}
|
eventPixelOffset={this.props.eventPixelOffset}
|
||||||
isConferenceUser={this.props.ConferenceHandler ?
|
|
||||||
this.props.ConferenceHandler.isConferenceUser :
|
|
||||||
null }
|
|
||||||
onScroll={ this.onMessageListScroll }
|
onScroll={ this.onMessageListScroll }
|
||||||
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
|
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
|
||||||
/>);
|
/>);
|
||||||
|
@ -1332,7 +1348,7 @@ module.exports = React.createClass({
|
||||||
<div className="mx_RoomView_auxPanel" ref="auxPanel">
|
<div className="mx_RoomView_auxPanel" ref="auxPanel">
|
||||||
{ fileDropTarget }
|
{ fileDropTarget }
|
||||||
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}
|
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}
|
||||||
onResize={this.onChildResize} />
|
onResize={this.onCallViewResize} />
|
||||||
{ conferenceCallNotification }
|
{ conferenceCallNotification }
|
||||||
{ aux }
|
{ aux }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -124,10 +124,9 @@ module.exports = React.createClass({
|
||||||
// after adding event tiles, we may need to tweak the scroll (either to
|
// 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
|
// keep at the bottom of the timeline, or to maintain the view after
|
||||||
// adding events to the top).
|
// adding events to the top).
|
||||||
this._restoreSavedScrollState();
|
//
|
||||||
|
// This will also re-check the fill state, in case the paginate was inadequate
|
||||||
// we also re-check the fill state, in case the paginate was inadequate
|
this.checkScroll();
|
||||||
this.checkFillState();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -178,6 +177,13 @@ module.exports = React.createClass({
|
||||||
this.checkFillState();
|
this.checkFillState();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// after an update to the contents of the panel, check that the scroll is
|
||||||
|
// where it ought to be, and set off pagination requests if necessary.
|
||||||
|
checkScroll: function() {
|
||||||
|
this._restoreSavedScrollState();
|
||||||
|
this.checkFillState();
|
||||||
|
},
|
||||||
|
|
||||||
// return true if the content is fully scrolled down right now; else false.
|
// return true if the content is fully scrolled down right now; else false.
|
||||||
//
|
//
|
||||||
// note that this is independent of the 'stuckAtBottom' state - it is simply
|
// note that this is independent of the 'stuckAtBottom' state - it is simply
|
||||||
|
|
|
@ -72,11 +72,6 @@ var TimelinePanel = React.createClass({
|
||||||
// 1/3 of the way down the viewport.
|
// 1/3 of the way down the viewport.
|
||||||
eventPixelOffset: React.PropTypes.number,
|
eventPixelOffset: React.PropTypes.number,
|
||||||
|
|
||||||
// callback to determine if a user is the magic freeswitch conference
|
|
||||||
// user. Takes one parameter, which is a user id. Should return true if
|
|
||||||
// the user is the conference user.
|
|
||||||
isConferenceUser: React.PropTypes.func,
|
|
||||||
|
|
||||||
// callback which is called when the panel is scrolled.
|
// callback which is called when the panel is scrolled.
|
||||||
onScroll: React.PropTypes.func,
|
onScroll: React.PropTypes.func,
|
||||||
|
|
||||||
|
@ -118,6 +113,7 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
|
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
||||||
|
|
||||||
this._initTimeline(this.props);
|
this._initTimeline(this.props);
|
||||||
},
|
},
|
||||||
|
@ -146,6 +142,7 @@ var TimelinePanel = React.createClass({
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
if (client) {
|
if (client) {
|
||||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
|
client.removeListener("Room.redaction", this.onRoomRedaction);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -238,10 +235,21 @@ var TimelinePanel = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRoomRedaction: function(ev, room) {
|
||||||
|
if (this.unmounted) return;
|
||||||
|
|
||||||
|
// ignore events for other rooms
|
||||||
|
if (room !== this.props.room) return;
|
||||||
|
|
||||||
|
// we could skip an update if the event isn't in our timeline,
|
||||||
|
// but that's probably an early optimisation.
|
||||||
|
this.forceUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
sendReadReceipt: function() {
|
sendReadReceipt: function() {
|
||||||
if (!this.refs.messagePanel) return;
|
if (!this.refs.messagePanel) return;
|
||||||
|
|
||||||
var currentReadUpToEventId = this._getCurrentReadReceipt();
|
var currentReadUpToEventId = this._getCurrentReadReceipt(true);
|
||||||
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
||||||
|
|
||||||
// We want to avoid sending out read receipts when we are looking at
|
// We want to avoid sending out read receipts when we are looking at
|
||||||
|
@ -531,15 +539,20 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the id of the event corresponding to our user's latest read-receipt.
|
* get the id of the event corresponding to our user's latest read-receipt.
|
||||||
|
*
|
||||||
|
* @param {Boolean} ignoreSynthesized If true, return only receipts that
|
||||||
|
* have been sent by the server, not
|
||||||
|
* implicit ones generated by the JS
|
||||||
|
* SDK.
|
||||||
*/
|
*/
|
||||||
_getCurrentReadReceipt: function() {
|
_getCurrentReadReceipt: function(ignoreSynthesized) {
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
// the client can be null on logout
|
// the client can be null on logout
|
||||||
if (client == null)
|
if (client == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var myUserId = client.credentials.userId;
|
var myUserId = client.credentials.userId;
|
||||||
return this.props.room.getEventReadUpTo(myUserId);
|
return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
||||||
},
|
},
|
||||||
|
|
||||||
_setReadMarker: function(eventId, eventTs) {
|
_setReadMarker: function(eventId, eventTs) {
|
||||||
|
@ -601,7 +614,6 @@ var TimelinePanel = React.createClass({
|
||||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||||
stickyBottom={ stickyBottom }
|
stickyBottom={ stickyBottom }
|
||||||
isConferenceUser={ this.props.isConferenceUser }
|
|
||||||
onScroll={ this.onMessageListScroll }
|
onScroll={ this.onMessageListScroll }
|
||||||
onFillRequest={ this.onMessageListFillRequest }
|
onFillRequest={ this.onMessageListFillRequest }
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -35,7 +35,8 @@ module.exports = React.createClass({displayName: 'Login',
|
||||||
// login shouldn't know or care how registration is done.
|
// login shouldn't know or care how registration is done.
|
||||||
onRegisterClick: React.PropTypes.func.isRequired,
|
onRegisterClick: React.PropTypes.func.isRequired,
|
||||||
// login shouldn't care how password recovery is done.
|
// login shouldn't care how password recovery is done.
|
||||||
onForgotPasswordClick: React.PropTypes.func
|
onForgotPasswordClick: React.PropTypes.func,
|
||||||
|
onLoginAsGuestClick: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -128,11 +129,30 @@ module.exports = React.createClass({displayName: 'Login',
|
||||||
if (!errCode && err.httpStatus) {
|
if (!errCode && err.httpStatus) {
|
||||||
errCode = "HTTP " + err.httpStatus;
|
errCode = "HTTP " + err.httpStatus;
|
||||||
}
|
}
|
||||||
this.setState({
|
|
||||||
errorText: (
|
var errorText = "Error: Problem communicating with the given homeserver " +
|
||||||
"Error: Problem communicating with the given homeserver " +
|
|
||||||
(errCode ? "(" + errCode + ")" : "")
|
(errCode ? "(" + errCode + ")" : "")
|
||||||
)
|
|
||||||
|
if (err.cors === 'rejected') {
|
||||||
|
if (window.location.protocol === 'https:' &&
|
||||||
|
(this.state.enteredHomeserverUrl.startsWith("http:") ||
|
||||||
|
!this.state.enteredHomeserverUrl.startsWith("http")))
|
||||||
|
{
|
||||||
|
errorText = <span>
|
||||||
|
Can't connect to homeserver via HTTP when using a vector served by HTTPS.
|
||||||
|
Either use HTTPS or <a href='https://www.google.com/search?&q=enable%20unsafe%20scripts'>enable unsafe scripts</a>
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
errorText = <span>
|
||||||
|
Can't connect to homeserver - please check your connectivity and ensure
|
||||||
|
your <a href={ this.state.enteredHomeserverUrl }>homeserver's SSL certificate</a> is trusted.
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
errorText: errorText
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -167,6 +187,13 @@ module.exports = React.createClass({displayName: 'Login',
|
||||||
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
var LoginFooter = sdk.getComponent("login.LoginFooter");
|
||||||
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
var loader = this.state.busy ? <div className="mx_Login_loader"><Loader /></div> : null;
|
||||||
|
|
||||||
|
var loginAsGuestJsx;
|
||||||
|
if (this.props.onLoginAsGuestClick) {
|
||||||
|
loginAsGuestJsx =
|
||||||
|
<a className="mx_Login_create" onClick={this.props.onLoginAsGuestClick} href="#">
|
||||||
|
Login as guest
|
||||||
|
</a>
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_Login">
|
<div className="mx_Login">
|
||||||
<div className="mx_Login_box">
|
<div className="mx_Login_box">
|
||||||
|
@ -188,6 +215,7 @@ module.exports = React.createClass({displayName: 'Login',
|
||||||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||||
Create a new account
|
Create a new account
|
||||||
</a>
|
</a>
|
||||||
|
{ loginAsGuestJsx }
|
||||||
<br/>
|
<br/>
|
||||||
<LoginFooter />
|
<LoginFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -115,6 +115,9 @@ module.exports = React.createClass({
|
||||||
onProcessingRegistration: function(promise) {
|
onProcessingRegistration: function(promise) {
|
||||||
var self = this;
|
var self = this;
|
||||||
promise.done(function(response) {
|
promise.done(function(response) {
|
||||||
|
self.setState({
|
||||||
|
busy: false
|
||||||
|
});
|
||||||
if (!response || !response.access_token) {
|
if (!response || !response.access_token) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"FIXME: Register fulfilled without a final response, " +
|
"FIXME: Register fulfilled without a final response, " +
|
||||||
|
@ -126,7 +129,7 @@ module.exports = React.createClass({
|
||||||
if (!response || !response.user_id || !response.access_token) {
|
if (!response || !response.user_id || !response.access_token) {
|
||||||
console.error("Final response is missing keys.");
|
console.error("Final response is missing keys.");
|
||||||
self.setState({
|
self.setState({
|
||||||
errorText: "There was a problem processing the response."
|
errorText: "Registration failed on server"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -136,9 +139,6 @@ module.exports = React.createClass({
|
||||||
identityServerUrl: self.registerLogic.getIdentityServerUrl(),
|
identityServerUrl: self.registerLogic.getIdentityServerUrl(),
|
||||||
accessToken: response.access_token
|
accessToken: response.access_token
|
||||||
});
|
});
|
||||||
self.setState({
|
|
||||||
busy: false
|
|
||||||
});
|
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err.message) {
|
if (err.message) {
|
||||||
self.setState({
|
self.setState({
|
||||||
|
|
|
@ -31,14 +31,22 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onKeyDown: function(e) {
|
||||||
|
if (e.keyCode === 27) { // escape
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
this.cancelPrompt();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
Sign out?
|
Sign out?
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
|
||||||
<button onClick={this.logOut}>Sign Out</button>
|
<button autoFocus onClick={this.logOut}>Sign Out</button>
|
||||||
<button onClick={this.cancelPrompt}>Cancel</button>
|
<button onClick={this.cancelPrompt}>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,9 +26,20 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
if (this.props.currentDisplayName) {
|
||||||
value: this.props.currentDisplayName || "Guest "+MatrixClientPeg.get().getUserIdLocalpart(),
|
return { value: this.props.currentDisplayName };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
|
return { value : "Guest " + MatrixClientPeg.get().getUserIdLocalpart() };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return { value : MatrixClientPeg.get().getUserIdLocalpart() };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.refs.input_value.select();
|
||||||
},
|
},
|
||||||
|
|
||||||
getValue: function() {
|
getValue: function() {
|
||||||
|
@ -54,11 +65,12 @@ module.exports = React.createClass({
|
||||||
Set a Display Name
|
Set a Display Name
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
Your display name is how you'll appear to others when you speak in rooms. What would you like it to be?
|
Your display name is how you'll appear to others when you speak in rooms.<br/>
|
||||||
|
What would you like it to be?
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={this.onFormSubmit}>
|
<form onSubmit={this.onFormSubmit}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<input type="text" value={this.state.value}
|
<input type="text" ref="input_value" value={this.state.value}
|
||||||
autoFocus={true} onChange={this.onValueChange} size="30"
|
autoFocus={true} onChange={this.onValueChange} size="30"
|
||||||
className="mx_SetDisplayNameDialog_input"
|
className="mx_SetDisplayNameDialog_input"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -27,6 +27,14 @@ var dis = require("../../../dispatcher");
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MImageBody',
|
displayName: 'MImageBody',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
/* the MatrixEvent to show */
|
||||||
|
mxEvent: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/* callback called when images in events are loaded */
|
||||||
|
onImageLoad: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
thumbHeight: function(fullWidth, fullHeight, thumbWidth, thumbHeight) {
|
thumbHeight: function(fullWidth, fullHeight, thumbWidth, thumbHeight) {
|
||||||
if (!fullWidth || !fullHeight) {
|
if (!fullWidth || !fullHeight) {
|
||||||
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
||||||
|
@ -94,7 +102,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
_getThumbUrl: function() {
|
_getThumbUrl: function() {
|
||||||
var content = this.props.mxEvent.getContent();
|
var content = this.props.mxEvent.getContent();
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 480, 360);
|
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
@ -103,10 +111,10 @@ module.exports = React.createClass({
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
var thumbHeight = null;
|
var thumbHeight = null;
|
||||||
if (content.info) thumbHeight = this.thumbHeight(content.info.w, content.info.h, 480, 360);
|
if (content.info) thumbHeight = this.thumbHeight(content.info.w, content.info.h, 800, 600);
|
||||||
|
|
||||||
var imgStyle = {};
|
var imgStyle = {};
|
||||||
if (thumbHeight) imgStyle['height'] = thumbHeight;
|
if (thumbHeight) imgStyle['maxHeight'] = thumbHeight;
|
||||||
|
|
||||||
var thumbUrl = this._getThumbUrl();
|
var thumbUrl = this._getThumbUrl();
|
||||||
if (thumbUrl) {
|
if (thumbUrl) {
|
||||||
|
@ -116,7 +124,8 @@ module.exports = React.createClass({
|
||||||
<img className="mx_MImageBody_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}
|
||||||
|
onLoad={this.props.onImageLoad} />
|
||||||
</a>
|
</a>
|
||||||
<div className="mx_MImageBody_download">
|
<div className="mx_MImageBody_download">
|
||||||
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
||||||
|
|
|
@ -28,6 +28,21 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
/* the MatrixEvent to show */
|
||||||
|
mxEvent: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/* a list of words to highlight */
|
||||||
|
highlights: React.PropTypes.array,
|
||||||
|
|
||||||
|
/* link URL for the highlights */
|
||||||
|
highlightLink: React.PropTypes.string,
|
||||||
|
|
||||||
|
/* callback called when images in events are loaded */
|
||||||
|
onImageLoad: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var UnknownMessageTile = sdk.getComponent('messages.UnknownBody');
|
var UnknownMessageTile = sdk.getComponent('messages.UnknownBody');
|
||||||
|
|
||||||
|
@ -48,6 +63,7 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TileType mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
return <TileType mxEvent={this.props.mxEvent} highlights={this.props.highlights}
|
||||||
onHighlightClick={this.props.onHighlightClick} />;
|
highlightLink={this.props.highlightLink}
|
||||||
|
onImageLoad={this.props.onImageLoad} />;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,17 @@ linkifyMatrix(linkify);
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'TextualBody',
|
displayName: 'TextualBody',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
/* the MatrixEvent to show */
|
||||||
|
mxEvent: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
/* a list of words to highlight */
|
||||||
|
highlights: React.PropTypes.array,
|
||||||
|
|
||||||
|
/* link URL for the highlights */
|
||||||
|
highlightLink: React.PropTypes.string,
|
||||||
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
linkifyElement(this.refs.content, linkifyMatrix.options);
|
linkifyElement(this.refs.content, linkifyMatrix.options);
|
||||||
|
|
||||||
|
@ -46,14 +57,15 @@ module.exports = React.createClass({
|
||||||
shouldComponentUpdate: function(nextProps) {
|
shouldComponentUpdate: function(nextProps) {
|
||||||
// exploit that events are immutable :)
|
// exploit that events are immutable :)
|
||||||
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
|
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
|
||||||
nextProps.highlights !== this.props.highlights);
|
nextProps.highlights !== this.props.highlights ||
|
||||||
|
nextProps.highlightLink !== this.props.highlightLink);
|
||||||
},
|
},
|
||||||
|
|
||||||
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});
|
{highlightLink: this.props.highlightLink});
|
||||||
|
|
||||||
switch (content.msgtype) {
|
switch (content.msgtype) {
|
||||||
case "m.emote":
|
case "m.emote":
|
||||||
|
|
|
@ -65,6 +65,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
statics: {
|
statics: {
|
||||||
haveTileForEvent: function(e) {
|
haveTileForEvent: function(e) {
|
||||||
|
if (e.isRedacted()) return false;
|
||||||
if (eventTileTypes[e.getType()] == undefined) return false;
|
if (eventTileTypes[e.getType()] == undefined) return false;
|
||||||
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
|
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
|
||||||
return TextForEvent.textForEvent(e) !== '';
|
return TextForEvent.textForEvent(e) !== '';
|
||||||
|
@ -96,11 +97,14 @@ module.exports = React.createClass({
|
||||||
/* a list of words to highlight */
|
/* a list of words to highlight */
|
||||||
highlights: React.PropTypes.array,
|
highlights: React.PropTypes.array,
|
||||||
|
|
||||||
/* a function to be called when the highlight is clicked */
|
/* link URL for the highlights */
|
||||||
onHighlightClick: React.PropTypes.func,
|
highlightLink: React.PropTypes.string,
|
||||||
|
|
||||||
/* is this the focussed event */
|
/* is this the focussed event */
|
||||||
isSelectedEvent: React.PropTypes.bool,
|
isSelectedEvent: React.PropTypes.bool,
|
||||||
|
|
||||||
|
/* callback called when images in events are loaded */
|
||||||
|
onImageLoad: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -110,6 +114,14 @@ module.exports = React.createClass({
|
||||||
shouldHighlight: function() {
|
shouldHighlight: function() {
|
||||||
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
|
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
|
||||||
if (!actions || !actions.tweaks) { return false; }
|
if (!actions || !actions.tweaks) { return false; }
|
||||||
|
|
||||||
|
// don't show self-highlights from another of our clients
|
||||||
|
if (this.props.mxEvent.sender &&
|
||||||
|
this.props.mxEvent.sender.userId === MatrixClientPeg.get().credentials.userId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return actions.tweaks.highlight;
|
return actions.tweaks.highlight;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -314,7 +326,8 @@ module.exports = React.createClass({
|
||||||
{ 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} />
|
highlightLink={this.props.highlightLink}
|
||||||
|
onImageLoad={this.props.onImageLoad} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -327,7 +327,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var memberList = self.state.members.filter(function(userId) {
|
var memberList = self.state.members.filter(function(userId) {
|
||||||
var m = self.memberDict[userId];
|
var m = self.memberDict[userId];
|
||||||
if (query && m.name.toLowerCase().indexOf(query) !== 0) {
|
if (query && m.name.toLowerCase().indexOf(query) === -1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return m.membership == membership;
|
return m.membership == membership;
|
||||||
|
|
|
@ -291,6 +291,13 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// slightly ugly hack to offset if there's a toolbar present.
|
||||||
|
// we really should be calculating our absolute offsets of top by recursing through the DOM
|
||||||
|
toolbar = document.getElementsByClassName("mx_MatrixToolbar")[0];
|
||||||
|
if (toolbar) {
|
||||||
|
top += toolbar.offsetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
incomingCallBox.style.top = top + "px";
|
incomingCallBox.style.top = top + "px";
|
||||||
incomingCallBox.style.left = scroll.offsetLeft + scroll.offsetWidth + "px";
|
incomingCallBox.style.left = scroll.offsetLeft + scroll.offsetWidth + "px";
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,8 +29,10 @@ module.exports = React.createClass({
|
||||||
// a list of strings to be highlighted in the results
|
// a list of strings to be highlighted in the results
|
||||||
searchHighlights: React.PropTypes.array,
|
searchHighlights: React.PropTypes.array,
|
||||||
|
|
||||||
// callback to be called when the user selects this result
|
// href for the highlights in this result
|
||||||
onSelect: React.PropTypes.func,
|
resultLink: React.PropTypes.string,
|
||||||
|
|
||||||
|
onImageLoad: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
@ -53,7 +55,8 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
if (EventTile.haveTileForEvent(ev)) {
|
if (EventTile.haveTileForEvent(ev)) {
|
||||||
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
|
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
|
||||||
onHighlightClick={this.props.onSelect}/>)
|
highlightLink={this.props.resultLink}
|
||||||
|
onImageLoad={this.props.onImageLoad} />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -110,19 +110,17 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
|
||||||
var avatarImg;
|
var avatarImg;
|
||||||
// Having just set an avatar we just display that since it will take a little
|
// Having just set an avatar we just display that since it will take a little
|
||||||
// time to propagate through to the RoomAvatar.
|
// time to propagate through to the RoomAvatar.
|
||||||
if (this.props.room && !this.avatarSet) {
|
if (this.props.room && !this.avatarSet) {
|
||||||
|
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||||
avatarImg = <RoomAvatar room={this.props.room} width={ this.props.width } height={ this.props.height } resizeMethod='crop' />;
|
avatarImg = <RoomAvatar room={this.props.room} width={ this.props.width } height={ this.props.height } resizeMethod='crop' />;
|
||||||
} else {
|
} else {
|
||||||
var style = {
|
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
width: this.props.width,
|
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
|
||||||
height: this.props.height,
|
avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
|
||||||
objectFit: 'cover',
|
name='?' idName={ MatrixClientPeg.get().getUserIdLocalpart() } url={this.state.avatarUrl} />
|
||||||
};
|
|
||||||
avatarImg = <img className="mx_BaseAvatar_image" src={this.state.avatarUrl} style={style} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var uploadSection;
|
var uploadSection;
|
||||||
|
|
|
@ -85,9 +85,9 @@ module.exports = React.createClass({
|
||||||
// if this call is a conf call, don't display local video as the
|
// if this call is a conf call, don't display local video as the
|
||||||
// conference will have us in it
|
// conference will have us in it
|
||||||
this.getVideoView().getLocalVideoElement().style.display = (
|
this.getVideoView().getLocalVideoElement().style.display = (
|
||||||
call.confUserId ? "none" : "initial"
|
call.confUserId ? "none" : "block"
|
||||||
);
|
);
|
||||||
this.getVideoView().getRemoteVideoElement().style.display = "initial";
|
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.getVideoView().getLocalVideoElement().style.display = "none";
|
this.getVideoView().getLocalVideoElement().style.display = "none";
|
||||||
|
|
|
@ -64,6 +64,7 @@ module.exports = React.createClass({
|
||||||
element.msRequestFullscreen
|
element.msRequestFullscreen
|
||||||
);
|
);
|
||||||
requestMethod.call(element);
|
requestMethod.call(element);
|
||||||
|
this.getRemoteVideoElement().style.maxHeight = "inherit";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var exitMethod = (
|
var exitMethod = (
|
||||||
|
|
|
@ -114,6 +114,17 @@ matrixLinkify.options = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatHref: function (href, type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'roomalias':
|
||||||
|
return '#/room/' + href;
|
||||||
|
case 'userid':
|
||||||
|
return '#';
|
||||||
|
default:
|
||||||
|
return href;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue