diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 82c295756b..bbd714fa57 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -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}); + } }); } diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 0b7f17b2b2..6db2b08fd1 100644 --- a/src/HtmlUtils.js +++ b/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 = "" + + snippet + ""; + + if (this.highlightLink) { + span = "" + +span+""; + } + return span; + } +} + +class TextHighlighter extends BaseHighlighter { + constructor(highlightClass, highlightLink) { + super(highlightClass, highlightLink); + this._key = 0; + } /* create a 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 = + + { snippet } + ; + + if (highlight && this.highlightLink) { + node = {node} } - if (this.html) { - return (); - } - else { - return ({ spanBody }); - } + 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 { diff --git a/src/Notifier.js b/src/Notifier.js index e52fd252fe..b64a001a5f 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -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) { diff --git a/src/Tinter.js b/src/Tinter.js index b258930425..a83ccdce74 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -64,6 +64,7 @@ var cssAttrs = [ "borderColor", "borderTopColor", "borderBottomColor", + "borderLeftColor", ]; var svgAttrs = [ diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 5869c8ef33..0a9231247c 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -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} + /> ); } } diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index f2d2cf901b..45b6f5ea20 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -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(
  • ); + } + + 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
  • 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
  • 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}> + last={last} isSelectedEvent={highlight} + onImageLoad={this._onImageLoad} />
  • ); @@ -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 ( diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 9ff3925b10..2e0897e3d0 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -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 ( +
    + +
    + ); + } + + if (!this.props.atEndOfLiveTimeline) { + return ( +
    + Scroll to bottom of page +
    + ); + } + + if (this.props.hasActiveCall) { + return ( + + ); + } + + if (this.state.syncState === "ERROR") { + return null; + } + + if (wantPlaceholder) { + return ( +
    ...
    + ); + } + + 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 ( -
    +
    /!\ -
    -
    - Connectivity to the server has been lost. -
    -
    - Sent messages will be stored until your connection has returned. -
    +
    + Connectivity to the server has been lost. +
    +
    + Sent messages will be stored until your connection has returned.
    ); @@ -102,11 +192,10 @@ module.exports = React.createClass({ if (this.props.tabCompleteEntries) { return ( -
    -
    ...
    -
    +
    +
    -
    +
    Auto-complete
    @@ -117,18 +206,16 @@ module.exports = React.createClass({ if (this.props.hasUnsentMessages) { return ( -
    +
    /!\ -
    -
    - Some of your messages have not been sent. -
    -
    - - Resend all now - or select individual messages to re-send. -
    +
    + Some of your messages have not been sent. +
    +
    + + Resend all now + or select individual messages to re-send.
    ); @@ -141,8 +228,8 @@ module.exports = React.createClass({ (this.props.numUnreadMessages > 1 ? "s" : ""); return ( -
    - +
    {unreadMsgs}
    ); @@ -151,30 +238,35 @@ module.exports = React.createClass({ var typingString = WhoIsTyping.whoIsTypingString(this.props.room); if (typingString) { return ( -
    -
    ...
    - {typingString} +
    + {typingString}
    ); } - if (!this.props.atEndOfLiveTimeline) { - return ( -
    - Scroll to bottom of page -
    - ); - } - if (this.props.hasActiveCall) { return ( -
    - +
    Active call
    ); } - return
    ; + return null; }, + + + render: function() { + var content = this._getContent(); + var indicator = this._getIndicator(content !== null); + + return ( +
    +
    + {indicator} +
    + {content} +
    + ); + }, }); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index dfb28e9f2a..dbd4e3f0aa 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -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(); + 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 (
    @@ -1064,7 +1089,6 @@ module.exports = React.createClass({ canJoin={ true } canPreview={ false } spinner={this.state.joining} /> -
    {joinErrorText}
    @@ -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} /> -
    {joinErrorText}
    -
    {rejectErrorText}
    @@ -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({
    { fileDropTarget } + onResize={this.onCallViewResize} /> { conferenceCallNotification } { aux }
    diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 7269defe55..030904cb57 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -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 diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 175faea1f3..a1fb1e065c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -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 } /> diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index b853b8fd95..ef6b095da0 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -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 = + Can't connect to homeserver via HTTP when using a vector served by HTTPS. + Either use HTTPS or enable unsafe scripts + ; + } + else { + errorText = + Can't connect to homeserver - please check your connectivity and ensure + your homeserver's SSL certificate is trusted. + ; + } + } + + 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 ?
    : null; + var loginAsGuestJsx; + if (this.props.onLoginAsGuestClick) { + loginAsGuestJsx = + + Login as guest + + } return (
    @@ -188,6 +215,7 @@ module.exports = React.createClass({displayName: 'Login', Create a new account + { loginAsGuestJsx }
    diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 5666318368..9ec379a814 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -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({ diff --git a/src/components/views/dialogs/LogoutPrompt.js b/src/components/views/dialogs/LogoutPrompt.js index 824924e999..67fedfe840 100644 --- a/src/components/views/dialogs/LogoutPrompt.js +++ b/src/components/views/dialogs/LogoutPrompt.js @@ -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 (
    Sign out?
    -
    - +
    +
    diff --git a/src/components/views/dialogs/SetDisplayNameDialog.js b/src/components/views/dialogs/SetDisplayNameDialog.js index d1287e2570..624bb50a46 100644 --- a/src/components/views/dialogs/SetDisplayNameDialog.js +++ b/src/components/views/dialogs/SetDisplayNameDialog.js @@ -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
    - 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.
    + What would you like it to be?
    - diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 691380d678..b60098295a 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -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({ {content.body} + onMouseLeave={this.onImageLeave} + onLoad={this.props.onImageLoad} />
    diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 2490d9be8b..34d6d53924 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -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 ; + highlightLink={this.props.highlightLink} + onImageLoad={this.props.onImageLoad} />; }, }); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index e3613ef9a3..92447dd1da 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -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": diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f580686128..8ac7ab7996 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -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 }
    - +
    ); diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 4e8f38b035..eba09ca9da 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -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; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 5c729c6047..07ed450ef2 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -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"; } diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.js index 9d3af16ee7..1fc0384433 100644 --- a/src/components/views/rooms/SearchResultTile.js +++ b/src/components/views/rooms/SearchResultTile.js @@ -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() + highlightLink={this.props.resultLink} + onImageLoad={this.props.onImageLoad} />); } } return ( diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 89303856b2..9b03aba1a3 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -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 = ; } else { - var style = { - width: this.props.width, - height: this.props.height, - objectFit: 'cover', - }; - avatarImg = ; + 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 = } var uploadSection; diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js index ed44313b9e..5958c2b278 100644 --- a/src/components/views/voip/CallView.js +++ b/src/components/views/voip/CallView.js @@ -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"; diff --git a/src/components/views/voip/VideoView.js b/src/components/views/voip/VideoView.js index 08e587f47f..6b4b546270 100644 --- a/src/components/views/voip/VideoView.js +++ b/src/components/views/voip/VideoView.js @@ -64,6 +64,7 @@ module.exports = React.createClass({ element.msRequestFullscreen ); requestMethod.call(element); + this.getRemoteVideoElement().style.maxHeight = "inherit"; } else { var exitMethod = ( diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index abb51e85d8..7fb043f800 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -114,6 +114,17 @@ matrixLinkify.options = { } }; } + }, + + formatHref: function (href, type) { + switch (type) { + case 'roomalias': + return '#/room/' + href; + case 'userid': + return '#'; + default: + return href; + } } };