From d979a028a5ebaba567711dd3c3bf982f33e65a20 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 17 Dec 2015 02:49:09 +0000 Subject: [PATCH 1/8] position the inbound call box correctly, and fix various issues with when the video preview and callview are shown --- src/CallHandler.js | 10 ++- src/components/structures/RoomView.js | 3 +- src/components/views/rooms/MessageComposer.js | 2 + src/components/views/rooms/RoomList.js | 67 +++++++++++++++++- src/components/views/rooms/RoomTile.js | 8 +++ src/components/views/voip/CallView.js | 12 +--- src/components/views/voip/IncomingCallBox.js | 70 ++----------------- 7 files changed, 94 insertions(+), 78 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index 187449924f..189e99b307 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -138,9 +138,17 @@ function _setCallListeners(call) { function _setCallState(call, roomId, status) { console.log( - "Call state in %s changed to %s (%s)", roomId, status, (call ? call.state : "-") + "Call state in %s changed to %s (%s)", roomId, status, (call ? call.call_state : "-") ); calls[roomId] = call; + + if (status === "ringing") { + play("ringAudio") + } + else if (call && call.call_state === "ringing") { + pause("ringAudio") + } + if (call) { call.call_state = status; } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 6db2659986..01b877e6cf 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1207,8 +1207,9 @@ module.exports = React.createClass({ } var call = CallHandler.getCallForRoom(this.props.roomId); + //var call = CallHandler.getAnyActiveCall(); var inCall = false; - if (call && this.state.callState != 'ended') { + if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) { inCall = true; var zoomButton, voiceMuteButton, videoMuteButton; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 73f553af6e..7c228b5c9d 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -529,6 +529,7 @@ module.exports = React.createClass({ onHangupClick: function() { var call = CallHandler.getCallForRoom(this.props.room.roomId); + //var call = CallHandler.getAnyActiveCall(); if (!call) { return; } @@ -563,6 +564,7 @@ module.exports = React.createClass({ var callButton, videoCallButton, hangupButton; var call = CallHandler.getCallForRoom(this.props.room.roomId); + //var call = CallHandler.getAnyActiveCall(); if (this.props.callState && this.props.callState !== 'ended') { hangupButton =
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index a89dd55f1a..9ec2bf0d45 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -19,6 +19,7 @@ var React = require("react"); var ReactDOM = require("react-dom"); var GeminiScrollbar = require('react-gemini-scrollbar'); var MatrixClientPeg = require("../../../MatrixClientPeg"); +var CallHandler = require('../../../CallHandler'); var RoomListSorter = require("../../../RoomListSorter"); var UnreadStatus = require('../../../UnreadStatus'); var dis = require("../../../dispatcher"); @@ -39,6 +40,7 @@ module.exports = React.createClass({ return { activityMap: null, lists: {}, + incomingCall: null, } }, @@ -66,7 +68,21 @@ module.exports = React.createClass({ this.tooltip = payload.tooltip; this._repositionTooltip(); if (this.tooltip) this.tooltip.style.display = 'block'; - break + break; + case 'call_state': + var call = CallHandler.getCall(payload.room_id); + if (call && call.call_state === 'ringing') { + this.setState({ + incomingCall: call + }); + this._repositionIncomingCallBox(undefined, true); + } + else { + this.setState({ + incomingCall: null + }); + } + break; } }, @@ -212,10 +228,49 @@ module.exports = React.createClass({ return s; }, + _getScrollNode: function() { + var panel = ReactDOM.findDOMNode(this); + if (!panel) return null; + + if (panel.classList.contains('gm-prevented')) { + return panel; + } else { + return panel.children[2]; // XXX: Fragile! + } + }, + + _repositionTooltips: function(e) { + this._repositionTooltip(e); + this._repositionIncomingCallBox(e, false); + }, + _repositionTooltip: function(e) { if (this.tooltip && this.tooltip.parentElement) { var scroll = ReactDOM.findDOMNode(this); - this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.children[2].scrollTop) + "px"; + this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px"; + } + }, + + _repositionIncomingCallBox: function(e, firstTime) { + var incomingCallBox = document.getElementById("incomingCallBox"); + if (incomingCallBox && incomingCallBox.parentElement) { + var scroll = this._getScrollNode(); + var top = (scroll.offsetTop + incomingCallBox.parentElement.offsetTop - scroll.scrollTop); + + if (firstTime) { + // scroll to make sure the callbox is on the screen... + if (top < 10) { + scroll.scrollTop = incomingCallBox.parentElement.offsetTop - 10; + } + else if (top > scroll.clientHeight - incomingCallBox.offsetHeight) { + scroll.scrollTop = incomingCallBox.parentElement.offsetTop - scroll.offsetHeight + incomingCallBox.offsetHeight; + } + // recalculate top in case we clipped it. + top = (scroll.offsetTop + incomingCallBox.parentElement.offsetTop - scroll.scrollTop); + } + + incomingCallBox.style.top = top + "px"; + incomingCallBox.style.left = scroll.offsetLeft + scroll.offsetWidth + "px"; } }, @@ -234,7 +289,7 @@ module.exports = React.createClass({ var self = this; return ( - +
{ expandButton } @@ -244,6 +299,7 @@ module.exports = React.createClass({ order="recent" activityMap={ self.state.activityMap } selectedRoom={ self.props.selectedRoom } + incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } /> { Object.keys(self.state.lists).map(function(tagName) { @@ -276,6 +334,7 @@ module.exports = React.createClass({ order="manual" activityMap={ self.state.activityMap } selectedRoom={ self.props.selectedRoom } + incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } /> } @@ -290,6 +349,7 @@ module.exports = React.createClass({ bottommost={ self.state.lists['im.vector.fake.archived'].length === 0 } activityMap={ self.state.activityMap } selectedRoom={ self.props.selectedRoom } + incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } />
diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 0a03ebe89d..37a77f9561 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -38,6 +38,7 @@ module.exports = React.createClass({ highlight: React.PropTypes.bool.isRequired, isInvite: React.PropTypes.bool.isRequired, roomSubList: React.PropTypes.object.isRequired, + incomingCall: React.PropTypes.object, }, getInitialState: function() { @@ -105,6 +106,12 @@ module.exports = React.createClass({ label = ; } + var incomingCallBox; + if (this.props.incomingCall) { + var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); + incomingCallBox = ; + } + var RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); // These props are injected by React DnD, @@ -120,6 +127,7 @@ module.exports = React.createClass({ { badge }
{ label } + { incomingCallBox } )); } diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js index fbaed1dcd7..d67147dd1e 100644 --- a/src/components/views/voip/CallView.js +++ b/src/components/views/voip/CallView.js @@ -35,19 +35,13 @@ module.exports = React.createClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); - this._trackedRoom = null; if (this.props.room) { - this._trackedRoom = this.props.room; - this.showCall(this._trackedRoom.roomId); + this.showCall(this.props.room.roomId); } else { + // XXX: why would we ever not have a this.props.room? var call = CallHandler.getAnyActiveCall(); if (call) { - console.log( - "Global CallView is now tracking active call in room %s", - call.roomId - ); - this._trackedRoom = MatrixClientPeg.get().getRoom(call.roomId); this.showCall(call.roomId); } } @@ -81,7 +75,7 @@ module.exports = React.createClass({ // and for the voice stream of screen captures call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement()); } - if (call && call.type === "video" && call.state !== 'ended') { + if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") { // 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 = ( diff --git a/src/components/views/voip/IncomingCallBox.js b/src/components/views/voip/IncomingCallBox.js index 263bbf543c..b110d45043 100644 --- a/src/components/views/voip/IncomingCallBox.js +++ b/src/components/views/voip/IncomingCallBox.js @@ -21,87 +21,29 @@ var CallHandler = require("../../../CallHandler"); module.exports = React.createClass({ displayName: 'IncomingCallBox', - componentDidMount: function() { - this.dispatcherRef = dis.register(this.onAction); - }, - - componentWillUnmount: function() { - dis.unregister(this.dispatcherRef); - }, - - getInitialState: function() { - return { - incomingCall: null - } - }, - - onAction: function(payload) { - if (payload.action !== 'call_state') { - return; - } - var call = CallHandler.getCall(payload.room_id); - if (!call || call.call_state !== 'ringing') { - this.setState({ - incomingCall: null, - }); - this.getRingAudio().pause(); - return; - } - if (call.call_state === "ringing") { - this.getRingAudio().load(); - this.getRingAudio().play(); - } - else { - this.getRingAudio().pause(); - } - - this.setState({ - incomingCall: call - }); - }, - onAnswerClick: function() { dis.dispatch({ action: 'answer', - room_id: this.state.incomingCall.roomId + room_id: this.props.incomingCall.roomId }); }, onRejectClick: function() { dis.dispatch({ action: 'hangup', - room_id: this.state.incomingCall.roomId + room_id: this.props.incomingCall.roomId }); }, - getRingAudio: function() { - return this.refs.ringAudio; - }, - render: function() { - // NB: This block MUST have a "key" so React doesn't clobber the elements - // between in-call / not-in-call. - var audioBlock = ( - - ); - if (!this.state.incomingCall || !this.state.incomingCall.roomId) { - return ( -
- {audioBlock} -
- ); - } - var caller = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId).name; + var room = MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId); + var caller = room ? room.name : "unknown"; return ( -
- {audioBlock} +
- Incoming { this.state.incomingCall ? this.state.incomingCall.type : '' } call from { caller } + Incoming { this.props.incomingCall ? this.props.incomingCall.type : '' } call from { caller }
From bb184c12a1b0abb5096f64f959e88b30d7775793 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 17 Dec 2015 11:56:41 +0000 Subject: [PATCH 2/8] fix NPE --- src/components/views/voip/IncomingCallBox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voip/IncomingCallBox.js b/src/components/views/voip/IncomingCallBox.js index b110d45043..a9601931bb 100644 --- a/src/components/views/voip/IncomingCallBox.js +++ b/src/components/views/voip/IncomingCallBox.js @@ -37,7 +37,7 @@ module.exports = React.createClass({ render: function() { - var room = MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId); + var room = this.props.incomingCall ? MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId) : null; var caller = room ? room.name : "unknown"; return (
From 3ff19dc4db18db52ac4fc8651366a7f7db46b1bc Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 17 Dec 2015 14:14:26 +0000 Subject: [PATCH 3/8] Fix issue with rooms not scrolling down when new events arrive Remove an optimisation which tried to avoid recalculating the scroll on every render. The problem is that sometimes, when new events, the number of event tiles remains the same, but we still need to do a scroll, because the height of the message list changes. The optimisation was a bit of a waste of time anyway - the actual render will always be much more difficult than recalculating the scroll position. --- src/components/structures/RoomView.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index a7944f91f5..2e8a39aae9 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -318,11 +318,7 @@ module.exports = React.createClass({ if (this.state.searchResults) return; - if (this.needsScrollReset) { - if (DEBUG_SCROLL) console.log("Resetting scroll position after tile count change"); - this._restoreSavedScrollState(); - this.needsScrollReset = false; - } + this._restoreSavedScrollState(); // have to fill space in case we're accepting an invite if (!this.state.paginating) this.fillSpace(); @@ -683,10 +679,6 @@ module.exports = React.createClass({ } ++count; } - if (count != this.lastEventTileCount) { - if (DEBUG_SCROLL) console.log("Queuing scroll reset (event count changed; now "+count+"; was "+this.lastEventTileCount+")"); - this.needsScrollReset = true; - } this.lastEventTileCount = count; return ret; }, From 5c999fe1ab307083ef224728c45e1b8f8c3ed681 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 17 Dec 2015 14:56:09 +0000 Subject: [PATCH 4/8] stop the incoming call box from scrolling off the screen --- src/components/views/rooms/RoomList.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 9ec2bf0d45..ad749295ae 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -259,15 +259,24 @@ module.exports = React.createClass({ if (firstTime) { // scroll to make sure the callbox is on the screen... - if (top < 10) { + if (top < 10) { // 10px of vertical margin at top of screen scroll.scrollTop = incomingCallBox.parentElement.offsetTop - 10; } - else if (top > scroll.clientHeight - incomingCallBox.offsetHeight) { - scroll.scrollTop = incomingCallBox.parentElement.offsetTop - scroll.offsetHeight + incomingCallBox.offsetHeight; + else if (top > scroll.clientHeight - incomingCallBox.offsetHeight + 50) { + scroll.scrollTop = incomingCallBox.parentElement.offsetTop - scroll.offsetHeight + incomingCallBox.offsetHeight - 50; } // recalculate top in case we clipped it. top = (scroll.offsetTop + incomingCallBox.parentElement.offsetTop - scroll.scrollTop); } + else { + // stop the box from scrolling off the screen + if (top < 10) { + top = 10; + } + else if (top > scroll.clientHeight - incomingCallBox.offsetHeight + 50) { + top = scroll.clientHeight - incomingCallBox.offsetHeight + 50; + } + } incomingCallBox.style.top = top + "px"; incomingCallBox.style.left = scroll.offsetLeft + scroll.offsetWidth + "px"; From e0d05d4f4b770c51ab7bd088d1ac977ea1fe6b71 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 17 Dec 2015 22:07:56 +0000 Subject: [PATCH 5/8] Initialise the messagepanel correctly after accepting an invite This should fix vector-im/vector-web#538. I'm sorry. --- src/components/structures/RoomView.js | 46 ++++++++++++++------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 2e8a39aae9..2425fab958 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -77,13 +77,6 @@ module.exports = React.createClass({ }, componentWillUnmount: function() { - if (this.refs.messagePanel) { - var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel); - messagePanel.removeEventListener('drop', this.onDrop); - messagePanel.removeEventListener('dragover', this.onDragOver); - messagePanel.removeEventListener('dragleave', this.onDragLeaveOrEnd); - messagePanel.removeEventListener('dragend', this.onDragLeaveOrEnd); - } dis.unregister(this.dispatcherRef); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); @@ -285,16 +278,7 @@ module.exports = React.createClass({ componentDidMount: function() { if (this.refs.messagePanel) { - var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel); - - messagePanel.addEventListener('drop', this.onDrop); - messagePanel.addEventListener('dragover', this.onDragOver); - messagePanel.addEventListener('dragleave', this.onDragLeaveOrEnd); - messagePanel.addEventListener('dragend', this.onDragLeaveOrEnd); - - this.scrollToBottom(); - this.sendReadReceipt(); - this.fillSpace(); + this._initialiseMessagePanel(); } var call = CallHandler.getCallForRoom(this.props.roomId); @@ -309,19 +293,37 @@ module.exports = React.createClass({ this.onResize(); }, + _initialiseMessagePanel: function() { + var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel); + this.refs.messagePanel.initialised = true; + + messagePanel.addEventListener('drop', this.onDrop); + messagePanel.addEventListener('dragover', this.onDragOver); + messagePanel.addEventListener('dragleave', this.onDragLeaveOrEnd); + messagePanel.addEventListener('dragend', this.onDragLeaveOrEnd); + + this.scrollToBottom(); + this.sendReadReceipt(); + this.fillSpace(); + }, + componentDidUpdate: function() { + // we need to initialise the messagepanel if we've just joined the + // room. TODO: we really really ought to factor out messagepanel to a + // separate component to avoid this ridiculous dance. + if (!this.refs.messagePanel) return; + + if (!this.refs.messagePanel.initialised) { + this._initialiseMessagePanel(); + } + // 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). - if (!this.refs.messagePanel) return; - if (this.state.searchResults) return; this._restoreSavedScrollState(); - - // have to fill space in case we're accepting an invite - if (!this.state.paginating) this.fillSpace(); }, _paginateCompleted: function() { From 0f82b72e075f91d8adca122edfc30d16cd038690 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 18 Dec 2015 00:13:49 +0000 Subject: [PATCH 6/8] lost copyright --- src/Resend.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Resend.js b/src/Resend.js index 0e67a306bd..e07e571455 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -1,3 +1,19 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + var MatrixClientPeg = require('./MatrixClientPeg'); var dis = require('./dispatcher'); From d6c208a27507b2935f94b8d4b62a139a236f7002 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 18 Dec 2015 10:19:07 +0000 Subject: [PATCH 7/8] Reinstate the DnD event listener removals, with comments --- src/components/structures/RoomView.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 2425fab958..81ac1db639 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -77,6 +77,17 @@ module.exports = React.createClass({ }, componentWillUnmount: function() { + if (this.refs.messagePanel) { + // disconnect the D&D event listeners from the message panel. This + // is really just for hygiene - the messagePanel is going to be + // deleted anyway, so it doesn't matter if the event listeners + // don't get cleaned up. + var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel); + messagePanel.removeEventListener('drop', this.onDrop); + messagePanel.removeEventListener('dragover', this.onDragOver); + messagePanel.removeEventListener('dragleave', this.onDragLeaveOrEnd); + messagePanel.removeEventListener('dragend', this.onDragLeaveOrEnd); + } dis.unregister(this.dispatcherRef); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); From 4b271a332e5e817bde391a9517a9bbde8362b7b0 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Fri, 18 Dec 2015 11:11:41 +0000 Subject: [PATCH 8/8] Refactor the search stuff in RoomView * factor out the call to MatrixClient.search to a separate _getSearchBatch (so that we can reuse it for paginated results in a bit) * Don't group cross-room searches by room - just display them in timeline order. --- src/components/structures/RoomView.js | 207 ++++++++++++----------- src/components/views/rooms/RoomHeader.js | 4 +- 2 files changed, 109 insertions(+), 102 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 81ac1db639..e9341b7389 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -490,75 +490,65 @@ module.exports = React.createClass({ }, onSearch: function(term, scope) { - var filter; - if (scope === "Room") { - filter = { - // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( - rooms: [ - this.props.roomId - ] - }; - } - - var self = this; - self.setState({ - searchInProgress: true + this.setState({ + searchTerm: term, + searchScope: scope, + searchResults: [], + searchHighlights: [], + searchCount: null, }); - MatrixClientPeg.get().search({ - body: { - search_categories: { - room_events: { - search_term: term, - filter: filter, - order_by: "recent", - include_state: true, - groupings: { - group_by: [ - { - key: "room_id" - } - ] - }, - event_context: { - before_limit: 1, - after_limit: 1, - include_profile: true, - } - } - } - } - }).then(function(data) { + this._getSearchBatch(term, scope); + }, - if (!self.state.searching || term !== self.refs.search_bar.refs.search_term.value) { + // fire off a request for a batch of search results + _getSearchBatch: function(term, scope) { + this.setState({ + searchInProgress: true, + }); + + // make sure that we don't end up merging results from + // different searches by keeping a unique id. + // + // todo: should cancel any previous search requests. + var searchId = this.searchId = new Date().getTime(); + + var self = this; + + MatrixClientPeg.get().search({ body: this._getSearchCondition(term, scope) }) + .then(function(data) { + if (!self.state.searching || self.searchId != searchId) { console.error("Discarding stale search results"); return; } - // for debugging: - // data.search_categories.room_events.highlights = ["hello", "everybody"]; + var results = data.search_categories.room_events; - var highlights; - if (data.search_categories.room_events.highlights && - data.search_categories.room_events.highlights.length > 0) - { - // postgres on synapse returns us precise details of the - // strings which actually got matched for highlighting. - // for overlapping highlights, favour longer (more specific) terms first - highlights = data.search_categories.room_events.highlights - .sort(function(a, b) { b.length - a.length }); - } - else { - // sqlite doesn't, so just try to highlight the literal search term + // postgres on synapse returns us precise details of the + // strings which actually got matched for highlighting. + + // combine the highlight list with our existing list; build an object + // to avoid O(N^2) fail + var highlights = {}; + results.highlights.forEach(function(hl) { highlights[hl] = 1; }); + self.state.searchHighlights.forEach(function(hl) { highlights[hl] = 1; }); + + // turn it back into an ordered list. For overlapping highlights, + // favour longer (more specific) terms first + highlights = Object.keys(highlights).sort(function(a, b) { b.length - a.length }); + + // sqlite doesn't give us any highlights, so just try to highlight the literal search term + if (highlights.length == 0) { highlights = [ term ]; } + // append the new results to our existing results + var events = self.state.searchResults.concat(results.results); + self.setState({ - highlights: highlights, - searchTerm: term, - searchResults: data, - searchScope: scope, - searchCount: data.search_categories.room_events.count, + searchHighlights: highlights, + searchResults: events, + searchCount: results.count, }); }, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -570,7 +560,35 @@ module.exports = React.createClass({ self.setState({ searchInProgress: false }); - }); + }).done(); + }, + + _getSearchCondition: function(term, scope) { + var filter; + + if (scope === "Room") { + filter = { + // XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( + rooms: [ + this.props.roomId + ] + }; + } + + return { + search_categories: { + room_events: { + search_term: term, + filter: filter, + order_by: "recent", + event_context: { + before_limit: 1, + after_limit: 1, + include_profile: true, + } + } + } + } }, getEventTiles: function() { @@ -585,57 +603,44 @@ module.exports = React.createClass({ if (this.state.searchResults) { - if (!this.state.searchResults.search_categories.room_events.results || - !this.state.searchResults.search_categories.room_events.groups) - { - return ret; - } + // XXX: todo: merge overlapping results somehow? + // XXX: why doesn't searching on name work? - // XXX: this dance is foul, due to the results API not directly returning sorted results - var results = this.state.searchResults.search_categories.room_events.results; - var roomIdGroups = this.state.searchResults.search_categories.room_events.groups.room_id; + var lastRoomId; - if (Array.isArray(results)) { - // Old search API used to return results as a event_id -> result dict, but now - // returns a straightforward list. - results = results.reduce(function(prev, curr) { - prev[curr.result.event_id] = curr; - return prev; - }, {}); - } + for (var i = this.state.searchResults.length - 1; i >= 0; i--) { + var result = this.state.searchResults[i]; + var mxEv = new Matrix.MatrixEvent(result.result); - Object.keys(roomIdGroups) - .sort(function(a, b) { roomIdGroups[a].order - roomIdGroups[b].order }) // WHY NOT RETURN AN ORDERED ARRAY?!?!?! - .forEach(function(roomId) - { - // XXX: todo: merge overlapping results somehow? - // XXX: why doesn't searching on name work? if (self.state.searchScope === 'All') { - ret.push(
  • Room: { cli.getRoom(roomId).name }

  • ); + var roomId = result.result.room_id; + if(roomId != lastRoomId) { + ret.push(
  • Room: { cli.getRoom(roomId).name }

  • ); + lastRoomId = roomId; + } } - var resultList = roomIdGroups[roomId].results.map(function(eventId) { return results[eventId]; }); - for (var i = resultList.length - 1; i >= 0; i--) { - var ts1 = resultList[i].result.origin_server_ts; - ret.push(
  • ); // Rank: {resultList[i].rank} - var mxEv = new Matrix.MatrixEvent(resultList[i].result); - if (resultList[i].context.events_before[0]) { - var mxEv2 = new Matrix.MatrixEvent(resultList[i].context.events_before[0]); - if (EventTile.haveTileForEvent(mxEv2)) { - ret.push(
  • ); - } - } - if (EventTile.haveTileForEvent(mxEv)) { - ret.push(
  • ); - } - if (resultList[i].context.events_after[0]) { - var mxEv2 = new Matrix.MatrixEvent(resultList[i].context.events_after[0]); - if (EventTile.haveTileForEvent(mxEv2)) { - ret.push(
  • ); - } + var ts1 = result.result.origin_server_ts; + ret.push(
  • ); // Rank: {resultList[i].rank} + + if (result.context.events_before[0]) { + var mxEv2 = new Matrix.MatrixEvent(result.context.events_before[0]); + if (EventTile.haveTileForEvent(mxEv2)) { + ret.push(
  • ); } } - }); + + if (EventTile.haveTileForEvent(mxEv)) { + ret.push(
  • ); + } + + if (result.context.events_after[0]) { + var mxEv2 = new Matrix.MatrixEvent(result.context.events_after[0]); + if (EventTile.haveTileForEvent(mxEv2)) { + ret.push(
  • ); + } + } + } return ret; } diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 068dff85d6..1e287ac7dc 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -103,7 +103,9 @@ module.exports = React.createClass({ // var searchStatus; - if (this.props.searchInfo && this.props.searchInfo.searchTerm) { + // don't display the search count until the search completes and + // gives us a non-null searchCount. + if (this.props.searchInfo && this.props.searchInfo.searchCount !== null) { searchStatus =
     ({ this.props.searchInfo.searchCount } results)
    ; }