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/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'); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 04aa81d3e1..683076d932 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -78,6 +78,10 @@ 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); @@ -285,16 +289,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,23 +304,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; - if (this.needsScrollReset) { - if (DEBUG_SCROLL) console.log("Resetting scroll position after tile count change"); - this._restoreSavedScrollState(); - this.needsScrollReset = false; - } - - // have to fill space in case we're accepting an invite - if (!this.state.paginating) this.fillSpace(); + this._restoreSavedScrollState(); }, _paginateCompleted: function() { @@ -481,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"); @@ -561,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() { @@ -576,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; } @@ -683,10 +697,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; }, @@ -1282,8 +1292,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/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index aaf570305c..d045c486f5 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)
    ; } diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 887f6adb5e..f1d467f5ab 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"); @@ -40,6 +41,7 @@ module.exports = React.createClass({ activityMap: null, isLoadingLeftRooms: false, lists: {}, + incomingCall: null, } }, @@ -68,7 +70,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; } }, @@ -272,10 +288,58 @@ 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) { // 10px of vertical margin at top of screen + scroll.scrollTop = incomingCallBox.parentElement.offsetTop - 10; + } + 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"; } }, @@ -294,7 +358,7 @@ module.exports = React.createClass({ var self = this; return ( - +
    { expandButton } @@ -304,6 +368,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) { @@ -336,6 +403,7 @@ module.exports = React.createClass({ order="manual" activityMap={ self.state.activityMap } selectedRoom={ self.props.selectedRoom } + incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } /> } @@ -350,6 +418,7 @@ module.exports = React.createClass({ bottommost={ false } activityMap={ self.state.activityMap } selectedRoom={ self.props.selectedRoom } + incomingCall={ self.state.incomingCall } collapsed={ self.props.collapsed } /> + onHeaderClick= { self.onArchivedHeaderClick } + incomingCall={ self.state.incomingCall } />
    ); 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..a9601931bb 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 = this.props.incomingCall ? MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId) : null; + 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 }