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
	
	 Richard van der Hoff
						Richard van der Hoff