Merge branch 'rav/roomview_works' into rav/read_marker
Conflicts: src/components/structures/MessagePanel.js src/components/structures/TimelinePanel.jspull/21833/head
						commit
						029f47d91c
					
				| 
						 | 
				
			
			@ -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 New Issue