Merge branch 'rav/roomview_works' into rav/read_marker
Conflicts: src/components/structures/MessagePanel.js src/components/structures/TimelinePanel.js
This commit is contained in:
commit
029f47d91c
25 changed files with 520 additions and 245 deletions
|
@ -92,6 +92,7 @@ class ContentMessages {
|
|||
this.inprogress.push(upload);
|
||||
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});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
103
src/HtmlUtils.js
103
src/HtmlUtils.js
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -64,6 +64,7 @@ var cssAttrs = [
|
|||
"borderColor",
|
||||
"borderTopColor",
|
||||
"borderBottomColor",
|
||||
"borderLeftColor",
|
||||
];
|
||||
|
||||
var svgAttrs = [
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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} />;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -64,6 +64,7 @@ module.exports = React.createClass({
|
|||
element.msRequestFullscreen
|
||||
);
|
||||
requestMethod.call(element);
|
||||
this.getRemoteVideoElement().style.maxHeight = "inherit";
|
||||
}
|
||||
else {
|
||||
var exitMethod = (
|
||||
|
|
|
@ -114,6 +114,17 @@ matrixLinkify.options = {
|
|||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
formatHref: function (href, type) {
|
||||
switch (type) {
|
||||
case 'roomalias':
|
||||
return '#/room/' + href;
|
||||
case 'userid':
|
||||
return '#';
|
||||
default:
|
||||
return href;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue