Land refactored roomview branch 'rav/roomview_works'.
This branch has been sitting around a while; it includes a substantial refactor of RoomView (into separate MessagePanel and TimelinePanel), as well as a number of fixes.
This commit is contained in:
commit
1959b03104
9 changed files with 1017 additions and 621 deletions
|
@ -31,9 +31,11 @@ module.exports.components['structures.login.Login'] = require('./components/stru
|
||||||
module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
|
module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
|
||||||
module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
|
module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
|
||||||
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
|
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
|
||||||
|
module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel');
|
||||||
module.exports.components['structures.RoomStatusBar'] = require('./components/structures/RoomStatusBar');
|
module.exports.components['structures.RoomStatusBar'] = require('./components/structures/RoomStatusBar');
|
||||||
module.exports.components['structures.RoomView'] = require('./components/structures/RoomView');
|
module.exports.components['structures.RoomView'] = require('./components/structures/RoomView');
|
||||||
module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel');
|
module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel');
|
||||||
|
module.exports.components['structures.TimelinePanel'] = require('./components/structures/TimelinePanel');
|
||||||
module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar');
|
module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar');
|
||||||
module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings');
|
module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings');
|
||||||
module.exports.components['views.avatars.BaseAvatar'] = require('./components/views/avatars/BaseAvatar');
|
module.exports.components['views.avatars.BaseAvatar'] = require('./components/views/avatars/BaseAvatar');
|
||||||
|
@ -70,6 +72,7 @@ module.exports.components['views.messages.TextualEvent'] = require('./components
|
||||||
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
|
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
|
||||||
module.exports.components['views.room_settings.AliasSettings'] = require('./components/views/room_settings/AliasSettings');
|
module.exports.components['views.room_settings.AliasSettings'] = require('./components/views/room_settings/AliasSettings');
|
||||||
module.exports.components['views.room_settings.ColorSettings'] = require('./components/views/room_settings/ColorSettings');
|
module.exports.components['views.room_settings.ColorSettings'] = require('./components/views/room_settings/ColorSettings');
|
||||||
|
module.exports.components['views.rooms.AuxPanel'] = require('./components/views/rooms/AuxPanel');
|
||||||
module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile');
|
module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile');
|
||||||
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile');
|
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile');
|
||||||
module.exports.components['views.rooms.InviteMemberList'] = require('./components/views/rooms/InviteMemberList');
|
module.exports.components['views.rooms.InviteMemberList'] = require('./components/views/rooms/InviteMemberList');
|
||||||
|
|
305
src/components/structures/MessagePanel.js
Normal file
305
src/components/structures/MessagePanel.js
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var sdk = require('../../index');
|
||||||
|
|
||||||
|
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||||
|
*/
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'MessagePanel',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// true to give the component a 'display: none' style.
|
||||||
|
hidden: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// the list of MatrixEvents to display
|
||||||
|
events: React.PropTypes.array.isRequired,
|
||||||
|
|
||||||
|
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||||
|
highlightedEventId: React.PropTypes.string,
|
||||||
|
|
||||||
|
// event after which we should show a read marker
|
||||||
|
readMarkerEventId: React.PropTypes.string,
|
||||||
|
|
||||||
|
// the userid of our user. This is used to suppress the read marker
|
||||||
|
// for pending messages.
|
||||||
|
ourUserId: React.PropTypes.string,
|
||||||
|
|
||||||
|
// true to suppress the date at the start of the timeline
|
||||||
|
suppressFirstDateSeparator: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// true if updates to the event list should cause the scroll panel to
|
||||||
|
// scroll down when we are at the bottom of the window. See ScrollPanel
|
||||||
|
// for more details.
|
||||||
|
stickyBottom: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// callback which is called when the panel is scrolled.
|
||||||
|
onScroll: React.PropTypes.func,
|
||||||
|
|
||||||
|
// callback which is called when more content is needed.
|
||||||
|
onFillRequest: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
// the event after which we put a visible unread marker on the last
|
||||||
|
// render cycle; null if readMarkerVisible was false or the RM was
|
||||||
|
// suppressed (eg because it was at the end of the timeline)
|
||||||
|
this.currentReadMarkerEventId = null;
|
||||||
|
|
||||||
|
// the event after which we are showing a disappearing read marker
|
||||||
|
// animation
|
||||||
|
this.currentGhostEventId = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/* get the DOM node representing the given event */
|
||||||
|
getNodeForEventId: function(eventId) {
|
||||||
|
if (!this.eventNodes) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.eventNodes[eventId];
|
||||||
|
},
|
||||||
|
|
||||||
|
/* return true if the content is fully scrolled down right now; else false.
|
||||||
|
*/
|
||||||
|
isAtBottom: function() {
|
||||||
|
return this.refs.scrollPanel
|
||||||
|
&& this.refs.scrollPanel.isAtBottom();
|
||||||
|
},
|
||||||
|
|
||||||
|
/* get the current scroll state. See ScrollPanel.getScrollState for
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* returns null if we are not mounted.
|
||||||
|
*/
|
||||||
|
getScrollState: function() {
|
||||||
|
if (!this.refs.scrollPanel) { return null; }
|
||||||
|
return this.refs.scrollPanel.getScrollState();
|
||||||
|
},
|
||||||
|
|
||||||
|
/* jump to the bottom of the content.
|
||||||
|
*/
|
||||||
|
scrollToBottom: function() {
|
||||||
|
if (this.refs.scrollPanel) {
|
||||||
|
this.refs.scrollPanel.scrollToBottom();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* jump to the given event id.
|
||||||
|
*
|
||||||
|
* pixelOffset gives the number of pixels between the bottom of the node
|
||||||
|
* and the bottom of the container. If undefined, it will put the node
|
||||||
|
* in the middle of the container.
|
||||||
|
*/
|
||||||
|
scrollToEvent: function(eventId, pixelOffset) {
|
||||||
|
if (this.refs.scrollPanel) {
|
||||||
|
this.refs.scrollPanel.scrollToToken(eventId, pixelOffset);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* check the scroll state and send out pagination requests if necessary.
|
||||||
|
*/
|
||||||
|
checkFillState: function() {
|
||||||
|
if (this.refs.scrollPanel) {
|
||||||
|
this.refs.scrollPanel.checkFillState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getEventTiles: function() {
|
||||||
|
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
|
|
||||||
|
this.eventNodes = {};
|
||||||
|
|
||||||
|
var i;
|
||||||
|
|
||||||
|
// first figure out which is the last event in the list which we're
|
||||||
|
// actually going to show; this allows us to behave slightly
|
||||||
|
// differently for the last event in the list.
|
||||||
|
for (i = this.props.events.length-1; i >= 0; i--) {
|
||||||
|
var mxEv = this.props.events[i];
|
||||||
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var lastShownEventIndex = i;
|
||||||
|
|
||||||
|
var ret = [];
|
||||||
|
|
||||||
|
var prevEvent = null; // the last event we showed
|
||||||
|
|
||||||
|
// assume there is no read marker until proven otherwise
|
||||||
|
var readMarkerVisible = false;
|
||||||
|
|
||||||
|
for (i = 0; i < this.props.events.length; i++) {
|
||||||
|
var mxEv = this.props.events[i];
|
||||||
|
var wantTile = true;
|
||||||
|
var eventId = mxEv.getId();
|
||||||
|
|
||||||
|
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||||
|
wantTile = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var last = (i == lastShownEventIndex);
|
||||||
|
|
||||||
|
if (wantTile) {
|
||||||
|
ret.push(this._getTilesForEvent(prevEvent, mxEv, last));
|
||||||
|
prevEvent = mxEv;
|
||||||
|
} else if (!mxEv.status) {
|
||||||
|
// if we aren't showing the event, put in a dummy scroll token anyway, so
|
||||||
|
// that we can scroll to the right place.
|
||||||
|
ret.push(<li key={eventId} data-scroll-token={eventId}/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventId == this.props.readMarkerEventId && i < lastShownEventIndex) {
|
||||||
|
// suppress the read marker if the next event is sent by us; this
|
||||||
|
// is a nonsensical and temporary situation caused by the delay between
|
||||||
|
// us sending a message and receiving the synthesized receipt.
|
||||||
|
var nextEvent = this.props.events[i+1];
|
||||||
|
if (nextEvent.sender && nextEvent.sender.userId != this.props.ourUserId) {
|
||||||
|
ret.push(this._getReadMarkerTile());
|
||||||
|
readMarkerVisible = true;
|
||||||
|
}
|
||||||
|
} else if (eventId == this.currentReadMarkerEventId && !this.currentGhostEventId) {
|
||||||
|
// there is currently a read-up-to marker at this point, but no
|
||||||
|
// more. Show an animation of it disappearing.
|
||||||
|
ret.push(this._getReadMarkerGhostTile());
|
||||||
|
this.currentGhostEventId = eventId;
|
||||||
|
} else if (eventId == this.currentGhostEventId) {
|
||||||
|
// if we're showing an animation, continue to show it.
|
||||||
|
ret.push(this._getReadMarkerGhostTile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentReadMarkerEventId = readMarkerVisible ? this.props.readMarkerEventId : null;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getTilesForEvent: function(prevEvent, mxEv, last) {
|
||||||
|
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
|
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
|
var ret = [];
|
||||||
|
|
||||||
|
// is this a continuation of the previous message?
|
||||||
|
var continuation = false;
|
||||||
|
if (prevEvent !== null && prevEvent.sender && mxEv.sender
|
||||||
|
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||||
|
&& mxEv.getType() == prevEvent.getType()) {
|
||||||
|
continuation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// do we need a date separator since the last event?
|
||||||
|
var ts1 = mxEv.getTs();
|
||||||
|
if ((prevEvent == null && !this.props.suppressFirstDateSeparator) ||
|
||||||
|
(prevEvent != null &&
|
||||||
|
new Date(prevEvent.getTs()).toDateString()
|
||||||
|
!== new Date(ts1).toDateString())) {
|
||||||
|
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>;
|
||||||
|
ret.push(dateSeparator);
|
||||||
|
continuation = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventId = mxEv.getId();
|
||||||
|
var highlight = (eventId == this.props.highlightedEventId);
|
||||||
|
|
||||||
|
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||||
|
// Local echos have a send "status".
|
||||||
|
var scrollToken = mxEv.status ? undefined : eventId;
|
||||||
|
|
||||||
|
ret.push(
|
||||||
|
<li key={eventId}
|
||||||
|
ref={this._collectEventNode.bind(this, eventId)}
|
||||||
|
data-scroll-token={scrollToken}>
|
||||||
|
<EventTile mxEvent={mxEv} continuation={continuation}
|
||||||
|
last={last} isSelectedEvent={highlight}
|
||||||
|
onImageLoad={this._onImageLoad} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getReadMarkerTile: function() {
|
||||||
|
var hr;
|
||||||
|
hr = <hr className="mx_RoomView_myReadMarker"
|
||||||
|
style={{opacity: 1, width: '99%'}}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key="_readupto"
|
||||||
|
className="mx_RoomView_myReadMarker_container">
|
||||||
|
{hr}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_getReadMarkerGhostTile: function() {
|
||||||
|
// reset the ghostEventId when the animation finishes, so that
|
||||||
|
// we can make a new one (and so that we don't run the
|
||||||
|
// animation code every time we render)
|
||||||
|
var completeFunc = () => {
|
||||||
|
this.currentGhostEventId = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
var hr = <hr className="mx_RoomView_myReadMarker"
|
||||||
|
style={{opacity: 1, width: '99%'}}
|
||||||
|
ref={function(n) {
|
||||||
|
Velocity(n, {opacity: '0', width: '10%'},
|
||||||
|
{duration: 400, easing: 'easeInSine',
|
||||||
|
delay: 1000, complete: completeFunc});
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
// give it a key which depends on the event id. That will ensure that
|
||||||
|
// we get a new DOM node (restarting the animation) when the ghost
|
||||||
|
// moves to a different event.
|
||||||
|
return (
|
||||||
|
<li key={"_readuptoghost_"+this.currentGhostEventId}
|
||||||
|
className="mx_RoomView_myReadMarker_container">
|
||||||
|
{hr}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_collectEventNode: function(eventId, node) {
|
||||||
|
this.eventNodes[eventId] = node;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// once images in the events load, make the scrollPanel check the
|
||||||
|
// scroll offsets.
|
||||||
|
_onImageLoad: function() {
|
||||||
|
var scrollPanel = this.refs.messagePanel;
|
||||||
|
if (scrollPanel) {
|
||||||
|
scrollPanel.checkScroll();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||||
|
return (
|
||||||
|
<ScrollPanel ref="scrollPanel" className="mx_RoomView_messagePanel"
|
||||||
|
onScroll={ this.props.onScroll }
|
||||||
|
onFillRequest={ this.props.onFillRequest }
|
||||||
|
style={ this.props.hidden ? { display: 'none' } : {} }
|
||||||
|
stickyBottom={ this.props.stickyBottom }>
|
||||||
|
{this._getEventTiles()}
|
||||||
|
</ScrollPanel>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -61,11 +61,13 @@ module.exports = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
syncState: MatrixClientPeg.get().getSyncState(),
|
syncState: MatrixClientPeg.get().getSyncState(),
|
||||||
|
whoisTypingString: WhoIsTyping.whoIsTypingString(this.props.room),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
MatrixClientPeg.get().on("sync", this.onSyncStateChange);
|
||||||
|
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function(prevProps, prevState) {
|
componentDidUpdate: function(prevProps, prevState) {
|
||||||
|
@ -76,8 +78,10 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
|
||||||
if (MatrixClientPeg.get()) {
|
var client = MatrixClientPeg.get();
|
||||||
MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange);
|
if (client) {
|
||||||
|
client.removeListener("sync", this.onSyncStateChange);
|
||||||
|
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -90,6 +94,12 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRoomMemberTyping: function(ev, member) {
|
||||||
|
this.setState({
|
||||||
|
whoisTypingString: WhoIsTyping.whoIsTypingString(this.props.room),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// determine if we need to call onResize
|
// determine if we need to call onResize
|
||||||
_checkForResize: function(prevProps, prevState) {
|
_checkForResize: function(prevProps, prevState) {
|
||||||
// figure out the old height and the new height of the status bar. We
|
// figure out the old height and the new height of the status bar. We
|
||||||
|
@ -235,7 +245,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var typingString = WhoIsTyping.whoIsTypingString(this.props.room);
|
var typingString = this.state.whoisTypingString;
|
||||||
if (typingString) {
|
if (typingString) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomStatusBar_typingBar">
|
<div className="mx_RoomStatusBar_typingBar">
|
||||||
|
|
|
@ -15,18 +15,15 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO: This component is enormous! There's several things which could stand-alone:
|
// TODO: This component is enormous! There's several things which could stand-alone:
|
||||||
// - Aux component
|
|
||||||
// - Search results component
|
// - Search results component
|
||||||
// - Drag and drop
|
// - Drag and drop
|
||||||
// - File uploading - uploadFile()
|
// - File uploading - uploadFile()
|
||||||
// - Timeline component (alllll the logic in getEventTiles())
|
|
||||||
|
|
||||||
var React = require("react");
|
var React = require("react");
|
||||||
var ReactDOM = require("react-dom");
|
var ReactDOM = require("react-dom");
|
||||||
var q = require("q");
|
var q = require("q");
|
||||||
var classNames = require("classnames");
|
var classNames = require("classnames");
|
||||||
var Matrix = require("matrix-js-sdk");
|
var Matrix = require("matrix-js-sdk");
|
||||||
var EventTimeline = Matrix.EventTimeline;
|
|
||||||
|
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
var ContentMessages = require("../../ContentMessages");
|
var ContentMessages = require("../../ContentMessages");
|
||||||
|
@ -42,14 +39,9 @@ var dis = require("../../dispatcher");
|
||||||
var Tinter = require("../../Tinter");
|
var Tinter = require("../../Tinter");
|
||||||
var rate_limited_func = require('../../ratelimitedfunc');
|
var rate_limited_func = require('../../ratelimitedfunc');
|
||||||
|
|
||||||
var PAGINATE_SIZE = 20;
|
var DEBUG = false;
|
||||||
var INITIAL_SIZE = 20;
|
|
||||||
var SEND_READ_RECEIPT_DELAY = 2000;
|
|
||||||
var TIMELINE_CAP = 1000; // the most events to show in a timeline
|
|
||||||
|
|
||||||
var DEBUG_SCROLL = false;
|
if (DEBUG) {
|
||||||
|
|
||||||
if (DEBUG_SCROLL) {
|
|
||||||
// using bind means that we get to keep useful line numbers in the console
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
var debuglog = console.log.bind(console);
|
var debuglog = console.log.bind(console);
|
||||||
} else {
|
} else {
|
||||||
|
@ -81,17 +73,11 @@ module.exports = React.createClass({
|
||||||
highlightedEventId: React.PropTypes.string,
|
highlightedEventId: React.PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
/* properties in RoomView objects include:
|
|
||||||
*
|
|
||||||
* 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 {
|
||||||
room: room,
|
room: room,
|
||||||
events: [],
|
roomLoading: !room,
|
||||||
canBackPaginate: true,
|
|
||||||
paginating: room != null,
|
|
||||||
editingRoomSettings: false,
|
editingRoomSettings: false,
|
||||||
uploadingRoomSettings: false,
|
uploadingRoomSettings: false,
|
||||||
numUnreadMessages: 0,
|
numUnreadMessages: 0,
|
||||||
|
@ -100,29 +86,24 @@ module.exports = React.createClass({
|
||||||
searchResults: null,
|
searchResults: null,
|
||||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||||
callState: null,
|
callState: null,
|
||||||
timelineLoading: true, // track whether our room timeline is loading
|
|
||||||
guestsCanJoin: false,
|
guestsCanJoin: false,
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
readMarkerEventId: room ? room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId) : null,
|
|
||||||
readMarkerGhostEventId: undefined,
|
|
||||||
|
|
||||||
// this is true if we are fully scrolled-down, and are looking at
|
// this is true if we are fully scrolled-down, and are looking at
|
||||||
// the end of the live timeline. It has the effect of hiding the
|
// the end of the live timeline. It has the effect of hiding the
|
||||||
// 'scroll to bottom' knob, among a couple of other things.
|
// 'scroll to bottom' knob, among a couple of other things.
|
||||||
atEndOfLiveTimeline: true,
|
atEndOfLiveTimeline: true,
|
||||||
|
|
||||||
|
auxPanelMaxHeight: undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this.last_rr_sent_event_id = undefined;
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on("Room", this.onRoom);
|
MatrixClientPeg.get().on("Room", this.onRoom);
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
|
||||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||||
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
||||||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
|
||||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
|
||||||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||||
// xchat-style tab complete, add a colon if tab
|
// xchat-style tab complete, add a colon if tab
|
||||||
// completing at the start of the text
|
// completing at the start of the text
|
||||||
|
@ -136,13 +117,6 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// to make the timeline load work correctly, build up a chain of promises which
|
|
||||||
// take us through the necessary steps.
|
|
||||||
|
|
||||||
// First of all, we may need to load the room. Construct a promise
|
|
||||||
// which resolves to the Room object.
|
|
||||||
var roomProm;
|
|
||||||
|
|
||||||
// if this is an unknown room then we're in one of three states:
|
// if this is an unknown room then we're in one of three states:
|
||||||
// - This is a room we can peek into (search engine) (we can /peek)
|
// - This is a room we can peek into (search engine) (we can /peek)
|
||||||
// - This is a room we can publicly join or were invited to. (we can /join)
|
// - This is a room we can publicly join or were invited to. (we can /join)
|
||||||
|
@ -153,93 +127,28 @@ module.exports = React.createClass({
|
||||||
if (!this.state.room) {
|
if (!this.state.room) {
|
||||||
console.log("Attempting to peek into room %s", this.props.roomId);
|
console.log("Attempting to peek into room %s", this.props.roomId);
|
||||||
|
|
||||||
roomProm = MatrixClientPeg.get().peekInRoom(this.props.roomId).then((room) => {
|
MatrixClientPeg.get().peekInRoom(this.props.roomId).then((room) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
room: room
|
room: room,
|
||||||
|
roomLoading: false,
|
||||||
});
|
});
|
||||||
return room;
|
this._onRoomLoaded(room);
|
||||||
});
|
}, (err) => {
|
||||||
} else {
|
// This won't necessarily be a MatrixError, but we duck-type
|
||||||
roomProm = q(this.state.room);
|
// here and say if it's got an 'errcode' key with the right value,
|
||||||
}
|
// it means we can't peek.
|
||||||
|
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
|
||||||
// Next, load the timeline.
|
// This is fine: the room just isn't peekable (we assume).
|
||||||
roomProm.then((room) => {
|
this.setState({
|
||||||
this._calculatePeekRules(room);
|
roomLoading: false,
|
||||||
return this._initTimeline(this.props);
|
});
|
||||||
}).catch((err) => {
|
|
||||||
// This won't necessarily be a MatrixError, but we duck-type
|
|
||||||
// here and say if it's got an 'errcode' key with the right value,
|
|
||||||
// it means we can't peek.
|
|
||||||
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
|
|
||||||
// This is fine: the room just isn't peekable (we assume).
|
|
||||||
this.setState({
|
|
||||||
timelineLoading: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}).done();
|
|
||||||
},
|
|
||||||
|
|
||||||
_initTimeline: function(props) {
|
|
||||||
var initialEvent = props.eventId;
|
|
||||||
var pixelOffset = props.eventPixelOffset;
|
|
||||||
return this._loadTimeline(initialEvent, pixelOffset);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (re)-load the event timeline, and initialise the scroll state, centered
|
|
||||||
* around the given event.
|
|
||||||
*
|
|
||||||
* @param {string?} eventId the event to focus on. If undefined, will
|
|
||||||
* scroll to the bottom of the room.
|
|
||||||
*
|
|
||||||
* @param {number?} pixelOffset offset to position the given event at
|
|
||||||
* (pixels from the bottom of the view). If undefined, will put the
|
|
||||||
* event in the middle of the view.
|
|
||||||
*
|
|
||||||
* returns a promise which will resolve when the load completes.
|
|
||||||
*/
|
|
||||||
_loadTimeline: function(eventId, pixelOffset) {
|
|
||||||
// TODO: we could optimise this, by not resetting the window if the
|
|
||||||
// event is in the current window (though it's not obvious how we can
|
|
||||||
// tell if the current window is on the live event stream)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
events: [],
|
|
||||||
searchResults: null, // we may have arrived here by clicking on a
|
|
||||||
// search result. Hide the results.
|
|
||||||
timelineLoading: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this._timelineWindow = new Matrix.TimelineWindow(
|
|
||||||
MatrixClientPeg.get(), this.state.room,
|
|
||||||
{windowLimit: TIMELINE_CAP});
|
|
||||||
|
|
||||||
return this._timelineWindow.load(eventId, INITIAL_SIZE).then(() => {
|
|
||||||
debuglog("RoomView: timeline loaded");
|
|
||||||
this._onTimelineUpdated(true);
|
|
||||||
}).finally(() => {
|
|
||||||
this.setState({
|
|
||||||
timelineLoading: false,
|
|
||||||
}, () => {
|
|
||||||
// initialise the scroll state of the message panel
|
|
||||||
if (!this.refs.messagePanel) {
|
|
||||||
// this shouldn't happen.
|
|
||||||
console.log("can't initialise scroll state because " +
|
|
||||||
"messagePanel didn't load");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (eventId) {
|
|
||||||
this.refs.messagePanel.scrollToToken(eventId, pixelOffset);
|
|
||||||
} else {
|
} else {
|
||||||
this.refs.messagePanel.scrollToBottom();
|
throw err;
|
||||||
}
|
}
|
||||||
|
}).done();
|
||||||
this.sendReadReceipt();
|
} else {
|
||||||
});
|
this._onRoomLoaded(this.state.room);
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -264,11 +173,8 @@ module.exports = React.createClass({
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().removeListener("Room.redaction", this.onRoomRedaction);
|
|
||||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||||
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
||||||
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
|
||||||
MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
|
||||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,15 +228,6 @@ module.exports = React.createClass({
|
||||||
callState: callState
|
callState: callState
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
|
||||||
case 'user_activity':
|
|
||||||
case 'user_activity_end':
|
|
||||||
// we could treat user_activity_end differently and not
|
|
||||||
// send receipts for messages that have arrived between
|
|
||||||
// the actual user activity and the time they stopped
|
|
||||||
// being active, but let's see if this is actually
|
|
||||||
// necessary.
|
|
||||||
this.sendReadReceipt();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -341,9 +238,8 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newProps.eventId != this.props.eventId) {
|
if (newProps.eventId != this.props.eventId) {
|
||||||
console.log("RoomView switching to eventId " + newProps.eventId +
|
// when we change focussed event id, hide the search results.
|
||||||
" (was " + this.props.eventId + ")");
|
this.setState({searchResults: null});
|
||||||
return this._initTimeline(newProps);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -372,26 +268,12 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tell the messagepanel to go paginate itself. This in turn will cause
|
|
||||||
// onMessageListFillRequest to be called, which will call
|
|
||||||
// _onTimelineUpdated, which will update the state with the new event -
|
|
||||||
// so there is no need update the state here.
|
|
||||||
//
|
|
||||||
if (this.refs.messagePanel) {
|
|
||||||
this.refs.messagePanel.checkFillState();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomRedaction: function(ev, room) {
|
// called when state.room is first initialised (either at initial load,
|
||||||
if (this.unmounted) return;
|
// after a successful peek, or after we join the room).
|
||||||
|
_onRoomLoaded: function(room) {
|
||||||
// ignore events for other rooms
|
this._calculatePeekRules(room);
|
||||||
if (room.roomId != this.props.roomId) return;
|
|
||||||
|
|
||||||
// we could skip an update if the event isn't in our timeline,
|
|
||||||
// but that's probably an early optimisation.
|
|
||||||
this.forceUpdate();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_calculatePeekRules: function(room) {
|
_calculatePeekRules: function(room) {
|
||||||
|
@ -416,19 +298,25 @@ module.exports = React.createClass({
|
||||||
// set it in our state and start using it (ie. init the timeline)
|
// set it in our state and start using it (ie. init the timeline)
|
||||||
// This will happen if we start off viewing a room we're not joined,
|
// This will happen if we start off viewing a room we're not joined,
|
||||||
// then join it whilst RoomView is looking at that room.
|
// then join it whilst RoomView is looking at that room.
|
||||||
if (room.roomId == this.props.roomId) {
|
if (room.roomId == this.props.roomId && !this.state.room) {
|
||||||
this.setState({
|
this.setState({
|
||||||
room: room
|
room: room
|
||||||
});
|
});
|
||||||
this._initTimeline(this.props).done();
|
this._onRoomLoaded(room);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomName: function(room) {
|
onRoomName: function(room) {
|
||||||
if (room.roomId == this.props.roomId) {
|
// NB don't set state.room here.
|
||||||
this.setState({
|
//
|
||||||
room: room
|
// When peeking, this event lands *before* the timeline is correctly
|
||||||
});
|
// synced; if we set state.room here, the TimelinePanel will be
|
||||||
|
// instantiated, and it will initialise its scroll state, with *no
|
||||||
|
// events*. In short, the scroll state will be all messed up.
|
||||||
|
//
|
||||||
|
// There's no need to set state.room here anyway.
|
||||||
|
if (room.roomId == this.props.roomId) {
|
||||||
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -455,42 +343,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomReceipt: function(receiptEvent, room) {
|
|
||||||
if (room.roomId == this.props.roomId) {
|
|
||||||
var readMarkerEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
|
|
||||||
var readMarkerGhostEventId = this.state.readMarkerGhostEventId;
|
|
||||||
if (this.state.readMarkerEventId !== undefined && this.state.readMarkerEventId != readMarkerEventId) {
|
|
||||||
readMarkerGhostEventId = this.state.readMarkerEventId;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// if the event after the one referenced in the read receipt if sent by us, do nothing since
|
|
||||||
// this is a temporary period before the synthesized receipt for our own message arrives
|
|
||||||
var readMarkerGhostEventIndex;
|
|
||||||
for (var i = 0; i < this.state.events.length; ++i) {
|
|
||||||
if (this.state.events[i].getId() == readMarkerGhostEventId) {
|
|
||||||
readMarkerGhostEventIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (readMarkerGhostEventIndex + 1 < this.state.events.length) {
|
|
||||||
var nextEvent = this.state.events[readMarkerGhostEventIndex + 1];
|
|
||||||
if (nextEvent.sender && nextEvent.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
|
||||||
readMarkerGhostEventId = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
readMarkerEventId: readMarkerEventId,
|
|
||||||
readMarkerGhostEventId: readMarkerGhostEventId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onRoomMemberTyping: function(ev, member) {
|
|
||||||
this.forceUpdate();
|
|
||||||
},
|
|
||||||
|
|
||||||
onRoomStateMember: function(ev, state, member) {
|
onRoomStateMember: function(ev, state, member) {
|
||||||
if (member.roomId === this.props.roomId) {
|
if (member.roomId === this.props.roomId) {
|
||||||
// a member state changed in this room, refresh the tab complete list
|
// a member state changed in this room, refresh the tab complete list
|
||||||
|
@ -554,10 +406,6 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
if (this.refs.messagePanel) {
|
|
||||||
this._initialiseMessagePanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
var call = CallHandler.getCallForRoom(this.props.roomId);
|
var call = CallHandler.getCallForRoom(this.props.roomId);
|
||||||
var callState = call ? call.call_state : "ended";
|
var callState = call ? call.call_state : "ended";
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -594,18 +442,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}, 500),
|
}, 500),
|
||||||
|
|
||||||
_initialiseMessagePanel: function() {
|
|
||||||
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
|
|
||||||
this.refs.messagePanel.initialised = true;
|
|
||||||
this.updateTint();
|
|
||||||
},
|
|
||||||
|
|
||||||
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.roomView) {
|
if (this.refs.roomView) {
|
||||||
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||||
if (!roomView.ondrop) {
|
if (!roomView.ondrop) {
|
||||||
|
@ -615,27 +452,6 @@ module.exports = React.createClass({
|
||||||
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
|
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.refs.messagePanel.initialised) {
|
|
||||||
this._initialiseMessagePanel();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_onTimelineUpdated: function(gotResults) {
|
|
||||||
// we might have switched rooms since the load started - just bin
|
|
||||||
// the results if so.
|
|
||||||
if (this.unmounted) return;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
paginating: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (gotResults) {
|
|
||||||
this.setState({
|
|
||||||
events: this._timelineWindow.getEvents(),
|
|
||||||
canBackPaginate: this._timelineWindow.canPaginate(EventTimeline.BACKWARDS),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchResultsFillRequest: function(backwards) {
|
onSearchResultsFillRequest: function(backwards) {
|
||||||
|
@ -653,23 +469,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// set off a pagination request.
|
|
||||||
onMessageListFillRequest: function(backwards) {
|
|
||||||
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
|
||||||
if(!this._timelineWindow.canPaginate(dir)) {
|
|
||||||
debuglog("RoomView: can't paginate at this time; backwards:"+backwards);
|
|
||||||
return q(false);
|
|
||||||
}
|
|
||||||
this.setState({paginating: true});
|
|
||||||
|
|
||||||
debuglog("RoomView: Initiating paginate; backwards:"+backwards);
|
|
||||||
return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((r) => {
|
|
||||||
debuglog("RoomView: paginate complete backwards:"+backwards+"; success:"+r);
|
|
||||||
this._onTimelineUpdated(r);
|
|
||||||
return r;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onResendAllClick: function() {
|
onResendAllClick: function() {
|
||||||
var eventsToResend = this._getUnsentMessages(this.state.room);
|
var eventsToResend = this._getUnsentMessages(this.state.room);
|
||||||
eventsToResend.forEach(function(event) {
|
eventsToResend.forEach(function(event) {
|
||||||
|
@ -746,19 +545,16 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onMessageListScroll: function(ev) {
|
onMessageListScroll: function(ev) {
|
||||||
if (this.refs.messagePanel.isAtBottom() &&
|
if (this.refs.messagePanel.isAtEndOfLiveTimeline()) {
|
||||||
!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
this.setState({
|
||||||
if (this.state.numUnreadMessages != 0) {
|
numUnreadMessages: 0,
|
||||||
this.setState({ numUnreadMessages: 0 });
|
atEndOfLiveTimeline: true,
|
||||||
}
|
});
|
||||||
if (!this.state.atEndOfLiveTimeline) {
|
|
||||||
this.setState({ atEndOfLiveTimeline: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (this.state.atEndOfLiveTimeline) {
|
this.setState({
|
||||||
this.setState({ atEndOfLiveTimeline: false });
|
atEndOfLiveTimeline: false,
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -976,226 +772,6 @@ module.exports = React.createClass({
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
|
|
||||||
getEventTiles: function() {
|
|
||||||
var DateSeparator = sdk.getComponent('messages.DateSeparator');
|
|
||||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
|
||||||
|
|
||||||
// once images in the events load, make the scrollPanel check the
|
|
||||||
// scroll offsets.
|
|
||||||
var onImageLoad = () => {
|
|
||||||
var scrollPanel = this.refs.messagePanel;
|
|
||||||
if (scrollPanel) {
|
|
||||||
scrollPanel.checkScroll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret = [];
|
|
||||||
var count = 0;
|
|
||||||
|
|
||||||
var prevEvent = null; // the last event we showed
|
|
||||||
var ghostIndex;
|
|
||||||
var readMarkerIndex;
|
|
||||||
for (var i = 0; i < this.state.events.length; i++) {
|
|
||||||
var mxEv = this.state.events[i];
|
|
||||||
var eventId = mxEv.getId();
|
|
||||||
|
|
||||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
|
||||||
// Local echos have a send "status".
|
|
||||||
var scrollToken = mxEv.status ? undefined : eventId;
|
|
||||||
|
|
||||||
var wantTile = true;
|
|
||||||
|
|
||||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
|
||||||
wantTile = false;
|
|
||||||
}
|
|
||||||
else if (this.props.ConferenceHandler && mxEv.getType() === "m.room.member") {
|
|
||||||
if (this.props.ConferenceHandler.isConferenceUser(mxEv.getSender()) ||
|
|
||||||
this.props.ConferenceHandler.isConferenceUser(mxEv.getStateKey())) {
|
|
||||||
// don't suppress conf user join/parts entirely, as they're useful!
|
|
||||||
// wantTile = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wantTile) {
|
|
||||||
// if we aren't showing the event, put in a dummy scroll token anyway, so
|
|
||||||
// that we can scroll to the right place.
|
|
||||||
ret.push(<li key={eventId} data-scroll-token={scrollToken}/>);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// now we've decided whether or not to show this message,
|
|
||||||
// add the read up to marker if appropriate
|
|
||||||
// doing this here means we implicitly do not show the marker
|
|
||||||
// if it's at the bottom
|
|
||||||
// NB. it would be better to decide where the read marker was going
|
|
||||||
// when the state changed rather than here in the render method, but
|
|
||||||
// this is where we decide what messages we show so it's the only
|
|
||||||
// place we know whether we're at the bottom or not.
|
|
||||||
var self = this;
|
|
||||||
var mxEvSender = mxEv.sender ? mxEv.sender.userId : null;
|
|
||||||
if (prevEvent && prevEvent.getId() == this.state.readMarkerEventId && mxEvSender != MatrixClientPeg.get().credentials.userId) {
|
|
||||||
var hr;
|
|
||||||
hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '99%'}} ref={function(n) {
|
|
||||||
self.readMarkerNode = n;
|
|
||||||
}} />);
|
|
||||||
readMarkerIndex = ret.length;
|
|
||||||
ret.push(<li key="_readupto" className="mx_RoomView_myReadMarker_container">{hr}</li>);
|
|
||||||
}
|
|
||||||
|
|
||||||
// is this a continuation of the previous message?
|
|
||||||
var continuation = false;
|
|
||||||
if (prevEvent !== null) {
|
|
||||||
if (mxEv.sender &&
|
|
||||||
prevEvent.sender &&
|
|
||||||
(mxEv.sender.userId === prevEvent.sender.userId) &&
|
|
||||||
(mxEv.getType() == prevEvent.getType())
|
|
||||||
)
|
|
||||||
{
|
|
||||||
continuation = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// do we need a date separator since the last event?
|
|
||||||
var ts1 = mxEv.getTs();
|
|
||||||
if ((prevEvent == null && !this.state.canBackPaginate) ||
|
|
||||||
(prevEvent != null &&
|
|
||||||
new Date(prevEvent.getTs()).toDateString() !== new Date(ts1).toDateString())) {
|
|
||||||
var dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1}/></li>;
|
|
||||||
ret.push(dateSeparator);
|
|
||||||
continuation = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var last = false;
|
|
||||||
if (i == this.state.events.length - 1) {
|
|
||||||
// XXX: we might not show a tile for the last event.
|
|
||||||
last = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var highlight = (eventId == this.props.highlightedEventId);
|
|
||||||
|
|
||||||
ret.push(
|
|
||||||
<li key={eventId}
|
|
||||||
ref={this._collectEventNode.bind(this, eventId)}
|
|
||||||
data-scroll-token={scrollToken}>
|
|
||||||
<EventTile mxEvent={mxEv} continuation={continuation}
|
|
||||||
last={last} isSelectedEvent={highlight}
|
|
||||||
onImageLoad={onImageLoad} />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
|
|
||||||
// A read up to marker has died and returned as a ghost!
|
|
||||||
// Lives in the dom as the ghost of the previous one while it fades away
|
|
||||||
if (eventId == this.state.readMarkerGhostEventId) {
|
|
||||||
ghostIndex = ret.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
prevEvent = mxEv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// splice the read marker ghost in now that we know whether the read receipt
|
|
||||||
// is the last element or not, because we only decide as we're going along.
|
|
||||||
if (readMarkerIndex === undefined && ghostIndex && ghostIndex <= ret.length) {
|
|
||||||
var hr;
|
|
||||||
hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '99%'}} ref={function(n) {
|
|
||||||
Velocity(n, {opacity: '0', width: '10%'}, {duration: 400, easing: 'easeInSine', delay: 1000, complete: function() {
|
|
||||||
if (!self.unmounted) self.setState({readMarkerGhostEventId: undefined});
|
|
||||||
}});
|
|
||||||
}} />);
|
|
||||||
ret.splice(ghostIndex, 0, (
|
|
||||||
<li key="_readuptoghost" className="mx_RoomView_myReadMarker_container">{hr}</li>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
},
|
|
||||||
|
|
||||||
_collectEventNode: function(eventId, node) {
|
|
||||||
if (this.eventNodes == undefined) this.eventNodes = {};
|
|
||||||
this.eventNodes[eventId] = node;
|
|
||||||
},
|
|
||||||
|
|
||||||
_indexForEventId(evId) {
|
|
||||||
for (var i = 0; i < this.state.events.length; ++i) {
|
|
||||||
if (evId == this.state.events[i].getId()) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
sendReadReceipt: function() {
|
|
||||||
if (!this.state.room) return;
|
|
||||||
if (!this.refs.messagePanel) return;
|
|
||||||
|
|
||||||
// we don't want to see our RR marker dropping down as we scroll
|
|
||||||
// through old history. For now, do this just by leaving the RR where
|
|
||||||
// it is until we hit the bottom of the room, though ultimately we
|
|
||||||
// probably want to keep sending RR, but hide the RR until we reach
|
|
||||||
// the bottom of the room again, or something.
|
|
||||||
if (!this.state.atEndOfLiveTimeline) return;
|
|
||||||
|
|
||||||
var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId, true);
|
|
||||||
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
|
||||||
|
|
||||||
// We want to avoid sending out read receipts when we are looking at
|
|
||||||
// events in the past which are before the latest RR.
|
|
||||||
//
|
|
||||||
// For now, let's apply a heuristic: if (a) the event corresponding to
|
|
||||||
// the latest RR (either from the server, or sent by ourselves) doesn't
|
|
||||||
// appear in our timeline, and (b) we could forward-paginate the event
|
|
||||||
// timeline, then don't send any more RRs.
|
|
||||||
//
|
|
||||||
// This isn't watertight, as we could be looking at a section of
|
|
||||||
// timeline which is *after* the latest RR (so we should actually send
|
|
||||||
// RRs) - but that is a bit of a niche case. It will sort itself out when
|
|
||||||
// the user eventually hits the live timeline.
|
|
||||||
//
|
|
||||||
if (currentReadUpToEventId && currentReadUpToEventIndex === null &&
|
|
||||||
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastReadEventIndex = this._getLastDisplayedEventIndexIgnoringOwn();
|
|
||||||
if (lastReadEventIndex === null) return;
|
|
||||||
|
|
||||||
var lastReadEvent = this.state.events[lastReadEventIndex];
|
|
||||||
|
|
||||||
// we also remember the last read receipt we sent to avoid spamming the same one at the server repeatedly
|
|
||||||
if (lastReadEventIndex > currentReadUpToEventIndex && this.last_rr_sent_event_id != lastReadEvent.getId()) {
|
|
||||||
this.last_rr_sent_event_id = lastReadEvent.getId();
|
|
||||||
MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => {
|
|
||||||
// it failed, so allow retries next time the user is active
|
|
||||||
this.last_rr_sent_event_id = undefined;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_getLastDisplayedEventIndexIgnoringOwn: function() {
|
|
||||||
if (this.eventNodes === undefined) return null;
|
|
||||||
|
|
||||||
var messageWrapper = this.refs.messagePanel;
|
|
||||||
if (messageWrapper === undefined) return null;
|
|
||||||
var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
|
|
||||||
|
|
||||||
for (var i = this.state.events.length-1; i >= 0; --i) {
|
|
||||||
var ev = this.state.events[i];
|
|
||||||
|
|
||||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var node = this.eventNodes[ev.getId()];
|
|
||||||
if (!node) continue;
|
|
||||||
|
|
||||||
var boundingRect = node.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (boundingRect.bottom < wrapperRect.bottom) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
onSettingsClick: function() {
|
onSettingsClick: function() {
|
||||||
this.showSettings(true);
|
this.showSettings(true);
|
||||||
},
|
},
|
||||||
|
@ -1298,28 +874,9 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onConferenceNotificationClick: function() {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'place_call',
|
|
||||||
type: "video",
|
|
||||||
room_id: this.props.roomId
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// jump down to the bottom of this room, where new events are arriving
|
// jump down to the bottom of this room, where new events are arriving
|
||||||
jumpToLiveTimeline: function() {
|
jumpToLiveTimeline: function() {
|
||||||
// if we can't forward-paginate the existing timeline, then there
|
this.refs.messagePanel.jumpToLiveTimeline();
|
||||||
// is no point reloading it - just jump straight to the bottom.
|
|
||||||
//
|
|
||||||
// Otherwise, reload the timeline rather than trying to paginate
|
|
||||||
// through all of space-time.
|
|
||||||
if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
|
||||||
this._loadTimeline();
|
|
||||||
} else {
|
|
||||||
if (this.refs.messagePanel) {
|
|
||||||
this.refs.messagePanel.scrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -1390,25 +947,10 @@ module.exports = React.createClass({
|
||||||
// but it's better than the video going missing entirely
|
// but it's better than the video going missing entirely
|
||||||
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
||||||
|
|
||||||
if (this.refs.callView) {
|
// we may need to resize the gemini panel after changing the aux panel
|
||||||
var fullscreenElement =
|
// size, so add a callback to onChildResize.
|
||||||
(document.fullscreenElement ||
|
this.setState({auxPanelMaxHeight: auxPanelMaxHeight},
|
||||||
document.mozFullScreenElement ||
|
this.onChildResize);
|
||||||
document.webkitFullscreenElement);
|
|
||||||
if (!fullscreenElement) {
|
|
||||||
var video = this.refs.callView.getVideoView().getRemoteVideoElement();
|
|
||||||
video.style.maxHeight = auxPanelMaxHeight + "px";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need to do this for general auxPanels too
|
|
||||||
if (this.refs.auxPanel) {
|
|
||||||
this.refs.auxPanel.style.maxHeight = auxPanelMaxHeight + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
// the above might have made the aux panel resize itself, so now
|
|
||||||
// we need to tell the gemini panel to adapt.
|
|
||||||
this.onChildResize();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onFullscreenClick: function() {
|
onFullscreenClick: function() {
|
||||||
|
@ -1442,11 +984,6 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onCallViewResize: function() {
|
|
||||||
this.onChildResize();
|
|
||||||
this.onResize();
|
|
||||||
},
|
|
||||||
|
|
||||||
onChildResize: function() {
|
onChildResize: function() {
|
||||||
// When the video, status bar, or the message composer resizes, the
|
// When the video, status bar, or the message composer resizes, the
|
||||||
// scroll panel also changes size. Work around GeminiScrollBar fail by
|
// scroll panel also changes size. Work around GeminiScrollBar fail by
|
||||||
|
@ -1469,17 +1006,18 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||||
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||||
var CallView = sdk.getComponent("voip.CallView");
|
|
||||||
var RoomSettings = sdk.getComponent("rooms.RoomSettings");
|
var RoomSettings = sdk.getComponent("rooms.RoomSettings");
|
||||||
|
var AuxPanel = sdk.getComponent("rooms.AuxPanel");
|
||||||
var SearchBar = sdk.getComponent("rooms.SearchBar");
|
var SearchBar = sdk.getComponent("rooms.SearchBar");
|
||||||
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
|
var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||||
|
|
||||||
if (!this._timelineWindow) {
|
if (!this.state.room) {
|
||||||
if (this.props.roomId) {
|
if (this.props.roomId) {
|
||||||
if (this.state.timelineLoading) {
|
if (this.state.roomLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView">
|
<div className="mx_RoomView">
|
||||||
<Loader />
|
<Loader />
|
||||||
|
@ -1553,7 +1091,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var scrollheader_classes = classNames({
|
var scrollheader_classes = classNames({
|
||||||
mx_RoomView_scrollheader: true,
|
mx_RoomView_scrollheader: true,
|
||||||
loading: this.state.paginating
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var statusBar;
|
var statusBar;
|
||||||
|
@ -1613,28 +1150,16 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var conferenceCallNotification = null;
|
var auxPanel = (
|
||||||
if (this.state.displayConfCallNotification) {
|
<AuxPanel ref="auxPanel" room={this.state.room}
|
||||||
var supportedText;
|
conferenceHandler={this.props.ConferenceHandler}
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
draggingFile={this.state.draggingFile}
|
||||||
supportedText = " (unsupported)";
|
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||||
}
|
maxHeight={this.state.auxPanelMaxHeight}
|
||||||
conferenceCallNotification = (
|
onCallViewVideoRezize={this.onChildResize} >
|
||||||
<div className="mx_RoomView_ongoingConfCallNotification" onClick={this.onConferenceNotificationClick}>
|
{ aux }
|
||||||
Ongoing conference call {supportedText}
|
</AuxPanel>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileDropTarget = null;
|
|
||||||
if (this.state.draggingFile) {
|
|
||||||
fileDropTarget = <div className="mx_RoomView_fileDropTarget">
|
|
||||||
<div className="mx_RoomView_fileDropTargetLabel" title="Drop File Here">
|
|
||||||
<TintableSvg src="img/upload-big.svg" width="45" height="59"/><br/>
|
|
||||||
Drop file here to upload
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageComposer, searchInfo;
|
var messageComposer, searchInfo;
|
||||||
var canSpeak = (
|
var canSpeak = (
|
||||||
|
@ -1709,47 +1234,20 @@ module.exports = React.createClass({
|
||||||
hideMessagePanel = true;
|
hideMessagePanel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var messagePanel;
|
var messagePanel = (
|
||||||
|
<TimelinePanel ref={(r) => {
|
||||||
// just show a spinner while the timeline loads.
|
this.refs.messagePanel = r;
|
||||||
//
|
if(r) {
|
||||||
// put it in a div of the right class (mx_RoomView_messagePanel) so
|
this.updateTint();
|
||||||
// that the order in the roomview flexbox is correct, and
|
}
|
||||||
// mx_RoomView_messageListWrapper to position the inner div in the
|
}}
|
||||||
// right place.
|
room={this.state.room}
|
||||||
//
|
hidden={hideMessagePanel}
|
||||||
// Note that the click-on-search-result functionality relies on the
|
highlightedEventId={this.props.highlightedEventId}
|
||||||
// fact that the messagePanel is hidden while the timeline reloads,
|
eventId={this.props.eventId}
|
||||||
// but that the RoomHeader (complete with search term) continues to
|
eventPixelOffset={this.props.eventPixelOffset}
|
||||||
// exist.
|
onScroll={ this.onMessageListScroll }
|
||||||
if (this.state.timelineLoading) {
|
/>);
|
||||||
messagePanel = (
|
|
||||||
<div className="mx_RoomView_messagePanel mx_RoomView_messageListWrapper">
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// give the messagepanel a stickybottom if we're at the end of the
|
|
||||||
// live timeline, so that the arrival of new events triggers a
|
|
||||||
// scroll.
|
|
||||||
//
|
|
||||||
// Make sure that stickyBottom is *false* if we can paginate
|
|
||||||
// forwards, otherwise if somebody hits the bottom of the loaded
|
|
||||||
// events when viewing historical messages, we get stuck in a loop
|
|
||||||
// of paginating our way through the entire history of the room.
|
|
||||||
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
|
||||||
|
|
||||||
messagePanel = (
|
|
||||||
<ScrollPanel ref="messagePanel" className="mx_RoomView_messagePanel"
|
|
||||||
onScroll={ this.onMessageListScroll }
|
|
||||||
onFillRequest={ this.onMessageListFillRequest }
|
|
||||||
style={ hideMessagePanel ? { display: 'none' } : {} }
|
|
||||||
stickyBottom={ stickyBottom }>
|
|
||||||
<li className={scrollheader_classes}></li>
|
|
||||||
{this.getEventTiles()}
|
|
||||||
</ScrollPanel>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
||||||
|
@ -1765,13 +1263,7 @@ module.exports = React.createClass({
|
||||||
onLeaveClick={
|
onLeaveClick={
|
||||||
(myMember && myMember.membership === "join") ? this.onLeaveClick : null
|
(myMember && myMember.membership === "join") ? this.onLeaveClick : null
|
||||||
} />
|
} />
|
||||||
<div className="mx_RoomView_auxPanel" ref="auxPanel">
|
{ auxPanel }
|
||||||
{ fileDropTarget }
|
|
||||||
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}
|
|
||||||
onResize={this.onCallViewResize} />
|
|
||||||
{ conferenceCallNotification }
|
|
||||||
{ aux }
|
|
||||||
</div>
|
|
||||||
{ messagePanel }
|
{ messagePanel }
|
||||||
{ searchResultsPanel }
|
{ searchResultsPanel }
|
||||||
<div className="mx_RoomView_statusArea">
|
<div className="mx_RoomView_statusArea">
|
||||||
|
|
463
src/components/structures/TimelinePanel.js
Normal file
463
src/components/structures/TimelinePanel.js
Normal file
|
@ -0,0 +1,463 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ReactDOM = require("react-dom");
|
||||||
|
var q = require("q");
|
||||||
|
|
||||||
|
var Matrix = require("matrix-js-sdk");
|
||||||
|
var EventTimeline = Matrix.EventTimeline;
|
||||||
|
|
||||||
|
var sdk = require('../../index');
|
||||||
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
|
var dis = require("../../dispatcher");
|
||||||
|
|
||||||
|
var PAGINATE_SIZE = 20;
|
||||||
|
var INITIAL_SIZE = 20;
|
||||||
|
var TIMELINE_CAP = 1000; // the most events to show in a timeline
|
||||||
|
|
||||||
|
var DEBUG = false;
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
|
var debuglog = console.log.bind(console);
|
||||||
|
} else {
|
||||||
|
var debuglog = function () {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Component which shows the event timeline in a room view.
|
||||||
|
*
|
||||||
|
* Also responsible for handling and sending read receipts.
|
||||||
|
*/
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'TimelinePanel',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// The js-sdk Room object for the room whose timeline we are
|
||||||
|
// representing.
|
||||||
|
room: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
// true to give the component a 'display: none' style.
|
||||||
|
hidden: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||||
|
// typically this will be either 'eventId' or undefined.
|
||||||
|
highlightedEventId: React.PropTypes.string,
|
||||||
|
|
||||||
|
// id of an event to jump to. If not given, will go to the end of the
|
||||||
|
// live timeline.
|
||||||
|
eventId: React.PropTypes.string,
|
||||||
|
|
||||||
|
// where to position the event given by eventId, in pixels from the
|
||||||
|
// bottom of the viewport. If not given, will try to put the event in the
|
||||||
|
// middle of the viewprt.
|
||||||
|
eventPixelOffset: React.PropTypes.number,
|
||||||
|
|
||||||
|
// callback which is called when the panel is scrolled.
|
||||||
|
onScroll: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
timelineLoading: true, // track whether our room timeline is loading
|
||||||
|
canBackPaginate: true,
|
||||||
|
readMarkerEventId: this._getCurrentReadReceipt(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
debuglog("TimelinePanel: mounting");
|
||||||
|
|
||||||
|
this.last_rr_sent_event_id = undefined;
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
|
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||||
|
MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
|
||||||
|
|
||||||
|
this._initTimeline(this.props);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
if (newProps.room !== this.props.room) {
|
||||||
|
throw new Error("changing room on a TimelinePanel is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newProps.eventId != this.props.eventId) {
|
||||||
|
console.log("TimelinePanel switching to eventId " + newProps.eventId +
|
||||||
|
" (was " + this.props.eventId + ")");
|
||||||
|
return this._initTimeline(newProps);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
// set a boolean to say we've been unmounted, which any pending
|
||||||
|
// promises can use to throw away their results.
|
||||||
|
//
|
||||||
|
// (We could use isMounted, but facebook have deprecated that.)
|
||||||
|
this.unmounted = true;
|
||||||
|
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
|
||||||
|
var client = MatrixClientPeg.get();
|
||||||
|
if (client) {
|
||||||
|
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
|
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
|
client.removeListener("Room.redaction", this.onRoomRedaction);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// set off a pagination request.
|
||||||
|
onMessageListFillRequest: function(backwards) {
|
||||||
|
var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||||
|
if(!this._timelineWindow.canPaginate(dir)) {
|
||||||
|
debuglog("TimelinePanel: can't paginate at this time; backwards:"+backwards);
|
||||||
|
return q(false);
|
||||||
|
}
|
||||||
|
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
|
||||||
|
return this._timelineWindow.paginate(dir, PAGINATE_SIZE).then((r) => {
|
||||||
|
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
||||||
|
this._onTimelineUpdated(r);
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onAction: function(payload) {
|
||||||
|
switch (payload.action) {
|
||||||
|
case 'user_activity':
|
||||||
|
case 'user_activity_end':
|
||||||
|
// we could treat user_activity_end differently and not
|
||||||
|
// send receipts for messages that have arrived between
|
||||||
|
// the actual user activity and the time they stopped
|
||||||
|
// being active, but let's see if this is actually
|
||||||
|
// necessary.
|
||||||
|
this.sendReadReceipt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
|
||||||
|
// ignore events for other rooms
|
||||||
|
if (room !== this.props.room) return;
|
||||||
|
|
||||||
|
// ignore anything but real-time updates at the end of the room:
|
||||||
|
// updates from pagination will happen when the paginate completes.
|
||||||
|
if (toStartOfTimeline || !data || !data.liveEvent) return;
|
||||||
|
|
||||||
|
// tell the messagepanel to go paginate itself. This in turn will cause
|
||||||
|
// onMessageListFillRequest to be called, which will call
|
||||||
|
// _onTimelineUpdated, which will update the state with the new event -
|
||||||
|
// so there is no need update the state here.
|
||||||
|
//
|
||||||
|
if (this.refs.messagePanel) {
|
||||||
|
this.refs.messagePanel.checkFillState();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomReceipt: function(receiptEvent, room) {
|
||||||
|
if (room !== this.props.room)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// the received event may or may not be for our user; but it turns out
|
||||||
|
// to be easier to do the processing anyway than to figure out if it
|
||||||
|
// is.
|
||||||
|
var oldReadMarker = this.state.readMarkerEventId;
|
||||||
|
var newReadMarker = this._getCurrentReadReceipt();
|
||||||
|
|
||||||
|
if (newReadMarker == oldReadMarker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// suppress the animation when moving forward over an event which was sent
|
||||||
|
// by us; the original RM will have been suppressed so we don't want to show
|
||||||
|
// the animation either.
|
||||||
|
var oldReadMarkerIndex = this._indexForEventId(oldReadMarker);
|
||||||
|
if (oldReadMarkerIndex + 1 < this.state.events.length) {
|
||||||
|
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
var nextEvent = this.state.events[oldReadMarkerIndex + 1];
|
||||||
|
if (nextEvent.sender && nextEvent.sender.userId == myUserId) {
|
||||||
|
oldReadMarker = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
readMarkerEventId: newReadMarker,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomRedaction: function(ev, room) {
|
||||||
|
if (this.unmounted) return;
|
||||||
|
|
||||||
|
// ignore events for other rooms
|
||||||
|
if (room !== this.props.room) return;
|
||||||
|
|
||||||
|
// we could skip an update if the event isn't in our timeline,
|
||||||
|
// but that's probably an early optimisation.
|
||||||
|
this.forceUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
sendReadReceipt: function() {
|
||||||
|
if (!this.refs.messagePanel) return;
|
||||||
|
|
||||||
|
var currentReadUpToEventId = this._getCurrentReadReceipt(true);
|
||||||
|
var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId);
|
||||||
|
|
||||||
|
// We want to avoid sending out read receipts when we are looking at
|
||||||
|
// events in the past which are before the latest RR.
|
||||||
|
//
|
||||||
|
// For now, let's apply a heuristic: if (a) the event corresponding to
|
||||||
|
// the latest RR (either from the server, or sent by ourselves) doesn't
|
||||||
|
// appear in our timeline, and (b) we could forward-paginate the event
|
||||||
|
// timeline, then don't send any more RRs.
|
||||||
|
//
|
||||||
|
// This isn't watertight, as we could be looking at a section of
|
||||||
|
// timeline which is *after* the latest RR (so we should actually send
|
||||||
|
// RRs) - but that is a bit of a niche case. It will sort itself out when
|
||||||
|
// the user eventually hits the live timeline.
|
||||||
|
//
|
||||||
|
if (currentReadUpToEventId && currentReadUpToEventIndex === null &&
|
||||||
|
this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastReadEventIndex = this._getLastDisplayedEventIndexIgnoringOwn();
|
||||||
|
if (lastReadEventIndex === null) return;
|
||||||
|
|
||||||
|
var lastReadEvent = this.state.events[lastReadEventIndex];
|
||||||
|
|
||||||
|
// we also remember the last read receipt we sent to avoid spamming the
|
||||||
|
// same one at the server repeatedly
|
||||||
|
if (lastReadEventIndex > currentReadUpToEventIndex
|
||||||
|
&& this.last_rr_sent_event_id != lastReadEvent.getId()) {
|
||||||
|
this.last_rr_sent_event_id = lastReadEvent.getId();
|
||||||
|
MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => {
|
||||||
|
// it failed, so allow retries next time the user is active
|
||||||
|
this.last_rr_sent_event_id = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* jump down to the bottom of this room, where new events are arriving
|
||||||
|
*/
|
||||||
|
jumpToLiveTimeline: function() {
|
||||||
|
// if we can't forward-paginate the existing timeline, then there
|
||||||
|
// is no point reloading it - just jump straight to the bottom.
|
||||||
|
//
|
||||||
|
// Otherwise, reload the timeline rather than trying to paginate
|
||||||
|
// through all of space-time.
|
||||||
|
if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
|
||||||
|
this._loadTimeline();
|
||||||
|
} else {
|
||||||
|
if (this.refs.messagePanel) {
|
||||||
|
this.refs.messagePanel.scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* return true if the content is fully scrolled down and we are
|
||||||
|
* at the end of the live timeline.
|
||||||
|
*/
|
||||||
|
isAtEndOfLiveTimeline: function() {
|
||||||
|
return this.refs.messagePanel
|
||||||
|
&& this.refs.messagePanel.isAtBottom()
|
||||||
|
&& this._timelineWindow
|
||||||
|
&& !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/* get the current scroll state. See ScrollPanel.getScrollState for
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* returns null if we are not mounted.
|
||||||
|
*/
|
||||||
|
getScrollState: function() {
|
||||||
|
if (!this.refs.messagePanel) { return null; }
|
||||||
|
return this.refs.messagePanel.getScrollState();
|
||||||
|
},
|
||||||
|
|
||||||
|
_initTimeline: function(props) {
|
||||||
|
var initialEvent = props.eventId;
|
||||||
|
var pixelOffset = props.eventPixelOffset;
|
||||||
|
return this._loadTimeline(initialEvent, pixelOffset);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (re)-load the event timeline, and initialise the scroll state, centered
|
||||||
|
* around the given event.
|
||||||
|
*
|
||||||
|
* @param {string?} eventId the event to focus on. If undefined, will
|
||||||
|
* scroll to the bottom of the room.
|
||||||
|
*
|
||||||
|
* @param {number?} pixelOffset offset to position the given event at
|
||||||
|
* (pixels from the bottom of the view). If undefined, will put the
|
||||||
|
* event in the middle of the view.
|
||||||
|
*
|
||||||
|
* returns a promise which will resolve when the load completes.
|
||||||
|
*/
|
||||||
|
_loadTimeline: function(eventId, pixelOffset) {
|
||||||
|
// TODO: we could optimise this, by not resetting the window if the
|
||||||
|
// event is in the current window (though it's not obvious how we can
|
||||||
|
// tell if the current window is on the live event stream)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
events: [],
|
||||||
|
timelineLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._timelineWindow = new Matrix.TimelineWindow(
|
||||||
|
MatrixClientPeg.get(), this.props.room,
|
||||||
|
{windowLimit: TIMELINE_CAP});
|
||||||
|
|
||||||
|
return this._timelineWindow.load(eventId, INITIAL_SIZE).then(() => {
|
||||||
|
debuglog("TimelinePanel: timeline loaded");
|
||||||
|
this._onTimelineUpdated(true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.setState({
|
||||||
|
timelineLoading: false,
|
||||||
|
}, () => {
|
||||||
|
// initialise the scroll state of the message panel
|
||||||
|
if (!this.refs.messagePanel) {
|
||||||
|
// this shouldn't happen.
|
||||||
|
console.log("can't initialise scroll state because " +
|
||||||
|
"messagePanel didn't load");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (eventId) {
|
||||||
|
this.refs.messagePanel.scrollToEvent(eventId, pixelOffset);
|
||||||
|
} else {
|
||||||
|
this.refs.messagePanel.scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendReadReceipt();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onTimelineUpdated: function(gotResults) {
|
||||||
|
// we might have switched rooms since the load started - just bin
|
||||||
|
// the results if so.
|
||||||
|
if (this.unmounted) return;
|
||||||
|
|
||||||
|
if (gotResults) {
|
||||||
|
this.setState({
|
||||||
|
events: this._timelineWindow.getEvents(),
|
||||||
|
canBackPaginate: this._timelineWindow.canPaginate(EventTimeline.BACKWARDS),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_indexForEventId: function(evId) {
|
||||||
|
for (var i = 0; i < this.state.events.length; ++i) {
|
||||||
|
if (evId == this.state.events[i].getId()) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getLastDisplayedEventIndexIgnoringOwn: function() {
|
||||||
|
var messagePanel = this.refs.messagePanel;
|
||||||
|
if (messagePanel === undefined) return null;
|
||||||
|
|
||||||
|
var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
|
||||||
|
|
||||||
|
for (var i = this.state.events.length-1; i >= 0; --i) {
|
||||||
|
var ev = this.state.events[i];
|
||||||
|
|
||||||
|
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var node = messagePanel.getNodeForEventId(ev.getId());
|
||||||
|
if (!node) continue;
|
||||||
|
|
||||||
|
var boundingRect = node.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (boundingRect.bottom < wrapperRect.bottom) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the id of the event corresponding to our user's latest read-receipt.
|
||||||
|
*
|
||||||
|
* @param {Boolean} ignoreSynthesized If true, return only receipts that
|
||||||
|
* have been sent by the server, not
|
||||||
|
* implicit ones generated by the JS
|
||||||
|
* SDK.
|
||||||
|
*/
|
||||||
|
_getCurrentReadReceipt: function(ignoreSynthesized) {
|
||||||
|
var client = MatrixClientPeg.get();
|
||||||
|
// the client can be null on logout
|
||||||
|
if (client == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var myUserId = client.credentials.userId;
|
||||||
|
return this.props.room.getEventReadUpTo(myUserId, ignoreSynthesized);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||||
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
|
// just show a spinner while the timeline loads.
|
||||||
|
//
|
||||||
|
// put it in a div of the right class (mx_RoomView_messagePanel) so
|
||||||
|
// that the order in the roomview flexbox is correct, and
|
||||||
|
// mx_RoomView_messageListWrapper to position the inner div in the
|
||||||
|
// right place.
|
||||||
|
//
|
||||||
|
// Note that the click-on-search-result functionality relies on the
|
||||||
|
// fact that the messagePanel is hidden while the timeline reloads,
|
||||||
|
// but that the RoomHeader (complete with search term) continues to
|
||||||
|
// exist.
|
||||||
|
if (this.state.timelineLoading) {
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomView_messagePanel mx_RoomView_messageListWrapper">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// give the messagepanel a stickybottom if we're at the end of the
|
||||||
|
// live timeline, so that the arrival of new events triggers a
|
||||||
|
// scroll.
|
||||||
|
//
|
||||||
|
// Make sure that stickyBottom is *false* if we can paginate
|
||||||
|
// forwards, otherwise if somebody hits the bottom of the loaded
|
||||||
|
// events when viewing historical messages, we get stuck in a loop
|
||||||
|
// of paginating our way through the entire history of the room.
|
||||||
|
var stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessagePanel ref="messagePanel"
|
||||||
|
hidden={ this.props.hidden }
|
||||||
|
events={ this.state.events }
|
||||||
|
highlightedEventId={ this.props.highlightedEventId }
|
||||||
|
readMarkerEventId={ this.state.readMarkerEventId }
|
||||||
|
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||||
|
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||||
|
stickyBottom={ stickyBottom }
|
||||||
|
onScroll={ this.props.onScroll }
|
||||||
|
onFillRequest={ this.onMessageListFillRequest }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
104
src/components/views/rooms/AuxPanel.js
Normal file
104
src/components/views/rooms/AuxPanel.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'AuxPanel',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// js-sdk room object
|
||||||
|
room: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
// Conference Handler implementation
|
||||||
|
conferenceHandler: React.PropTypes.object,
|
||||||
|
|
||||||
|
// set to true to show the file drop target
|
||||||
|
draggingFile: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// set to true to show the 'active conf call' banner
|
||||||
|
displayConfCallNotification: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// maxHeight attribute for the aux panel and the video
|
||||||
|
// therein
|
||||||
|
maxHeight: React.PropTypes.number,
|
||||||
|
|
||||||
|
// a callback which is called when the video element in a voip call is
|
||||||
|
// resized due to a change in video metadata
|
||||||
|
onCallViewVideoResize: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
onConferenceNotificationClick: function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'place_call',
|
||||||
|
type: "video",
|
||||||
|
room_id: this.props.room.roomId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var CallView = sdk.getComponent("voip.CallView");
|
||||||
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
|
var fileDropTarget = null;
|
||||||
|
if (this.props.draggingFile) {
|
||||||
|
fileDropTarget = (
|
||||||
|
<div className="mx_RoomView_fileDropTarget">
|
||||||
|
<div className="mx_RoomView_fileDropTargetLabel"
|
||||||
|
title="Drop File Here">
|
||||||
|
<TintableSvg src="img/upload-big.svg" width="45" height="59"/>
|
||||||
|
<br/>
|
||||||
|
Drop file here to upload
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var conferenceCallNotification = null;
|
||||||
|
if (this.props.displayConfCallNotification) {
|
||||||
|
var supportedText;
|
||||||
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
|
supportedText = " (unsupported)";
|
||||||
|
}
|
||||||
|
conferenceCallNotification = (
|
||||||
|
<div className="mx_RoomView_ongoingConfCallNotification"
|
||||||
|
onClick={this.onConferenceNotificationClick}>
|
||||||
|
Ongoing conference call {supportedText}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var callView = (
|
||||||
|
<CallView ref="callView" room={this.props.room}
|
||||||
|
ConferenceHandler={this.props.conferenceHandler}
|
||||||
|
onResize={this.props.onCallViewVideoResize}
|
||||||
|
maxVideoHeight={this.props.maxHeight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
|
||||||
|
{ fileDropTarget }
|
||||||
|
{ callView }
|
||||||
|
{ conferenceCallNotification }
|
||||||
|
{ this.props.children }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -19,38 +19,32 @@ var CallHandler = require("../../../CallHandler");
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
|
||||||
/*
|
|
||||||
* State vars:
|
|
||||||
* this.state.call = MatrixCall|null
|
|
||||||
*
|
|
||||||
* Props:
|
|
||||||
* this.props.room = Room (JS SDK)
|
|
||||||
* this.props.ConferenceHandler = A Conference Handler implementation
|
|
||||||
* Must have a function signature:
|
|
||||||
* getConferenceCallForRoom(roomId: string): MatrixCall
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'CallView',
|
displayName: 'CallView',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// a callback which is called when the video within the callview
|
// js-sdk room object
|
||||||
// due to a change in video metadata
|
room: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
// A Conference Handler implementation
|
||||||
|
// Must have a function signature:
|
||||||
|
// getConferenceCallForRoom(roomId: string): MatrixCall
|
||||||
|
ConferenceHandler: React.PropTypes.object,
|
||||||
|
|
||||||
|
// maxHeight style attribute for the video panel
|
||||||
|
maxVideoHeight: React.PropTypes.number,
|
||||||
|
|
||||||
|
// a callback which is called when the user clicks on the video div
|
||||||
|
onClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
// a callback which is called when the video within the callview is
|
||||||
|
// resized due to a change in video metadata
|
||||||
onResize: React.PropTypes.func,
|
onResize: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
if (this.props.room) {
|
this.showCall(this.props.room.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) {
|
|
||||||
this.showCall(call.roomId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
@ -103,7 +97,10 @@ module.exports = React.createClass({
|
||||||
render: function(){
|
render: function(){
|
||||||
var VideoView = sdk.getComponent('voip.VideoView');
|
var VideoView = sdk.getComponent('voip.VideoView');
|
||||||
return (
|
return (
|
||||||
<VideoView ref="video" onClick={ this.props.onClick } onResize={ this.props.onResize }/>
|
<VideoView ref="video" onClick={ this.props.onClick }
|
||||||
|
onResize={ this.props.onResize }
|
||||||
|
maxHeight={ this.props.maxVideoHeight }
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,9 @@ module.exports = React.createClass({
|
||||||
displayName: 'VideoFeed',
|
displayName: 'VideoFeed',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
// maxHeight style attribute for the video element
|
||||||
|
maxHeight: React.PropTypes.number,
|
||||||
|
|
||||||
// a callback which is called when the video element is resized
|
// a callback which is called when the video element is resized
|
||||||
// due to a change in video metadata
|
// due to a change in video metadata
|
||||||
onResize: React.PropTypes.func,
|
onResize: React.PropTypes.func,
|
||||||
|
@ -43,7 +46,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<video ref="vid">
|
<video ref="vid" style={{maxHeight: this.props.maxHeight}}>
|
||||||
</video>
|
</video>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,6 +25,18 @@ var dis = require('../../../dispatcher');
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'VideoView',
|
displayName: 'VideoView',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// maxHeight style attribute for the video element
|
||||||
|
maxHeight: React.PropTypes.number,
|
||||||
|
|
||||||
|
// a callback which is called when the user clicks on the video div
|
||||||
|
onClick: React.PropTypes.func,
|
||||||
|
|
||||||
|
// a callback which is called when the video element is resized due to
|
||||||
|
// a change in video metadata
|
||||||
|
onResize: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
},
|
},
|
||||||
|
@ -64,7 +76,6 @@ module.exports = React.createClass({
|
||||||
element.msRequestFullscreen
|
element.msRequestFullscreen
|
||||||
);
|
);
|
||||||
requestMethod.call(element);
|
requestMethod.call(element);
|
||||||
this.getRemoteVideoElement().style.maxHeight = "inherit";
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var exitMethod = (
|
var exitMethod = (
|
||||||
|
@ -83,10 +94,18 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var VideoFeed = sdk.getComponent('voip.VideoFeed');
|
var VideoFeed = sdk.getComponent('voip.VideoFeed');
|
||||||
|
|
||||||
|
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||||
|
var fullscreenElement = (document.fullscreenElement ||
|
||||||
|
document.mozFullScreenElement ||
|
||||||
|
document.webkitFullscreenElement);
|
||||||
|
var maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
|
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
|
||||||
<div className="mx_VideoView_remoteVideoFeed">
|
<div className="mx_VideoView_remoteVideoFeed">
|
||||||
<VideoFeed ref="remote" onResize={this.props.onResize}/>
|
<VideoFeed ref="remote" onResize={this.props.onResize}
|
||||||
|
maxHeight={maxVideoHeight} />
|
||||||
<audio ref="remoteAudio"/>
|
<audio ref="remoteAudio"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_VideoView_localVideoFeed">
|
<div className="mx_VideoView_localVideoFeed">
|
||||||
|
|
Loading…
Reference in a new issue