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);
dis.dispatch({action: 'upload_started'});
var error;
var self = this;
return def.promise.then(function() {
upload.promise = matrixClient.uploadContent(file);
@ -103,11 +104,10 @@ class ContentMessages {
dis.dispatch({action: 'upload_progress', upload: upload});
}
}).then(function(url) {
dis.dispatch({action: 'upload_finished', upload: upload});
content.url = url;
return matrixClient.sendMessage(roomId, content);
}, function(err) {
dis.dispatch({action: 'upload_failed', upload: upload});
error = err;
if (!upload.canceled) {
var desc = "The file '"+upload.fileName+"' failed to upload.";
if (err.http_status == 413) {
@ -128,6 +128,12 @@ class ContentMessages {
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';
var React = require('react');
var ReactDOMServer = require('react-dom/server')
var sanitizeHtml = require('sanitize-html');
var highlight = require('highlight.js');
@ -50,14 +49,23 @@ var sanitizeHtmlParams = {
},
};
class Highlighter {
constructor(html, highlightClass, onHighlightClick) {
this.html = html;
class BaseHighlighter {
constructor(highlightClass, highlightLink) {
this.highlightClass = highlightClass;
this.onHighlightClick = onHighlightClick;
this._key = 0;
this.highlightLink = highlightLink;
}
/**
* 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) {
var lastOffset = 0;
var offset;
@ -71,10 +79,12 @@ class Highlighter {
nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
}
// do highlight
nodes.push(this._createSpan(safeHighlight, true));
// do highlight. use the original string rather than safeHighlight
// 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
@ -92,31 +102,62 @@ class Highlighter {
}
else {
// 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
*
* spanBody: content of the span. If html, must have been sanitised
* snippet: content of the span
* highlight: true to highlight as a search match
*
* returns a React node
*/
_createSpan(spanBody, highlight) {
var spanProps = {
key: this._key++,
};
_processSnippet(snippet, highlight) {
var key = this._key++;
if (highlight) {
spanProps.onClick = this.onHighlightClick;
spanProps.className = this.highlightClass;
var node =
<span key={key} className={highlight ? this.highlightClass : null }>
{ snippet }
</span>;
if (highlight && this.highlightLink) {
node = <a key={key} href={this.highlightLink}>{node}</a>
}
if (this.html) {
return (<span {...spanProps} dangerouslySetInnerHTML={{ __html: spanBody }} />);
}
else {
return (<span {...spanProps}>{ spanBody }</span>);
}
return node;
}
}
@ -128,8 +169,7 @@ module.exports = {
*
* highlights: optional list of words to highlight, ordered by longest word first
*
* opts.onHighlightClick: optional callback function to be called when a
* highlighted word is clicked
* opts.highlightLink: optional href to add to highlights
*/
bodyToHtml: function(content, highlights, 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
try {
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) {
return sanitizeHtml(highlight, sanitizeHtmlParams);
});
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
sanitizeHtmlParams.textFilter = function(safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).map(function(span) {
// 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('');
return highlighter.applyHighlights(safeText, safeHighlights).join('');
};
}
safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
@ -167,7 +202,7 @@ module.exports = {
} else {
safeBody = content.body;
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);
}
else {

View file

@ -182,6 +182,9 @@ var Notifier = {
if (state === "PREPARED" || state === "SYNCING") {
this.isPrepared = true;
}
else if (state === "STOPPED" || state === "ERROR") {
this.isPrepared = false;
}
},
onRoomTimeline: function(ev, room, toStartOfTimeline) {

View file

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

View file

@ -175,7 +175,7 @@ module.exports = React.createClass({
guest: true
});
}, function(err) {
console.error(err.data);
console.error("Failed to register as guest: " + err + " " + err.data);
self._setAutoRegisterAsGuest(false);
});
},
@ -970,7 +970,9 @@ module.exports = React.createClass({
onRegisterClick={this.onRegisterClick}
homeserverUrl={this.props.config.default_hs_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.
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.
onScroll: React.PropTypes.func,
@ -163,54 +158,20 @@ module.exports = React.createClass({
this.eventNodes = {};
// we do two passes over the events list; first of all, we figure out
// 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 = [];
var i;
// the index in 'eventsToShow' of the event *before* which we put the
// read marker or its ghost. (Note that it may be equal to
// eventsToShow.length, which means it would be at the end of the timeline)
var ghostIndex, readMarkerIndex;
for (var i = 0; i < this.props.events.length; i++) {
// first figure out which is the last event in the list which we're
// actually going to show; this allows us to behave slightly
// differently for the last event in the list.
for (i = this.props.events.length-1; i >= 0; i--) {
var mxEv = this.props.events[i];
var wantTile = true;
if (!EventTile.haveTileForEvent(mxEv)) {
wantTile = false;
continue;
}
if (this.props.isConferenceUser && mxEv.getType() === "m.room.member") {
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;
}
break;
}
var lastShownEventIndex = i;
var ret = [];
@ -219,42 +180,54 @@ module.exports = React.createClass({
// assume there is no read marker until proven otherwise
var readMarkerVisible = false;
for (var i = 0; i < eventsToShow.length; i++) {
var mxEv = eventsToShow[i];
for (i = 0; i < this.props.events.length; i++) {
var mxEv = this.props.events[i];
var wantTile = true;
var eventId = mxEv.getId();
// insert the read marker if appropriate.
if (i == readMarkerIndex) {
if (!EventTile.haveTileForEvent(mxEv)) {
wantTile = false;
}
var last = (i == lastShownEventIndex);
if (wantTile) {
ret.push(this._getTilesForEvent(prevEvent, mxEv, last));
} 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;
// 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) {
// 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
// algorithms which depend on its position on the screen aren't confused.
if (i >= lastShownEventIndex) {
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 (i == ghostIndex) {
} 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());
}
var last = false;
if (i == eventsToShow.length - 1) {
last = true;
}
// add the tiles for this event
ret.push(this._getTilesForEvent(prevEvent, mxEv, last));
prevEvent = mxEv;
}
// 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
// algorithms which depend on its position on the screen aren't confused.
if (i == readMarkerIndex) {
ret.push(this._getReadMarkerTile(false));
}
this.currentReadMarkerEventId = readMarkerVisible ? this.props.readMarkerEventId : null;
@ -298,7 +271,8 @@ module.exports = React.createClass({
ref={this._collectEventNode.bind(this, eventId)}
data-scroll-token={scrollToken}>
<EventTile mxEvent={mxEv} continuation={continuation}
last={last} isSelectedEvent={highlight}/>
last={last} isSelectedEvent={highlight}
onImageLoad={this._onImageLoad} />
</li>
);
@ -353,6 +327,16 @@ module.exports = React.createClass({
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() {
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
return (

View file

@ -51,6 +51,11 @@ module.exports = React.createClass({
// callback for when the user clicks on the 'scroll to bottom' button
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() {
@ -63,8 +68,17 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
},
componentDidUpdate: function(prevProps, prevState) {
if(this.props.onResize && this._checkForResize(prevProps, prevState)) {
this.props.onResize();
}
},
componentWillUnmount: function() {
MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange);
// 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);
}
},
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 TintableSvg = sdk.getComponent("elements.TintableSvg");
@ -86,15 +178,13 @@ module.exports = React.createClass({
// a connection!
if (this.state.syncState === "ERROR") {
return (
<div className="mx_RoomView_connectionLostBar">
<div className="mx_RoomStatusBar_connectionLostBar">
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
<div className="mx_RoomView_connectionLostBar_textArea">
<div className="mx_RoomView_connectionLostBar_title">
Connectivity to the server has been lost.
</div>
<div className="mx_RoomView_connectionLostBar_desc">
Sent messages will be stored until your connection has returned.
</div>
<div className="mx_RoomStatusBar_connectionLostBar_title">
Connectivity to the server has been lost.
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
Sent messages will be stored until your connection has returned.
</div>
</div>
);
@ -102,11 +192,10 @@ module.exports = React.createClass({
if (this.props.tabCompleteEntries) {
return (
<div className="mx_RoomView_tabCompleteBar">
<div className="mx_RoomView_tabCompleteImage">...</div>
<div className="mx_RoomView_tabCompleteWrapper">
<div className="mx_RoomStatusBar_tabCompleteBar">
<div className="mx_RoomStatusBar_tabCompleteWrapper">
<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"/>
Auto-complete
</div>
@ -117,18 +206,16 @@ module.exports = React.createClass({
if (this.props.hasUnsentMessages) {
return (
<div className="mx_RoomView_connectionLostBar">
<div className="mx_RoomStatusBar_connectionLostBar">
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
<div className="mx_RoomView_connectionLostBar_textArea">
<div className="mx_RoomView_connectionLostBar_title">
Some of your messages have not been sent.
</div>
<div className="mx_RoomView_connectionLostBar_desc">
<a className="mx_RoomView_resend_link"
onClick={ this.props.onResendAllClick }>
Resend all now
</a> or select individual messages to re-send.
</div>
<div className="mx_RoomStatusBar_connectionLostBar_title">
Some of your messages have not been sent.
</div>
<div className="mx_RoomStatusBar_connectionLostBar_desc">
<a className="mx_RoomStatusBar_resend_link"
onClick={ this.props.onResendAllClick }>
Resend all now
</a> or select individual messages to re-send.
</div>
</div>
);
@ -141,8 +228,8 @@ module.exports = React.createClass({
(this.props.numUnreadMessages > 1 ? "s" : "");
return (
<div className="mx_RoomView_unreadMessagesBar" onClick={ this.props.onScrollToBottomClick }>
<img src="img/newmessages.svg" width="24" height="24" alt=""/>
<div className="mx_RoomStatusBar_unreadMessagesBar"
onClick={ this.props.onScrollToBottomClick }>
{unreadMsgs}
</div>
);
@ -151,30 +238,35 @@ module.exports = React.createClass({
var typingString = WhoIsTyping.whoIsTypingString(this.props.room);
if (typingString) {
return (
<div className="mx_RoomView_typingBar">
<div className="mx_RoomView_typingImage">...</div>
<span className="mx_RoomView_typingText">{typingString}</span>
<div className="mx_RoomStatusBar_typingBar">
{typingString}
</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>
);
}
if (this.props.hasActiveCall) {
return (
<div className="mx_RoomView_callBar">
<img src="img/sound-indicator.svg" width="23" height="20"/>
<div className="mx_RoomStatusBar_callBar">
<b>Active call</b>
</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);
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();
// XXX: EVIL HACK to autofocus inviting on empty rooms.
@ -453,6 +445,18 @@ module.exports = React.createClass({
);
}, 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) {
if (!backwards)
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() {
var EventTile = sdk.getComponent('rooms.EventTile');
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;
for (var i = this.state.searchResults.results.length - 1; i >= 0; i--) {
var result = this.state.searchResults.results[i];
var mxEv = result.context.getEvent();
var roomId = mxEv.getRoomId();
if (!EventTile.haveTileForEvent(mxEv)) {
// XXX: can this ever happen? It will make the result count
@ -744,7 +749,6 @@ module.exports = React.createClass({
}
if (this.state.searchScope === 'All') {
var roomId = mxEv.getRoomId();
if(roomId != lastRoomId) {
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()}
searchResult={result}
searchHighlights={this.state.searchHighlights}
onSelect={this._onSearchResultSelected.bind(this, result)}/>);
resultLink={resultLink}
onImageLoad={onImageLoad}/>);
}
return ret;
},
@ -843,11 +850,19 @@ module.exports = React.createClass({
self.setState({
rejecting: false
});
}, function(err) {
console.error("Failed to reject invite: %s", err);
}, function(error) {
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({
rejecting: false,
rejectError: err
rejectError: error
});
});
},
@ -969,9 +984,14 @@ module.exports = React.createClass({
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
if (this.refs.callView) {
var video = this.refs.callView.getVideoView().getRemoteVideoElement();
video.style.maxHeight = auxPanelMaxHeight + "px";
var fullscreenElement =
(document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement);
if (!fullscreenElement) {
var video = this.refs.callView.getVideoView().getRemoteVideoElement();
video.style.maxHeight = auxPanelMaxHeight + "px";
}
}
// we need to do this for general auxPanels too
@ -1015,10 +1035,16 @@ module.exports = React.createClass({
});
},
onCallViewResize: function() {
this.onChildResize();
this.onResize();
},
onChildResize: function() {
// When the video or the message composer resizes, the scroll panel
// also changes size. Work around GeminiScrollBar fail by telling it
// about it. This also ensures that the scroll offset is updated.
// When the video, status bar, or the message composer resizes, the
// scroll panel also changes size. Work around GeminiScrollBar fail by
// telling it about it. This also ensures that the scroll offset is
// updated.
if (this.refs.messagePanel) {
this.refs.messagePanel.forceUpdate();
}
@ -1055,7 +1081,6 @@ module.exports = React.createClass({
);
}
else {
var joinErrorText = this.state.joinError ? "Failed to join room!" : "";
return (
<div className="mx_RoomView">
<RoomHeader ref="header" room={this.state.room} simpleHeader="Join room"/>
@ -1064,7 +1089,6 @@ module.exports = React.createClass({
canJoin={ true } canPreview={ false }
spinner={this.state.joining}
/>
<div className="error">{joinErrorText}</div>
</div>
<div className="mx_RoomView_messagePanel"></div>
</div>
@ -1090,10 +1114,6 @@ module.exports = React.createClass({
} else {
var inviteEvent = myMember.events.member;
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
// as they could be a spam vector.
@ -1109,8 +1129,6 @@ module.exports = React.createClass({
canJoin={ true } canPreview={ false }
spinner={this.state.joining}
/>
<div className="error">{joinErrorText}</div>
<div className="error">{rejectErrorText}</div>
</div>
<div className="mx_RoomView_messagePanel"></div>
</div>
@ -1157,6 +1175,7 @@ module.exports = React.createClass({
hasActiveCall={inCall}
onResendAllClick={this.onResendAllClick}
onScrollToBottomClick={this.jumpToLiveTimeline}
onResize={this.onChildResize}
/>
}
@ -1295,9 +1314,6 @@ module.exports = React.createClass({
highlightedEventId={this.props.highlightedEventId}
eventId={this.props.eventId}
eventPixelOffset={this.props.eventPixelOffset}
isConferenceUser={this.props.ConferenceHandler ?
this.props.ConferenceHandler.isConferenceUser :
null }
onScroll={ this.onMessageListScroll }
onReadMarkerUpdated={ this._updateTopUnreadMessagesBar }
/>);
@ -1332,7 +1348,7 @@ module.exports = React.createClass({
<div className="mx_RoomView_auxPanel" ref="auxPanel">
{ fileDropTarget }
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}
onResize={this.onChildResize} />
onResize={this.onCallViewResize} />
{ conferenceCallNotification }
{ aux }
</div>

View file

@ -124,10 +124,9 @@ module.exports = React.createClass({
// after adding event tiles, we may need to tweak the scroll (either to
// keep at the bottom of the timeline, or to maintain the view after
// adding events to the top).
this._restoreSavedScrollState();
// we also re-check the fill state, in case the paginate was inadequate
this.checkFillState();
//
// This will also re-check the fill state, in case the paginate was inadequate
this.checkScroll();
},
componentWillUnmount: function() {
@ -178,6 +177,13 @@ module.exports = React.createClass({
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.
//
// 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.
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.
onScroll: React.PropTypes.func,
@ -118,6 +113,7 @@ var TimelinePanel = React.createClass({
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
this._initTimeline(this.props);
},
@ -146,6 +142,7 @@ var TimelinePanel = React.createClass({
var client = MatrixClientPeg.get();
if (client) {
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() {
if (!this.refs.messagePanel) return;
var currentReadUpToEventId = this._getCurrentReadReceipt();
var currentReadUpToEventId = this._getCurrentReadReceipt(true);
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
// 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.
*
* @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();
// the client can be null on logout
if (client == null)
return null;
var myUserId = client.credentials.userId;
return this.props.room.getEventReadUpTo(myUserId);
return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized);
},
_setReadMarker: function(eventId, eventTs) {
@ -601,7 +614,6 @@ var TimelinePanel = React.createClass({
suppressFirstDateSeparator={ this.state.canBackPaginate }
ourUserId={ MatrixClientPeg.get().credentials.userId }
stickyBottom={ stickyBottom }
isConferenceUser={ this.props.isConferenceUser }
onScroll={ this.onMessageListScroll }
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.
onRegisterClick: React.PropTypes.func.isRequired,
// login shouldn't care how password recovery is done.
onForgotPasswordClick: React.PropTypes.func
onForgotPasswordClick: React.PropTypes.func,
onLoginAsGuestClick: React.PropTypes.func,
},
getDefaultProps: function() {
@ -128,11 +129,30 @@ module.exports = React.createClass({displayName: 'Login',
if (!errCode && err.httpStatus) {
errCode = "HTTP " + err.httpStatus;
}
this.setState({
errorText: (
"Error: Problem communicating with the given homeserver " +
var errorText = "Error: Problem communicating with the given homeserver " +
(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 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 (
<div className="mx_Login">
<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="#">
Create a new account
</a>
{ loginAsGuestJsx }
<br/>
<LoginFooter />
</div>

View file

@ -115,6 +115,9 @@ module.exports = React.createClass({
onProcessingRegistration: function(promise) {
var self = this;
promise.done(function(response) {
self.setState({
busy: false
});
if (!response || !response.access_token) {
console.warn(
"FIXME: Register fulfilled without a final response, " +
@ -126,7 +129,7 @@ module.exports = React.createClass({
if (!response || !response.user_id || !response.access_token) {
console.error("Final response is missing keys.");
self.setState({
errorText: "There was a problem processing the response."
errorText: "Registration failed on server"
});
return;
}
@ -136,9 +139,6 @@ module.exports = React.createClass({
identityServerUrl: self.registerLogic.getIdentityServerUrl(),
accessToken: response.access_token
});
self.setState({
busy: false
});
}, function(err) {
if (err.message) {
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() {
return (
<div>
<div className="mx_Dialog_content">
Sign out?
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.logOut}>Sign Out</button>
<div className="mx_Dialog_buttons" onKeyDown={ this.onKeyDown }>
<button autoFocus onClick={this.logOut}>Sign Out</button>
<button onClick={this.cancelPrompt}>Cancel</button>
</div>
</div>

View file

@ -26,9 +26,20 @@ module.exports = React.createClass({
},
getInitialState: function() {
return {
value: this.props.currentDisplayName || "Guest "+MatrixClientPeg.get().getUserIdLocalpart(),
if (this.props.currentDisplayName) {
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() {
@ -54,11 +65,12 @@ module.exports = React.createClass({
Set a Display Name
</div>
<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>
<form onSubmit={this.onFormSubmit}>
<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"
className="mx_SetDisplayNameDialog_input"
/>

View file

@ -27,6 +27,14 @@ var dis = require("../../../dispatcher");
module.exports = React.createClass({
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) {
if (!fullWidth || !fullHeight) {
// 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() {
var content = this.props.mxEvent.getContent();
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 480, 360);
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
},
render: function() {
@ -103,10 +111,10 @@ module.exports = React.createClass({
var cli = MatrixClientPeg.get();
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 = {};
if (thumbHeight) imgStyle['height'] = thumbHeight;
if (thumbHeight) imgStyle['maxHeight'] = thumbHeight;
var thumbUrl = this._getThumbUrl();
if (thumbUrl) {
@ -116,7 +124,8 @@ module.exports = React.createClass({
<img className="mx_MImageBody_thumbnail" src={thumbUrl}
alt={content.body} style={imgStyle}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
onMouseLeave={this.onImageLeave}
onLoad={this.props.onImageLoad} />
</a>
<div className="mx_MImageBody_download">
<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() {
var UnknownMessageTile = sdk.getComponent('messages.UnknownBody');
@ -48,6 +63,7 @@ module.exports = React.createClass({
}
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({
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() {
linkifyElement(this.refs.content, linkifyMatrix.options);
@ -46,14 +57,15 @@ module.exports = React.createClass({
shouldComponentUpdate: function(nextProps) {
// exploit that events are immutable :)
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() {
var mxEvent = this.props.mxEvent;
var content = mxEvent.getContent();
var body = HtmlUtils.bodyToHtml(content, this.props.highlights,
{onHighlightClick: this.props.onHighlightClick});
{highlightLink: this.props.highlightLink});
switch (content.msgtype) {
case "m.emote":

View file

@ -65,6 +65,7 @@ module.exports = React.createClass({
statics: {
haveTileForEvent: function(e) {
if (e.isRedacted()) return false;
if (eventTileTypes[e.getType()] == undefined) return false;
if (eventTileTypes[e.getType()] == 'messages.TextualEvent') {
return TextForEvent.textForEvent(e) !== '';
@ -96,11 +97,14 @@ module.exports = React.createClass({
/* a list of words to highlight */
highlights: React.PropTypes.array,
/* a function to be called when the highlight is clicked */
onHighlightClick: React.PropTypes.func,
/* link URL for the highlights */
highlightLink: React.PropTypes.string,
/* is this the focussed event */
isSelectedEvent: React.PropTypes.bool,
/* callback called when images in events are loaded */
onImageLoad: React.PropTypes.func,
},
getInitialState: function() {
@ -110,6 +114,14 @@ module.exports = React.createClass({
shouldHighlight: function() {
var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent);
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;
},
@ -313,8 +325,9 @@ module.exports = React.createClass({
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<EventTileType mxEvent={this.props.mxEvent} highlights={this.props.highlights}
onHighlightClick={this.props.onHighlightClick} />
<EventTileType mxEvent={this.props.mxEvent} highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
onImageLoad={this.props.onImageLoad} />
</div>
</div>
);

View file

@ -327,7 +327,7 @@ module.exports = React.createClass({
var memberList = self.state.members.filter(function(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 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.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
searchHighlights: React.PropTypes.array,
// callback to be called when the user selects this result
onSelect: React.PropTypes.func,
// href for the highlights in this result
resultLink: React.PropTypes.string,
onImageLoad: React.PropTypes.func,
},
render: function() {
@ -53,7 +55,8 @@ module.exports = React.createClass({
}
if (EventTile.haveTileForEvent(ev)) {
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 (

View file

@ -110,19 +110,17 @@ module.exports = React.createClass({
},
render: function() {
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
var avatarImg;
// Having just set an avatar we just display that since it will take a little
// time to propagate through to the RoomAvatar.
if (this.props.room && !this.avatarSet) {
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
avatarImg = <RoomAvatar room={this.props.room} width={ this.props.width } height={ this.props.height } resizeMethod='crop' />;
} else {
var style = {
width: this.props.width,
height: this.props.height,
objectFit: 'cover',
};
avatarImg = <img className="mx_BaseAvatar_image" src={this.state.avatarUrl} style={style} />;
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
// XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ?
avatarImg = <BaseAvatar width={this.props.width} height={this.props.height} resizeMethod='crop'
name='?' idName={ MatrixClientPeg.get().getUserIdLocalpart() } url={this.state.avatarUrl} />
}
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
// conference will have us in it
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 {
this.getVideoView().getLocalVideoElement().style.display = "none";

View file

@ -64,6 +64,7 @@ module.exports = React.createClass({
element.msRequestFullscreen
);
requestMethod.call(element);
this.getRemoteVideoElement().style.maxHeight = "inherit";
}
else {
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;
}
}
};