Merge branch 'develop' into matthew/settings

pull/21833/head
Matthew Hodgson 2015-12-21 13:09:33 +00:00
commit b9ba4475b8
8 changed files with 533 additions and 314 deletions

View File

@ -138,9 +138,17 @@ function _setCallListeners(call) {
function _setCallState(call, roomId, status) { function _setCallState(call, roomId, status) {
console.log( 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; calls[roomId] = call;
if (status === "ringing") {
play("ringAudio")
}
else if (call && call.call_state === "ringing") {
pause("ringAudio")
}
if (call) { if (call) {
call.call_state = status; call.call_state = status;
} }

View File

@ -48,6 +48,17 @@ module.exports = React.createClass({
ConferenceHandler: React.PropTypes.any ConferenceHandler: React.PropTypes.any
}, },
/* properties in RoomView objects include:
*
* savedScrollState: the current scroll position in the backlog. Response
* from _calculateScrollState. Updated on scroll events.
*
* savedSearchScrollState: similar to savedScrollState, but specific to the
* search results (we need to preserve savedScrollState when search
* results are visible)
*
* eventNodes: a map from event id to DOM node representing that event
*/
getInitialState: function() { getInitialState: function() {
var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null; var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null;
return { return {
@ -78,6 +89,10 @@ module.exports = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
if (this.refs.messagePanel) { 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); var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
messagePanel.removeEventListener('drop', this.onDrop); messagePanel.removeEventListener('drop', this.onDrop);
messagePanel.removeEventListener('dragover', this.onDragOver); messagePanel.removeEventListener('dragover', this.onDragOver);
@ -203,7 +218,7 @@ module.exports = React.createClass({
if (!toStartOfTimeline && if (!toStartOfTimeline &&
(ev.getSender() !== MatrixClientPeg.get().credentials.userId)) { (ev.getSender() !== MatrixClientPeg.get().credentials.userId)) {
// update unread count when scrolled up // update unread count when scrolled up
if (this.savedScrollState.atBottom) { if (!this.state.searchResults && this.savedScrollState.atBottom) {
currentUnread = 0; currentUnread = 0;
} }
else { else {
@ -285,16 +300,7 @@ module.exports = React.createClass({
componentDidMount: function() { componentDidMount: function() {
if (this.refs.messagePanel) { if (this.refs.messagePanel) {
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel); this._initialiseMessagePanel();
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();
} }
var call = CallHandler.getCallForRoom(this.props.roomId); var call = CallHandler.getCallForRoom(this.props.roomId);
@ -309,19 +315,34 @@ module.exports = React.createClass({
this.onResize(); 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() { 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 // 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 // keep at the bottom of the timeline, or to maintain the view after
// adding events to the top). // adding events to the top).
if (!this.refs.messagePanel) return;
if (this.state.searchResults) return;
this._restoreSavedScrollState(); this._restoreSavedScrollState();
// have to fill space in case we're accepting an invite
if (!this.state.paginating) this.fillSpace();
}, },
_paginateCompleted: function() { _paginateCompleted: function() {
@ -333,39 +354,49 @@ module.exports = React.createClass({
// we might not have got enough results from the pagination // we might not have got enough results from the pagination
// request, so give fillSpace() a chance to set off another. // request, so give fillSpace() a chance to set off another.
if (!this.fillSpace()) {
this.setState({paginating: false}); this.setState({paginating: false});
if (!this.state.searchResults) {
this.fillSpace();
} }
}, },
// check the scroll position, and if we need to, set off a pagination // check the scroll position, and if we need to, set off a pagination
// request. // request.
//
// returns true if a pagination request was started (or is still in progress)
fillSpace: function() { fillSpace: function() {
if (!this.refs.messagePanel) return; if (!this.refs.messagePanel) return;
if (this.state.searchResults) return; // TODO: paginate search results
var messageWrapperScroll = this._getScrollNode(); var messageWrapperScroll = this._getScrollNode();
if (messageWrapperScroll.scrollTop < messageWrapperScroll.clientHeight && this.state.room.oldState.paginationToken) { if (messageWrapperScroll.scrollTop > messageWrapperScroll.clientHeight) {
// there's less than a screenful of messages left. Either wind back return;
// the message cap (if there are enough events in the timeline to }
// do so), or fire off a pagination request.
this.oldScrollHeight = messageWrapperScroll.scrollHeight; // there's less than a screenful of messages left - try to get some
// more messages.
if (this.state.searchResults) {
if (this.nextSearchBatch) {
if (DEBUG_SCROLL) console.log("requesting more search results");
this._getSearchBatch(this.state.searchTerm,
this.state.searchScope);
} else {
if (DEBUG_SCROLL) console.log("no more search results");
}
return;
}
// Either wind back the message cap (if there are enough events in the
// timeline to do so), or fire off a pagination request.
if (this.state.messageCap < this.state.room.timeline.length) { if (this.state.messageCap < this.state.room.timeline.length) {
var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length); var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length);
if (DEBUG_SCROLL) console.log("winding back message cap to", cap); if (DEBUG_SCROLL) console.log("winding back message cap to", cap);
this.setState({messageCap: cap}); this.setState({messageCap: cap});
} else { } else if(this.state.room.oldState.paginationToken) {
var cap = this.state.messageCap + PAGINATE_SIZE; var cap = this.state.messageCap + PAGINATE_SIZE;
if (DEBUG_SCROLL) console.log("starting paginate to cap", cap); if (DEBUG_SCROLL) console.log("starting paginate to cap", cap);
this.setState({messageCap: cap, paginating: true}); this.setState({messageCap: cap, paginating: true});
MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(this._paginateCompleted).done(); MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(this._paginateCompleted).done();
return true;
} }
}
return false;
}, },
onResendAllClick: function() { onResendAllClick: function() {
@ -418,14 +449,21 @@ module.exports = React.createClass({
this.recentEventScroll = undefined; this.recentEventScroll = undefined;
} }
if (this.refs.messagePanel && !this.state.searchResults) { if (this.refs.messagePanel) {
if (this.state.searchResults) {
this.savedSearchScrollState = this._calculateScrollState();
if (DEBUG_SCROLL) console.log("Saved search scroll state", this.savedSearchScrollState);
} else {
this.savedScrollState = this._calculateScrollState(); this.savedScrollState = this._calculateScrollState();
if (DEBUG_SCROLL) console.log("Saved scroll state", this.savedScrollState); if (DEBUG_SCROLL) console.log("Saved scroll state", this.savedScrollState);
if (this.savedScrollState.atBottom && this.state.numUnreadMessages != 0) { if (this.savedScrollState.atBottom && this.state.numUnreadMessages != 0) {
this.setState({numUnreadMessages: 0}); this.setState({numUnreadMessages: 0});
} }
} }
if (!this.state.paginating) this.fillSpace(); }
if (!this.state.paginating && !this.state.searchInProgress) {
this.fillSpace();
}
}, },
onDragOver: function(ev) { onDragOver: function(ev) {
@ -477,76 +515,72 @@ module.exports = React.createClass({
}, },
onSearch: function(term, scope) { onSearch: function(term, scope) {
var filter; this.setState({
if (scope === "Room") { searchTerm: term,
filter = { searchScope: scope,
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :( searchResults: [],
rooms: [ searchHighlights: [],
this.props.roomId searchCount: null,
]
};
}
var self = this;
self.setState({
searchInProgress: true
}); });
MatrixClientPeg.get().search({ this.savedSearchScrollState = {atBottom: true};
body: { this.nextSearchBatch = null;
search_categories: { this._getSearchBatch(term, scope);
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) {
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;
if (DEBUG_SCROLL) console.log("sending search request");
MatrixClientPeg.get().search({ body: this._getSearchCondition(term, scope),
next_batch: this.nextSearchBatch })
.then(function(data) {
if (DEBUG_SCROLL) console.log("search complete");
if (!self.state.searching || self.searchId != searchId) {
console.error("Discarding stale search results"); console.error("Discarding stale search results");
return; return;
} }
// for debugging: var results = data.search_categories.room_events;
// data.search_categories.room_events.highlights = ["hello", "everybody"];
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 // postgres on synapse returns us precise details of the
// strings which actually got matched for highlighting. // strings which actually got matched for highlighting.
// for overlapping highlights, favour longer (more specific) terms first
highlights = data.search_categories.room_events.highlights // combine the highlight list with our existing list; build an object
.sort(function(a, b) { b.length - a.length }); // to avoid O(N^2) fail
} var highlights = {};
else { results.highlights.forEach(function(hl) { highlights[hl] = 1; });
// sqlite doesn't, so just try to highlight the literal search term 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 ]; highlights = [ term ];
} }
// append the new results to our existing results
var events = self.state.searchResults.concat(results.results);
self.setState({ self.setState({
highlights: highlights, searchHighlights: highlights,
searchTerm: term, searchResults: events,
searchResults: data, searchCount: results.count,
searchScope: scope,
searchCount: data.search_categories.room_events.count,
}); });
self.nextSearchBatch = results.next_batch;
}, function(error) { }, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, { Modal.createDialog(ErrorDialog, {
@ -557,7 +591,35 @@ module.exports = React.createClass({
self.setState({ self.setState({
searchInProgress: false 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() { getEventTiles: function() {
@ -571,58 +633,51 @@ module.exports = React.createClass({
var self = this; var self = this;
if (this.state.searchResults) if (this.state.searchResults)
{
if (!this.state.searchResults.search_categories.room_events.results ||
!this.state.searchResults.search_categories.room_events.groups)
{
return ret;
}
// 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;
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;
}, {});
}
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: todo: merge overlapping results somehow?
// XXX: why doesn't searching on name work? // XXX: why doesn't searching on name work?
if (self.state.searchScope === 'All') {
ret.push(<li key={ roomId }><h1>Room: { cli.getRoom(roomId).name }</h1></li>); var lastRoomId;
for (var i = this.state.searchResults.length - 1; i >= 0; i--) {
var result = this.state.searchResults[i];
var mxEv = new Matrix.MatrixEvent(result.result);
if (!EventTile.haveTileForEvent(mxEv)) {
// XXX: can this ever happen? It will make the result count
// not match the displayed count.
continue;
} }
var resultList = roomIdGroups[roomId].results.map(function(eventId) { return results[eventId]; }); var eventId = mxEv.getId();
for (var i = resultList.length - 1; i >= 0; i--) {
var ts1 = resultList[i].result.origin_server_ts; if (self.state.searchScope === 'All') {
var roomId = result.result.room_id;
if(roomId != lastRoomId) {
ret.push(<li key={eventId + "-room"}><h1>Room: { cli.getRoom(roomId).name }</h1></li>);
lastRoomId = roomId;
}
}
var ts1 = result.result.origin_server_ts;
ret.push(<li key={ts1 + "-search"}><DateSeparator ts={ts1}/></li>); // Rank: {resultList[i].rank} ret.push(<li key={ts1 + "-search"}><DateSeparator ts={ts1}/></li>); // Rank: {resultList[i].rank}
var mxEv = new Matrix.MatrixEvent(resultList[i].result);
if (resultList[i].context.events_before[0]) { if (result.context.events_before[0]) {
var mxEv2 = new Matrix.MatrixEvent(resultList[i].context.events_before[0]); var mxEv2 = new Matrix.MatrixEvent(result.context.events_before[0]);
if (EventTile.haveTileForEvent(mxEv2)) { if (EventTile.haveTileForEvent(mxEv2)) {
ret.push(<li key={mxEv.getId() + "-1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>); ret.push(<li key={eventId+"-1"} data-scroll-token={eventId+"-1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
} }
} }
if (EventTile.haveTileForEvent(mxEv)) {
ret.push(<li key={mxEv.getId() + "+0"}><EventTile mxEvent={mxEv} highlights={self.state.highlights}/></li>); ret.push(<li key={eventId+"+0"} data-scroll-token={eventId+"+0"}><EventTile mxEvent={mxEv} highlights={self.state.searchHighlights}/></li>);
}
if (resultList[i].context.events_after[0]) { if (result.context.events_after[0]) {
var mxEv2 = new Matrix.MatrixEvent(resultList[i].context.events_after[0]); var mxEv2 = new Matrix.MatrixEvent(result.context.events_after[0]);
if (EventTile.haveTileForEvent(mxEv2)) { if (EventTile.haveTileForEvent(mxEv2)) {
ret.push(<li key={mxEv.getId() + "+1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>); ret.push(<li key={eventId+"+1"} data-scroll-token={eventId+"+1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
} }
} }
} }
});
return ret; return ret;
} }
@ -671,15 +726,17 @@ module.exports = React.createClass({
continuation = false; continuation = false;
} }
var eventId = mxEv.getId();
ret.unshift( ret.unshift(
<li key={mxEv.getId()} ref={this._collectEventNode.bind(this, mxEv.getId())}><EventTile mxEvent={mxEv} continuation={continuation} last={last}/></li> <li key={eventId} ref={this._collectEventNode.bind(this, eventId)} data-scroll-token={eventId}>
<EventTile mxEvent={mxEv} continuation={continuation} last={last}/>
</li>
); );
if (dateSeparator) { if (dateSeparator) {
ret.unshift(dateSeparator); ret.unshift(dateSeparator);
} }
++count; ++count;
} }
this.lastEventTileCount = count;
return ret; return ret;
}, },
@ -849,7 +906,7 @@ module.exports = React.createClass({
}, },
onCancelClick: function() { onCancelClick: function() {
this.setState(this.getInitialState()); this.setState({editingRoomSettings: false});
}, },
onLeaveClick: function() { onLeaveClick: function() {
@ -857,7 +914,19 @@ module.exports = React.createClass({
action: 'leave_room', action: 'leave_room',
room_id: this.props.roomId, room_id: this.props.roomId,
}); });
this.props.onFinished(); },
onForgetClick: function() {
MatrixClientPeg.get().forget(this.props.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' });
}, function(err) {
var errCode = err.errcode || "unknown error code";
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Error",
description: `Failed to forget room (${errCode})`
});
});
}, },
onRejectButtonClicked: function(ev) { onRejectButtonClicked: function(ev) {
@ -883,6 +952,13 @@ module.exports = React.createClass({
this.setState({ searching: true }); this.setState({ searching: true });
}, },
onCancelSearchClick: function () {
this.setState({
searching: false,
searchResults: null,
});
},
onConferenceNotificationClick: function() { onConferenceNotificationClick: function() {
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
@ -910,12 +986,6 @@ module.exports = React.createClass({
// pixel_offset gives the number of pixels between the bottom of the event // pixel_offset gives the number of pixels between the bottom of the event
// and the bottom of the container. // and the bottom of the container.
scrollToEvent: function(eventId, pixelOffset) { scrollToEvent: function(eventId, pixelOffset) {
var scrollNode = this._getScrollNode();
if (!scrollNode) return;
var messageWrapper = this.refs.messagePanel;
if (messageWrapper === undefined) return;
var idx = this._indexForEventId(eventId); var idx = this._indexForEventId(eventId);
if (idx === null) { if (idx === null) {
// we don't seem to have this event in our timeline. Presumably // we don't seem to have this event in our timeline. Presumably
@ -925,7 +995,7 @@ module.exports = React.createClass({
// //
// for now, just scroll to the top of the buffer. // for now, just scroll to the top of the buffer.
console.log("Refusing to scroll to unknown event "+eventId); console.log("Refusing to scroll to unknown event "+eventId);
scrollNode.scrollTop = 0; this._getScrollNode().scrollTop = 0;
return; return;
} }
@ -943,14 +1013,88 @@ module.exports = React.createClass({
this.setState({messageCap: minCap}); this.setState({messageCap: minCap});
} }
var node = this.eventNodes[eventId]; // the scrollTokens on our DOM nodes are the event IDs, so we can pass
if (node === null) { // eventId directly into _scrollToToken.
// getEventTiles should have sorted this out when we set the this._scrollToToken(eventId, pixelOffset);
// messageCap, so this is weird. },
console.error("No node for event, even after rolling back messageCap");
_restoreSavedScrollState: function() {
var scrollState = this.state.searchResults ? this.savedSearchScrollState : this.savedScrollState;
if (!scrollState || scrollState.atBottom) {
this.scrollToBottom();
} else if (scrollState.lastDisplayedScrollToken) {
this._scrollToToken(scrollState.lastDisplayedScrollToken,
scrollState.pixelOffset);
}
},
_calculateScrollState: function() {
// we don't save the absolute scroll offset, because that
// would be affected by window width, zoom level, amount of scrollback,
// etc.
//
// instead we save an identifier for the last fully-visible message,
// and the number of pixels the window was scrolled below it - which
// will hopefully be near enough.
//
// Our scroll implementation is agnostic of the precise contents of the
// message list (since it needs to work with both search results and
// timelines). 'refs.messageList' is expected to be a DOM node with a
// number of children, each of which may have a 'data-scroll-token'
// attribute. It is this token which is stored as the
// 'lastDisplayedScrollToken'.
var messageWrapperScroll = this._getScrollNode();
// + 1 here to avoid fractional pixel rounding errors
var atBottom = messageWrapperScroll.scrollHeight - messageWrapperScroll.scrollTop <= messageWrapperScroll.clientHeight + 1;
var messageWrapper = this.refs.messagePanel;
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
var messages = this.refs.messageList.children;
for (var i = messages.length-1; i >= 0; --i) {
var node = messages[i];
if (!node.dataset.scrollToken) continue;
var boundingRect = node.getBoundingClientRect();
if (boundingRect.bottom < wrapperRect.bottom) {
return {
atBottom: atBottom,
lastDisplayedScrollToken: node.dataset.scrollToken,
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
}
}
}
// apparently the entire timeline is below the viewport. Give up.
return { atBottom: true };
},
// scroll the message list to the node with the given scrollToken. See
// notes in _calculateScrollState on how this works.
//
// pixel_offset gives the number of pixels between the bottom of the node
// and the bottom of the container.
_scrollToToken: function(scrollToken, pixelOffset) {
/* find the dom node with the right scrolltoken */
var node;
var messages = this.refs.messageList.children;
for (var i = messages.length-1; i >= 0; --i) {
var m = messages[i];
if (!m.dataset.scrollToken) continue;
if (m.dataset.scrollToken == scrollToken) {
node = m;
break;
}
}
if (!node) {
console.error("No node with scrollToken '"+scrollToken+"'");
return; return;
} }
var scrollNode = this._getScrollNode();
var messageWrapper = this.refs.messagePanel;
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect(); var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
var boundingRect = node.getBoundingClientRect(); var boundingRect = node.getBoundingClientRect();
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom; var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
@ -962,59 +1106,11 @@ module.exports = React.createClass({
} }
if (DEBUG_SCROLL) { if (DEBUG_SCROLL) {
console.log("Scrolled to event", eventId, "+", pixelOffset+":", scrollNode.scrollTop, "(delta: "+scrollDelta+")"); console.log("Scrolled to token", node.dataset.scrollToken, "+", pixelOffset+":", scrollNode.scrollTop, "(delta: "+scrollDelta+")");
console.log("recentEventScroll now "+this.recentEventScroll); console.log("recentEventScroll now "+this.recentEventScroll);
} }
}, },
_restoreSavedScrollState: function() {
var scrollState = this.savedScrollState;
if (scrollState.atBottom) {
this.scrollToBottom();
} else if (scrollState.lastDisplayedEvent) {
this.scrollToEvent(scrollState.lastDisplayedEvent,
scrollState.pixelOffset);
}
},
_calculateScrollState: function() {
// we don't save the absolute scroll offset, because that
// would be affected by window width, zoom level, amount of scrollback,
// etc.
//
// instead we save the id of the last fully-visible event, and the
// number of pixels the window was scrolled below it - which will
// hopefully be near enough.
//
if (this.eventNodes === undefined) return null;
var messageWrapper = this.refs.messagePanel;
if (messageWrapper === undefined) return null;
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
var messageWrapperScroll = this._getScrollNode();
// + 1 here to avoid fractional pixel rounding errors
var atBottom = messageWrapperScroll.scrollHeight - messageWrapperScroll.scrollTop <= messageWrapperScroll.clientHeight + 1;
for (var i = this.state.room.timeline.length-1; i >= 0; --i) {
var ev = this.state.room.timeline[i];
var node = this.eventNodes[ev.getId()];
if (!node) continue;
var boundingRect = node.getBoundingClientRect();
if (boundingRect.bottom < wrapperRect.bottom) {
return {
atBottom: atBottom,
lastDisplayedEvent: ev.getId(),
pixelOffset: wrapperRect.bottom - boundingRect.bottom,
}
}
}
// apparently the entire timeline is below the viewport. Give up.
return { atBottom: true };
},
// get the current scroll position of the room, so that it can be // get the current scroll position of the room, so that it can be
// restored when we switch back to it // restored when we switch back to it
getScrollState: function() { getScrollState: function() {
@ -1022,11 +1118,17 @@ module.exports = React.createClass({
}, },
restoreScrollState: function(scrollState) { restoreScrollState: function(scrollState) {
if (!this.refs.messagePanel) return;
if(scrollState.atBottom) { if(scrollState.atBottom) {
// we were at the bottom before. Ideally we'd scroll to the // we were at the bottom before. Ideally we'd scroll to the
// 'read-up-to' mark here. // 'read-up-to' mark here.
} else if (scrollState.lastDisplayedEvent) { } else if (scrollState.lastDisplayedScrollToken) {
this.scrollToEvent(scrollState.lastDisplayedEvent, // we might need to backfill, so we call scrollToEvent rather than
// _scrollToToken here. The scrollTokens on our DOM nodes are the
// event IDs, so lastDisplayedScrollToken will be the event ID we need,
// and we can pass it directly into scrollToEvent.
this.scrollToEvent(scrollState.lastDisplayedScrollToken,
scrollState.pixelOffset); scrollState.pixelOffset);
} }
}, },
@ -1222,7 +1324,7 @@ module.exports = React.createClass({
aux = <Loader/>; aux = <Loader/>;
} }
else if (this.state.searching) { else if (this.state.searching) {
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelClick} onSearch={this.onSearch}/>; aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>;
} }
var conferenceCallNotification = null; var conferenceCallNotification = null;
@ -1249,21 +1351,29 @@ module.exports = React.createClass({
} }
var messageComposer, searchInfo; var messageComposer, searchInfo;
if (!this.state.searchResults) { var canSpeak = (
// joined and not showing search results
myMember && (myMember.membership == 'join') && !this.state.searchResults
);
if (canSpeak) {
messageComposer = messageComposer =
<MessageComposer room={this.state.room} roomView={this} uploadFile={this.uploadFile} callState={this.state.callState} /> <MessageComposer room={this.state.room} roomView={this} uploadFile={this.uploadFile} callState={this.state.callState} />
} }
else {
// TODO: Why aren't we storing the term/scope/count in this format
// in this.state if this is what RoomHeader desires?
if (this.state.searchResults) {
searchInfo = { searchInfo = {
searchTerm : this.state.searchTerm, searchTerm : this.state.searchTerm,
searchScope : this.state.searchScope, searchScope : this.state.searchScope,
searchCount : this.state.searchCount, searchCount : this.state.searchCount,
} };
} }
var call = CallHandler.getCallForRoom(this.props.roomId); var call = CallHandler.getCallForRoom(this.props.roomId);
//var call = CallHandler.getAnyActiveCall();
var inCall = false; var inCall = false;
if (call && this.state.callState != 'ended') { if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) {
inCall = true; inCall = true;
var zoomButton, voiceMuteButton, videoMuteButton; var zoomButton, voiceMuteButton, videoMuteButton;
@ -1304,8 +1414,18 @@ module.exports = React.createClass({
return ( return (
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") }> <div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") }>
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo} editing={this.state.editingRoomSettings} onSearchClick={this.onSearchClick} <RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
onSettingsClick={this.onSettingsClick} onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} onLeaveClick={this.onLeaveClick} /> editing={this.state.editingRoomSettings}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onSaveClick={this.onSaveClick}
onCancelClick={this.onCancelClick}
onForgetClick={
(myMember && myMember.membership === "leave") ? this.onForgetClick : null
}
onLeaveClick={
(myMember && myMember.membership === "join") ? this.onLeaveClick : null
} />
{ fileDropTarget } { fileDropTarget }
<div className="mx_RoomView_auxPanel"> <div className="mx_RoomView_auxPanel">
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}/> <CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}/>
@ -1314,7 +1434,7 @@ module.exports = React.createClass({
</div> </div>
<GeminiScrollbar autoshow={true} ref="messagePanel" className="mx_RoomView_messagePanel" onScroll={ this.onMessageListScroll }> <GeminiScrollbar autoshow={true} ref="messagePanel" className="mx_RoomView_messagePanel" onScroll={ this.onMessageListScroll }>
<div className="mx_RoomView_messageListWrapper"> <div className="mx_RoomView_messageListWrapper">
<ol className="mx_RoomView_MessageList" aria-live="polite"> <ol ref="messageList" className="mx_RoomView_MessageList" aria-live="polite">
<li className={scrollheader_classes}> <li className={scrollheader_classes}>
</li> </li>
{this.getEventTiles()} {this.getEventTiles()}

View File

@ -529,6 +529,7 @@ module.exports = React.createClass({
onHangupClick: function() { onHangupClick: function() {
var call = CallHandler.getCallForRoom(this.props.room.roomId); var call = CallHandler.getCallForRoom(this.props.room.roomId);
//var call = CallHandler.getAnyActiveCall();
if (!call) { if (!call) {
return; return;
} }
@ -563,6 +564,7 @@ module.exports = React.createClass({
var callButton, videoCallButton, hangupButton; var callButton, videoCallButton, hangupButton;
var call = CallHandler.getCallForRoom(this.props.room.roomId); var call = CallHandler.getCallForRoom(this.props.room.roomId);
//var call = CallHandler.getAnyActiveCall();
if (this.props.callState && this.props.callState !== 'ended') { if (this.props.callState && this.props.callState !== 'ended') {
hangupButton = hangupButton =
<div className="mx_MessageComposer_hangup" onClick={this.onHangupClick}> <div className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>

View File

@ -108,8 +108,10 @@ module.exports = React.createClass({
// <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} /> // <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />
var searchStatus; var searchStatus;
if (this.props.searchInfo && this.props.searchInfo.searchTerm) { // don't display the search count until the search completes and
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;({ this.props.searchInfo.searchCount } results)</div>; // gives us a non-null searchCount.
if (this.props.searchInfo && this.props.searchInfo.searchCount !== null) {
searchStatus = <div className="mx_RoomHeader_searchStatus">&nbsp;(~{ this.props.searchInfo.searchCount } results)</div>;
} }
name = name =
@ -134,7 +136,17 @@ module.exports = React.createClass({
if (this.props.onLeaveClick) { if (this.props.onLeaveClick) {
leave_button = leave_button =
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton"> <div className="mx_RoomHeader_button mx_RoomHeader_leaveButton">
<img src="img/leave.svg" title="Leave room" alt="Leave room" width="26" height="20" onClick={this.props.onLeaveClick}/> <img src="img/leave.svg" title="Leave room" alt="Leave room"
width="26" height="20" onClick={this.props.onLeaveClick}/>
</div>;
}
var forget_button;
if (this.props.onForgetClick) {
forget_button =
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton">
<img src="img/leave.svg" title="Forget room" alt="Forget room"
width="26" height="20" onClick={this.props.onForgetClick}/>
</div>; </div>;
} }
@ -152,6 +164,7 @@ module.exports = React.createClass({
{cancel_button} {cancel_button}
{save_button} {save_button}
<div className="mx_RoomHeader_rightRow"> <div className="mx_RoomHeader_rightRow">
{ forget_button }
{ leave_button } { leave_button }
<div className="mx_RoomHeader_button"> <div className="mx_RoomHeader_button">
<img src="img/search.svg" title="Search" alt="Search" width="21" height="19" onClick={this.props.onSearchClick}/> <img src="img/search.svg" title="Search" alt="Search" width="21" height="19" onClick={this.props.onSearchClick}/>

View File

@ -19,6 +19,7 @@ var React = require("react");
var ReactDOM = require("react-dom"); var ReactDOM = require("react-dom");
var GeminiScrollbar = require('react-gemini-scrollbar'); var GeminiScrollbar = require('react-gemini-scrollbar');
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
var CallHandler = require('../../../CallHandler');
var RoomListSorter = require("../../../RoomListSorter"); var RoomListSorter = require("../../../RoomListSorter");
var UnreadStatus = require('../../../UnreadStatus'); var UnreadStatus = require('../../../UnreadStatus');
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
@ -38,13 +39,16 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
activityMap: null, activityMap: null,
isLoadingLeftRooms: false,
lists: {}, lists: {},
incomingCall: null,
} }
}, },
componentWillMount: function() { componentWillMount: function() {
var cli = MatrixClientPeg.get(); var cli = MatrixClientPeg.get();
cli.on("Room", this.onRoom); cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom);
cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.timeline", this.onRoomTimeline);
cli.on("Room.name", this.onRoomName); cli.on("Room.name", this.onRoomName);
cli.on("Room.tags", this.onRoomTags); cli.on("Room.tags", this.onRoomTags);
@ -66,7 +70,21 @@ module.exports = React.createClass({
this.tooltip = payload.tooltip; this.tooltip = payload.tooltip;
this._repositionTooltip(); this._repositionTooltip();
if (this.tooltip) this.tooltip.style.display = 'block'; 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;
} }
}, },
@ -88,7 +106,26 @@ module.exports = React.createClass({
}, },
onRoom: function(room) { onRoom: function(room) {
this.refreshRoomList(); this._delayedRefreshRoomList();
},
onDeleteRoom: function(roomId) {
this._delayedRefreshRoomList();
},
onArchivedHeaderClick: function(isHidden) {
if (!isHidden) {
var self = this;
this.setState({ isLoadingLeftRooms: true });
// we don't care about the response since it comes down via "Room"
// events.
MatrixClientPeg.get().syncLeftRooms().catch(function(err) {
console.error("Failed to sync left rooms: %s", err);
console.error(err);
}).finally(function() {
self.setState({ isLoadingLeftRooms: false });
});
}
}, },
onRoomTimeline: function(ev, room, toStartOfTimeline) { onRoomTimeline: function(ev, room, toStartOfTimeline) {
@ -127,22 +164,57 @@ module.exports = React.createClass({
}, },
onRoomName: function(room) { onRoomName: function(room) {
this.refreshRoomList(); this._delayedRefreshRoomList();
}, },
onRoomTags: function(event, room) { onRoomTags: function(event, room) {
this.refreshRoomList(); this._delayedRefreshRoomList();
}, },
onRoomStateEvents: function(ev, state) { onRoomStateEvents: function(ev, state) {
setTimeout(this.refreshRoomList, 0); this._delayedRefreshRoomList();
}, },
onRoomMemberName: function(ev, member) { onRoomMemberName: function(ev, member) {
setTimeout(this.refreshRoomList, 0); this._delayedRefreshRoomList();
},
_delayedRefreshRoomList: function() {
// There can be 1000s of JS SDK events when rooms are initially synced;
// we don't want to do lots of work rendering until things have settled.
// Therefore, keep a 1s refresh buffer which will refresh the room list
// at MOST once every 1s to prevent thrashing.
var MAX_REFRESH_INTERVAL_MS = 1000;
var self = this;
if (!self._lastRefreshRoomListTs) {
self.refreshRoomList(); // first refresh evar
}
else {
var timeWaitedMs = Date.now() - self._lastRefreshRoomListTs;
if (timeWaitedMs > MAX_REFRESH_INTERVAL_MS) {
clearTimeout(self._refreshRoomListTimerId);
self._refreshRoomListTimerId = null;
self.refreshRoomList(); // refreshed more than MAX_REFRESH_INTERVAL_MS ago
}
else {
// refreshed less than MAX_REFRESH_INTERVAL_MS ago, wait the difference
// if we aren't already waiting. If we are waiting then NOP, it will
// fire soon, promise!
if (!self._refreshRoomListTimerId) {
self._refreshRoomListTimerId = setTimeout(function() {
self.refreshRoomList();
}, 10 + MAX_REFRESH_INTERVAL_MS - timeWaitedMs); // 10 is a buffer amount
}
}
}
}, },
refreshRoomList: function() { refreshRoomList: function() {
// console.log("DEBUG: Refresh room list delta=%s ms",
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
// );
// TODO: rather than bluntly regenerating and re-sorting everything // TODO: rather than bluntly regenerating and re-sorting everything
// every time we see any kind of room change from the JS SDK // every time we see any kind of room change from the JS SDK
// we could do incremental updates on our copy of the state // we could do incremental updates on our copy of the state
@ -150,6 +222,7 @@ module.exports = React.createClass({
// us re-rendering all the sublists every time anything changes anywhere // us re-rendering all the sublists every time anything changes anywhere
// in the state of the client. // in the state of the client.
this.setState(this.getRoomLists()); this.setState(this.getRoomLists());
this._lastRefreshRoomListTs = Date.now();
}, },
getRoomLists: function() { getRoomLists: function() {
@ -168,9 +241,12 @@ module.exports = React.createClass({
if (me && me.membership == "invite") { if (me && me.membership == "invite") {
s.lists["im.vector.fake.invite"].push(room); s.lists["im.vector.fake.invite"].push(room);
} }
else if (me && me.membership === "leave") {
s.lists["im.vector.fake.archived"].push(room);
}
else { else {
var shouldShowRoom = ( var shouldShowRoom = (
me && (me.membership == "join") me && (me.membership == "join" || me.membership === "ban")
); );
// hiding conf rooms only ever toggles shouldShowRoom to false // hiding conf rooms only ever toggles shouldShowRoom to false
@ -212,10 +288,58 @@ module.exports = React.createClass({
return s; 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) { _repositionTooltip: function(e) {
if (this.tooltip && this.tooltip.parentElement) { if (this.tooltip && this.tooltip.parentElement) {
var scroll = ReactDOM.findDOMNode(this); 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";
} }
}, },
@ -234,7 +358,7 @@ module.exports = React.createClass({
var self = this; var self = this;
return ( return (
<GeminiScrollbar className="mx_RoomList_scrollbar" autoshow={true} onScroll={self._repositionTooltip}> <GeminiScrollbar className="mx_RoomList_scrollbar" autoshow={true} onScroll={ self._repositionTooltips }>
<div className="mx_RoomList"> <div className="mx_RoomList">
{ expandButton } { expandButton }
@ -244,6 +368,7 @@ module.exports = React.createClass({
order="recent" order="recent"
activityMap={ self.state.activityMap } activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } /> collapsed={ self.props.collapsed } />
<RoomSubList list={ self.state.lists['m.favourite'] } <RoomSubList list={ self.state.lists['m.favourite'] }
@ -254,6 +379,7 @@ module.exports = React.createClass({
order="manual" order="manual"
activityMap={ self.state.activityMap } activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } /> collapsed={ self.props.collapsed } />
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] } <RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
@ -263,6 +389,7 @@ module.exports = React.createClass({
order="recent" order="recent"
activityMap={ self.state.activityMap } activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } /> collapsed={ self.props.collapsed } />
{ Object.keys(self.state.lists).map(function(tagName) { { Object.keys(self.state.lists).map(function(tagName) {
@ -276,6 +403,7 @@ module.exports = React.createClass({
order="manual" order="manual"
activityMap={ self.state.activityMap } activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } /> collapsed={ self.props.collapsed } />
} }
@ -287,19 +415,23 @@ module.exports = React.createClass({
verb="demote" verb="demote"
editable={ true } editable={ true }
order="recent" order="recent"
bottommost={ self.state.lists['im.vector.fake.archived'].length === 0 }
activityMap={ self.state.activityMap } activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed } /> collapsed={ self.props.collapsed } />
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] } <RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
label="Historical" label="Historical"
editable={ false } editable={ false }
order="recent" order="recent"
bottommost={ true }
activityMap={ self.state.activityMap } activityMap={ self.state.activityMap }
selectedRoom={ self.props.selectedRoom } selectedRoom={ self.props.selectedRoom }
collapsed={ self.props.collapsed } /> collapsed={ self.props.collapsed }
alwaysShowHeader={ true }
startAsHidden={ true }
showSpinner={ self.state.isLoadingLeftRooms }
onHeaderClick= { self.onArchivedHeaderClick }
incomingCall={ self.state.incomingCall } />
</div> </div>
</GeminiScrollbar> </GeminiScrollbar>
); );

View File

@ -38,6 +38,7 @@ module.exports = React.createClass({
highlight: React.PropTypes.bool.isRequired, highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired, isInvite: React.PropTypes.bool.isRequired,
roomSubList: React.PropTypes.object.isRequired, roomSubList: React.PropTypes.object.isRequired,
incomingCall: React.PropTypes.object,
}, },
getInitialState: function() { getInitialState: function() {
@ -105,6 +106,12 @@ module.exports = React.createClass({
label = <RoomTooltip room={this.props.room}/>; label = <RoomTooltip room={this.props.room}/>;
} }
var incomingCallBox;
if (this.props.incomingCall) {
var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
incomingCallBox = <IncomingCallBox incomingCall={ this.props.incomingCall }/>;
}
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
// These props are injected by React DnD, // These props are injected by React DnD,
@ -120,6 +127,7 @@ module.exports = React.createClass({
{ badge } { badge }
</div> </div>
{ label } { label }
{ incomingCallBox }
</div> </div>
)); ));
} }

View File

@ -35,19 +35,13 @@ module.exports = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
this._trackedRoom = null;
if (this.props.room) { if (this.props.room) {
this._trackedRoom = this.props.room; this.showCall(this.props.room.roomId);
this.showCall(this._trackedRoom.roomId);
} }
else { else {
// XXX: why would we ever not have a this.props.room?
var call = CallHandler.getAnyActiveCall(); var call = CallHandler.getAnyActiveCall();
if (call) { 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); this.showCall(call.roomId);
} }
} }
@ -81,7 +75,7 @@ module.exports = React.createClass({
// and for the voice stream of screen captures // and for the voice stream of screen captures
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement()); 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 // if this call is a conf call, don't display local video as the
// conference will have us in it // conference will have us in it
this.getVideoView().getLocalVideoElement().style.display = ( this.getVideoView().getLocalVideoElement().style.display = (

View File

@ -21,87 +21,29 @@ var CallHandler = require("../../../CallHandler");
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'IncomingCallBox', 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() { onAnswerClick: function() {
dis.dispatch({ dis.dispatch({
action: 'answer', action: 'answer',
room_id: this.state.incomingCall.roomId room_id: this.props.incomingCall.roomId
}); });
}, },
onRejectClick: function() { onRejectClick: function() {
dis.dispatch({ dis.dispatch({
action: 'hangup', action: 'hangup',
room_id: this.state.incomingCall.roomId room_id: this.props.incomingCall.roomId
}); });
}, },
getRingAudio: function() {
return this.refs.ringAudio;
},
render: function() { render: function() {
// NB: This block MUST have a "key" so React doesn't clobber the elements
// between in-call / not-in-call.
var audioBlock = (
<audio ref="ringAudio" key="voip_ring_audio" loop>
<source src="media/ring.ogg" type="audio/ogg" />
<source src="media/ring.mp3" type="audio/mpeg" />
</audio>
);
if (!this.state.incomingCall || !this.state.incomingCall.roomId) { var room = this.props.incomingCall ? MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId) : null;
var caller = room ? room.name : "unknown";
return ( return (
<div> <div className="mx_IncomingCallBox" id="incomingCallBox">
{audioBlock}
</div>
);
}
var caller = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId).name;
return (
<div className="mx_IncomingCallBox">
{audioBlock}
<img className="mx_IncomingCallBox_chevron" src="img/chevron-left.png" width="9" height="16" /> <img className="mx_IncomingCallBox_chevron" src="img/chevron-left.png" width="9" height="16" />
<div className="mx_IncomingCallBox_title"> <div className="mx_IncomingCallBox_title">
Incoming { this.state.incomingCall ? this.state.incomingCall.type : '' } call from { caller } Incoming { this.props.incomingCall ? this.props.incomingCall.type : '' } call from { caller }
</div> </div>
<div className="mx_IncomingCallBox_buttons"> <div className="mx_IncomingCallBox_buttons">
<div className="mx_IncomingCallBox_buttons_cell"> <div className="mx_IncomingCallBox_buttons_cell">