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:
Richard van der Hoff 2016-02-23 18:30:42 +00:00
commit 029f47d91c
25 changed files with 520 additions and 245 deletions

View file

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

View file

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

View file

@ -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) {

View file

@ -64,6 +64,7 @@ var cssAttrs = [
"borderColor", "borderColor",
"borderTopColor", "borderTopColor",
"borderBottomColor", "borderBottomColor",
"borderLeftColor",
]; ];
var svgAttrs = [ var svgAttrs = [

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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({

View file

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

View file

@ -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"
/> />

View file

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

View file

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

View file

@ -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":

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = (

View file

@ -114,6 +114,17 @@ matrixLinkify.options = {
} }
}; };
} }
},
formatHref: function (href, type) {
switch (type) {
case 'roomalias':
return '#/room/' + href;
case 'userid':
return '#';
default:
return href;
}
} }
}; };