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 }