Merge branch 'develop' into matthew/settings
This commit is contained in:
commit
b9ba4475b8
8 changed files with 533 additions and 314 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -48,6 +48,17 @@ module.exports = React.createClass({
|
|||
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() {
|
||||
var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null;
|
||||
return {
|
||||
|
@ -78,6 +89,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);
|
||||
|
@ -203,7 +218,7 @@ module.exports = React.createClass({
|
|||
if (!toStartOfTimeline &&
|
||||
(ev.getSender() !== MatrixClientPeg.get().credentials.userId)) {
|
||||
// update unread count when scrolled up
|
||||
if (this.savedScrollState.atBottom) {
|
||||
if (!this.state.searchResults && this.savedScrollState.atBottom) {
|
||||
currentUnread = 0;
|
||||
}
|
||||
else {
|
||||
|
@ -285,16 +300,7 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
if (this.refs.messagePanel) {
|
||||
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
|
||||
|
||||
messagePanel.addEventListener('drop', this.onDrop);
|
||||
messagePanel.addEventListener('dragover', this.onDragOver);
|
||||
messagePanel.addEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
messagePanel.addEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
|
||||
this.scrollToBottom();
|
||||
this.sendReadReceipt();
|
||||
this.fillSpace();
|
||||
this._initialiseMessagePanel();
|
||||
}
|
||||
|
||||
var call = CallHandler.getCallForRoom(this.props.roomId);
|
||||
|
@ -309,19 +315,34 @@ module.exports = React.createClass({
|
|||
this.onResize();
|
||||
},
|
||||
|
||||
_initialiseMessagePanel: function() {
|
||||
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
|
||||
this.refs.messagePanel.initialised = true;
|
||||
|
||||
messagePanel.addEventListener('drop', this.onDrop);
|
||||
messagePanel.addEventListener('dragover', this.onDragOver);
|
||||
messagePanel.addEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
messagePanel.addEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
|
||||
this.scrollToBottom();
|
||||
this.sendReadReceipt();
|
||||
this.fillSpace();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
// we need to initialise the messagepanel if we've just joined the
|
||||
// room. TODO: we really really ought to factor out messagepanel to a
|
||||
// separate component to avoid this ridiculous dance.
|
||||
if (!this.refs.messagePanel) return;
|
||||
|
||||
if (!this.refs.messagePanel.initialised) {
|
||||
this._initialiseMessagePanel();
|
||||
}
|
||||
|
||||
// after adding event tiles, we may need to tweak the scroll (either to
|
||||
// keep at the bottom of the timeline, or to maintain the view after
|
||||
// adding events to the top).
|
||||
|
||||
if (!this.refs.messagePanel) return;
|
||||
|
||||
if (this.state.searchResults) return;
|
||||
|
||||
this._restoreSavedScrollState();
|
||||
|
||||
// have to fill space in case we're accepting an invite
|
||||
if (!this.state.paginating) this.fillSpace();
|
||||
},
|
||||
|
||||
_paginateCompleted: function() {
|
||||
|
@ -333,39 +354,49 @@ module.exports = React.createClass({
|
|||
|
||||
// we might not have got enough results from the pagination
|
||||
// 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
|
||||
// request.
|
||||
//
|
||||
// returns true if a pagination request was started (or is still in progress)
|
||||
fillSpace: function() {
|
||||
if (!this.refs.messagePanel) return;
|
||||
if (this.state.searchResults) return; // TODO: paginate search results
|
||||
var messageWrapperScroll = this._getScrollNode();
|
||||
if (messageWrapperScroll.scrollTop < messageWrapperScroll.clientHeight && this.state.room.oldState.paginationToken) {
|
||||
// there's less than a screenful of messages left. Either wind back
|
||||
// the message cap (if there are enough events in the timeline to
|
||||
// do so), or fire off a pagination request.
|
||||
|
||||
this.oldScrollHeight = messageWrapperScroll.scrollHeight;
|
||||
|
||||
if (this.state.messageCap < 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);
|
||||
this.setState({messageCap: cap});
|
||||
} else {
|
||||
var cap = this.state.messageCap + PAGINATE_SIZE;
|
||||
if (DEBUG_SCROLL) console.log("starting paginate to cap", cap);
|
||||
this.setState({messageCap: cap, paginating: true});
|
||||
MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(this._paginateCompleted).done();
|
||||
return true;
|
||||
}
|
||||
if (messageWrapperScroll.scrollTop > messageWrapperScroll.clientHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
this.setState({messageCap: cap});
|
||||
} else if(this.state.room.oldState.paginationToken) {
|
||||
var cap = this.state.messageCap + PAGINATE_SIZE;
|
||||
if (DEBUG_SCROLL) console.log("starting paginate to cap", cap);
|
||||
this.setState({messageCap: cap, paginating: true});
|
||||
MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(this._paginateCompleted).done();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
onResendAllClick: function() {
|
||||
|
@ -418,14 +449,21 @@ module.exports = React.createClass({
|
|||
this.recentEventScroll = undefined;
|
||||
}
|
||||
|
||||
if (this.refs.messagePanel && !this.state.searchResults) {
|
||||
this.savedScrollState = this._calculateScrollState();
|
||||
if (DEBUG_SCROLL) console.log("Saved scroll state", this.savedScrollState);
|
||||
if (this.savedScrollState.atBottom && this.state.numUnreadMessages != 0) {
|
||||
this.setState({numUnreadMessages: 0});
|
||||
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();
|
||||
if (DEBUG_SCROLL) console.log("Saved scroll state", this.savedScrollState);
|
||||
if (this.savedScrollState.atBottom && this.state.numUnreadMessages != 0) {
|
||||
this.setState({numUnreadMessages: 0});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.state.paginating) this.fillSpace();
|
||||
if (!this.state.paginating && !this.state.searchInProgress) {
|
||||
this.fillSpace();
|
||||
}
|
||||
},
|
||||
|
||||
onDragOver: function(ev) {
|
||||
|
@ -477,76 +515,72 @@ 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.savedSearchScrollState = {atBottom: true};
|
||||
this.nextSearchBatch = null;
|
||||
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;
|
||||
|
||||
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");
|
||||
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,
|
||||
});
|
||||
self.nextSearchBatch = results.next_batch;
|
||||
}, function(error) {
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
|
@ -557,7 +591,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() {
|
||||
|
@ -572,57 +634,50 @@ 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);
|
||||
|
||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||
// XXX: can this ever happen? It will make the result count
|
||||
// not match the displayed count.
|
||||
continue;
|
||||
}
|
||||
|
||||
var eventId = mxEv.getId();
|
||||
|
||||
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(<li key={ roomId }><h1>Room: { cli.getRoom(roomId).name }</h1></li>);
|
||||
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 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(<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]) {
|
||||
var mxEv2 = new Matrix.MatrixEvent(resultList[i].context.events_before[0]);
|
||||
if (EventTile.haveTileForEvent(mxEv2)) {
|
||||
ret.push(<li key={mxEv.getId() + "-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>);
|
||||
}
|
||||
if (resultList[i].context.events_after[0]) {
|
||||
var mxEv2 = new Matrix.MatrixEvent(resultList[i].context.events_after[0]);
|
||||
if (EventTile.haveTileForEvent(mxEv2)) {
|
||||
ret.push(<li key={mxEv.getId() + "+1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
|
||||
}
|
||||
var ts1 = result.result.origin_server_ts;
|
||||
ret.push(<li key={ts1 + "-search"}><DateSeparator ts={ts1}/></li>); // 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(<li key={eventId+"-1"} data-scroll-token={eventId+"-1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ret.push(<li key={eventId+"+0"} data-scroll-token={eventId+"+0"}><EventTile mxEvent={mxEv} highlights={self.state.searchHighlights}/></li>);
|
||||
|
||||
if (result.context.events_after[0]) {
|
||||
var mxEv2 = new Matrix.MatrixEvent(result.context.events_after[0]);
|
||||
if (EventTile.haveTileForEvent(mxEv2)) {
|
||||
ret.push(<li key={eventId+"+1"} data-scroll-token={eventId+"+1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -671,15 +726,17 @@ module.exports = React.createClass({
|
|||
continuation = false;
|
||||
}
|
||||
|
||||
var eventId = mxEv.getId();
|
||||
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) {
|
||||
ret.unshift(dateSeparator);
|
||||
}
|
||||
++count;
|
||||
}
|
||||
this.lastEventTileCount = count;
|
||||
return ret;
|
||||
},
|
||||
|
||||
|
@ -849,7 +906,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onCancelClick: function() {
|
||||
this.setState(this.getInitialState());
|
||||
this.setState({editingRoomSettings: false});
|
||||
},
|
||||
|
||||
onLeaveClick: function() {
|
||||
|
@ -857,7 +914,19 @@ module.exports = React.createClass({
|
|||
action: 'leave_room',
|
||||
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) {
|
||||
|
@ -883,6 +952,13 @@ module.exports = React.createClass({
|
|||
this.setState({ searching: true });
|
||||
},
|
||||
|
||||
onCancelSearchClick: function () {
|
||||
this.setState({
|
||||
searching: false,
|
||||
searchResults: null,
|
||||
});
|
||||
},
|
||||
|
||||
onConferenceNotificationClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
|
@ -910,12 +986,6 @@ module.exports = React.createClass({
|
|||
// pixel_offset gives the number of pixels between the bottom of the event
|
||||
// and the bottom of the container.
|
||||
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);
|
||||
if (idx === null) {
|
||||
// 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.
|
||||
console.log("Refusing to scroll to unknown event "+eventId);
|
||||
scrollNode.scrollTop = 0;
|
||||
this._getScrollNode().scrollTop = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -943,14 +1013,88 @@ module.exports = React.createClass({
|
|||
this.setState({messageCap: minCap});
|
||||
}
|
||||
|
||||
var node = this.eventNodes[eventId];
|
||||
if (node === null) {
|
||||
// getEventTiles should have sorted this out when we set the
|
||||
// messageCap, so this is weird.
|
||||
console.error("No node for event, even after rolling back messageCap");
|
||||
// the scrollTokens on our DOM nodes are the event IDs, so we can pass
|
||||
// eventId directly into _scrollToToken.
|
||||
this._scrollToToken(eventId, pixelOffset);
|
||||
},
|
||||
|
||||
_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;
|
||||
}
|
||||
|
||||
var scrollNode = this._getScrollNode();
|
||||
var messageWrapper = this.refs.messagePanel;
|
||||
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
|
||||
|
@ -962,59 +1106,11 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
_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
|
||||
// restored when we switch back to it
|
||||
getScrollState: function() {
|
||||
|
@ -1022,11 +1118,17 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
restoreScrollState: function(scrollState) {
|
||||
if (!this.refs.messagePanel) return;
|
||||
|
||||
if(scrollState.atBottom) {
|
||||
// we were at the bottom before. Ideally we'd scroll to the
|
||||
// 'read-up-to' mark here.
|
||||
} else if (scrollState.lastDisplayedEvent) {
|
||||
this.scrollToEvent(scrollState.lastDisplayedEvent,
|
||||
} else if (scrollState.lastDisplayedScrollToken) {
|
||||
// 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);
|
||||
}
|
||||
},
|
||||
|
@ -1222,7 +1324,7 @@ module.exports = React.createClass({
|
|||
aux = <Loader/>;
|
||||
}
|
||||
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;
|
||||
|
@ -1249,21 +1351,29 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
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 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 = {
|
||||
searchTerm : this.state.searchTerm,
|
||||
searchScope : this.state.searchScope,
|
||||
searchCount : this.state.searchCount,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
@ -1304,8 +1414,18 @@ module.exports = React.createClass({
|
|||
|
||||
return (
|
||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") }>
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo} editing={this.state.editingRoomSettings} onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick} onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} onLeaveClick={this.onLeaveClick} />
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
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 }
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}/>
|
||||
|
@ -1314,7 +1434,7 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
<GeminiScrollbar autoshow={true} ref="messagePanel" className="mx_RoomView_messagePanel" onScroll={ this.onMessageListScroll }>
|
||||
<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>
|
||||
{this.getEventTiles()}
|
||||
|
|
|
@ -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 =
|
||||
<div className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||
|
|
|
@ -108,8 +108,10 @@ module.exports = React.createClass({
|
|||
// <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />
|
||||
|
||||
var searchStatus;
|
||||
if (this.props.searchInfo && this.props.searchInfo.searchTerm) {
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus"> ({ this.props.searchInfo.searchCount } results)</div>;
|
||||
// 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 = <div className="mx_RoomHeader_searchStatus"> (~{ this.props.searchInfo.searchCount } results)</div>;
|
||||
}
|
||||
|
||||
name =
|
||||
|
@ -134,7 +136,17 @@ module.exports = React.createClass({
|
|||
if (this.props.onLeaveClick) {
|
||||
leave_button =
|
||||
<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>;
|
||||
}
|
||||
|
||||
|
@ -152,6 +164,7 @@ module.exports = React.createClass({
|
|||
{cancel_button}
|
||||
{save_button}
|
||||
<div className="mx_RoomHeader_rightRow">
|
||||
{ forget_button }
|
||||
{ leave_button }
|
||||
<div className="mx_RoomHeader_button">
|
||||
<img src="img/search.svg" title="Search" alt="Search" width="21" height="19" onClick={this.props.onSearchClick}/>
|
||||
|
|
|
@ -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");
|
||||
|
@ -38,13 +39,16 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
activityMap: null,
|
||||
isLoadingLeftRooms: false,
|
||||
lists: {},
|
||||
incomingCall: null,
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
var cli = MatrixClientPeg.get();
|
||||
cli.on("Room", this.onRoom);
|
||||
cli.on("deleteRoom", this.onDeleteRoom);
|
||||
cli.on("Room.timeline", this.onRoomTimeline);
|
||||
cli.on("Room.name", this.onRoomName);
|
||||
cli.on("Room.tags", this.onRoomTags);
|
||||
|
@ -66,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;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -88,7 +106,26 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
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) {
|
||||
|
@ -127,22 +164,57 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onRoomName: function(room) {
|
||||
this.refreshRoomList();
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onRoomTags: function(event, room) {
|
||||
this.refreshRoomList();
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
setTimeout(this.refreshRoomList, 0);
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
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() {
|
||||
// console.log("DEBUG: Refresh room list delta=%s ms",
|
||||
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
|
||||
// );
|
||||
|
||||
// TODO: rather than bluntly regenerating and re-sorting everything
|
||||
// every time we see any kind of room change from the JS SDK
|
||||
// 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
|
||||
// in the state of the client.
|
||||
this.setState(this.getRoomLists());
|
||||
this._lastRefreshRoomListTs = Date.now();
|
||||
},
|
||||
|
||||
getRoomLists: function() {
|
||||
|
@ -168,9 +241,12 @@ module.exports = React.createClass({
|
|||
if (me && me.membership == "invite") {
|
||||
s.lists["im.vector.fake.invite"].push(room);
|
||||
}
|
||||
else if (me && me.membership === "leave") {
|
||||
s.lists["im.vector.fake.archived"].push(room);
|
||||
}
|
||||
else {
|
||||
var shouldShowRoom = (
|
||||
me && (me.membership == "join")
|
||||
me && (me.membership == "join" || me.membership === "ban")
|
||||
);
|
||||
|
||||
// hiding conf rooms only ever toggles shouldShowRoom to false
|
||||
|
@ -212,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";
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -234,7 +358,7 @@ module.exports = React.createClass({
|
|||
var self = this;
|
||||
|
||||
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">
|
||||
{ expandButton }
|
||||
|
||||
|
@ -244,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 } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['m.favourite'] }
|
||||
|
@ -254,6 +379,7 @@ module.exports = React.createClass({
|
|||
order="manual"
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
|
||||
|
@ -263,6 +389,7 @@ module.exports = React.createClass({
|
|||
order="recent"
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
{ Object.keys(self.state.lists).map(function(tagName) {
|
||||
|
@ -276,6 +403,7 @@ module.exports = React.createClass({
|
|||
order="manual"
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
}
|
||||
|
@ -287,19 +415,23 @@ module.exports = React.createClass({
|
|||
verb="demote"
|
||||
editable={ true }
|
||||
order="recent"
|
||||
bottommost={ self.state.lists['im.vector.fake.archived'].length === 0 }
|
||||
activityMap={ self.state.activityMap }
|
||||
selectedRoom={ self.props.selectedRoom }
|
||||
incomingCall={ self.state.incomingCall }
|
||||
collapsed={ self.props.collapsed } />
|
||||
|
||||
<RoomSubList list={ self.state.lists['im.vector.fake.archived'] }
|
||||
label="Historical"
|
||||
editable={ false }
|
||||
order="recent"
|
||||
bottommost={ true }
|
||||
activityMap={ self.state.activityMap }
|
||||
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>
|
||||
</GeminiScrollbar>
|
||||
);
|
||||
|
|
|
@ -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 = <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');
|
||||
|
||||
// These props are injected by React DnD,
|
||||
|
@ -120,6 +127,7 @@ module.exports = React.createClass({
|
|||
{ badge }
|
||||
</div>
|
||||
{ label }
|
||||
{ incomingCallBox }
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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 = (
|
||||
<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) {
|
||||
return (
|
||||
<div>
|
||||
{audioBlock}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<div className="mx_IncomingCallBox">
|
||||
{audioBlock}
|
||||
<div className="mx_IncomingCallBox" id="incomingCallBox">
|
||||
<img className="mx_IncomingCallBox_chevron" src="img/chevron-left.png" width="9" height="16" />
|
||||
<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 className="mx_IncomingCallBox_buttons">
|
||||
<div className="mx_IncomingCallBox_buttons_cell">
|
||||
|
|
Loading…
Reference in a new issue