From 04ef0262af6f77b898d307d87a49488afda0059a Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 20 Apr 2016 22:21:07 +0100 Subject: [PATCH 01/13] Various fixes to the velociraptor * handle having a single child, rather than an array of children * Correctly animate children which are added at the same time as the Velociraptor, rather than added afterwards * Set the child to hidden at the end of the initial animation, if that is required by the style property. --- src/Velociraptor.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 066b1e2d05..a3e013a2b4 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -13,25 +13,24 @@ module.exports = React.createClass({ displayName: 'Velociraptor', propTypes: { - children: React.PropTypes.array, + // either a list of child nodes, or a single child. + children: React.PropTypes.any, + + // optional transition information for changing existing children transition: React.PropTypes.object, - container: React.PropTypes.string }, componentWillMount: function() { this.children = {}; this.nodes = {}; - var self = this; - React.Children.map(this.props.children, function(c) { - self.children[c.key] = c; - }); + this.componentWillReceiveProps(this.props); }, componentWillReceiveProps: function(nextProps) { var self = this; var oldChildren = this.children; this.children = {}; - React.Children.map(nextProps.children, function(c) { + React.Children.toArray(nextProps.children).forEach(function(c) { if (oldChildren[c.key]) { var old = oldChildren[c.key]; var oldNode = ReactDom.findDOMNode(self.nodes[old.key]); @@ -92,22 +91,23 @@ module.exports = React.createClass({ //console.log("start: "+JSON.stringify(startStyles[i])); } // and then we animate to the resting state - Velocity(domNode, node.props._restingStyle, transitionOpts[i-1]); + Velocity(domNode, node.props._restingStyle, + transitionOpts[i-1]) + .then(() => { + // once we've reached the resting state, hide the element if + // appropriate + domNode.style.visibility = node.props._restingStyle.visibility; + }); + //console.log("enter: "+JSON.stringify(node.props._restingStyle)); } this.nodes[k] = node; }, render: function() { - var self = this; - var childList = Object.keys(this.children).map(function(k) { - return React.cloneElement(self.children[k], { - ref: self.collectNode.bind(self, self.children[k].key) - }); - }); return ( - {childList} + {Object.values(this.children)} ); }, From fa34dee09179c0fd08a26ea8e5f13b7a19d1c7e3 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 20 Apr 2016 23:03:05 +0100 Subject: [PATCH 02/13] Rewrite Read-receipt animation ... hopefully fixing https://github.com/vector-im/vector-web/issues/1437 in the process. The idea here is that, when we remove a read-receipt from the DOM, we stash its position in a map. Then, when the read-receipt appears again attached to another event, we know where to start the transition. --- src/component-index.js | 3 +- src/components/structures/MessagePanel.js | 5 + src/components/views/rooms/EventTile.js | 91 ++++------ .../views/rooms/ReadReceiptMarker.js | 166 ++++++++++++++++++ 4 files changed, 208 insertions(+), 57 deletions(-) create mode 100644 src/components/views/rooms/ReadReceiptMarker.js diff --git a/src/component-index.js b/src/component-index.js index 0cb7e257a0..967cc5d685 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -64,11 +64,11 @@ module.exports.components['views.login.LoginHeader'] = require('./components/vie module.exports.components['views.login.PasswordLogin'] = require('./components/views/login/PasswordLogin'); module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm'); module.exports.components['views.login.ServerConfig'] = require('./components/views/login/ServerConfig'); +module.exports.components['views.messages.MAudioBody'] = require('./components/views/messages/MAudioBody'); module.exports.components['views.messages.MFileBody'] = require('./components/views/messages/MFileBody'); module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody'); module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody'); module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); -module.exports.components['views.messages.MAudioBody'] = require('./components/views/messages/MAudioBody'); module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody'); module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent'); module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); @@ -85,6 +85,7 @@ module.exports.components['views.rooms.MemberTile'] = require('./components/view module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer'); module.exports.components['views.rooms.MessageComposerInput'] = require('./components/views/rooms/MessageComposerInput'); module.exports.components['views.rooms.PresenceLabel'] = require('./components/views/rooms/PresenceLabel'); +module.exports.components['views.rooms.ReadReceiptMarker'] = require('./components/views/rooms/ReadReceiptMarker'); module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader'); module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList'); module.exports.components['views.rooms.RoomNameEditor'] = require('./components/views/rooms/RoomNameEditor'); diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index defc8151a9..49f4783eaa 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -81,6 +81,10 @@ module.exports = React.createClass({ // the event after which we are showing a disappearing read marker // animation this.currentGhostEventId = null; + + // opaque readreceipt info for each userId; used by ReadReceiptMarker + // to manage its animations + this._readReceiptMap = {}; }, /* get the DOM node representing the given event */ @@ -346,6 +350,7 @@ module.exports = React.createClass({ diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 3fe6a08f97..5c70c9da10 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -17,7 +17,6 @@ limitations under the License. 'use strict'; var React = require('react'); -var ReactDom = require('react-dom'); var classNames = require("classnames"); var sdk = require('../../../index'); @@ -25,8 +24,6 @@ var MatrixClientPeg = require('../../../MatrixClientPeg') var TextForEvent = require('../../../TextForEvent'); var ContextualMenu = require('../../../ContextualMenu'); -var Velociraptor = require('../../../Velociraptor'); -require('../../../VelocityBounce'); var dispatcher = require("../../../dispatcher"); var ObjectUtils = require('../../../ObjectUtils'); @@ -113,6 +110,12 @@ module.exports = React.createClass({ /* a list of Room Members whose read-receipts we should show */ readReceipts: React.PropTypes.arrayOf(React.PropTypes.object), + /* opaque readreceipt info for each userId; used by ReadReceiptMarker + * to manage its animations. Should be an empty object when the room + * first loads + */ + readReceiptMap: React.PropTypes.object, + /* the status of this event - ie, mxEvent.status. Denormalised to here so * that we can tell when it changes. */ eventSendStatus: React.PropTypes.string, @@ -122,6 +125,15 @@ module.exports = React.createClass({ return {menu: false, allReadAvatars: false}; }, + componentWillMount: function() { + // don't do RR animations until we are mounted + this._suppressReadReceiptAnimation = true; + }, + + componentDidMount: function() { + this._suppressReadReceiptAnimation = false; + }, + shouldComponentUpdate: function (nextProps, nextState) { if (!ObjectUtils.shallowEqual(this.state, nextState)) { return true; @@ -217,80 +229,53 @@ module.exports = React.createClass({ }, getReadAvatars: function() { + var ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); var avatars = []; - var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var left = 0; - var reorderTransitionOpts = { - duration: 100, - easing: 'easeOut' - }; - var receipts = this.props.readReceipts || []; for (var i = 0; i < receipts.length; ++i) { var member = receipts[i]; - // Using react refs here would mean both getting Velociraptor to expose - // them and making them scoped to the whole RoomView. Not impossible, but - // getElementById seems simpler at least for a first cut. - var oldAvatarDomNode = document.getElementById('mx_readAvatar'+member.userId); - var startStyles = []; - var enterTransitionOpts = []; - var oldNodeTop = -15; // For avatars that weren't on screen, act as if they were just off the top - if (oldAvatarDomNode) { - oldNodeTop = oldAvatarDomNode.getBoundingClientRect().top; + var hidden = true; + if ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) { + hidden = false; } - if (this.readAvatarNode) { - var topOffset = oldNodeTop - this.readAvatarNode.getBoundingClientRect().top; + var userId = member.userId; + var readReceiptInfo; - if (oldAvatarDomNode && oldAvatarDomNode.style.left !== '0px') { - var leftOffset = oldAvatarDomNode.style.left; - // start at the old height and in the old h pos - startStyles.push({ top: topOffset, left: leftOffset }); - enterTransitionOpts.push(reorderTransitionOpts); + if (this.props.readReceiptMap) { + readReceiptInfo = this.props.readReceiptMap[userId]; + if (!readReceiptInfo) { + readReceiptInfo = {}; + this.props.readReceiptMap[userId] = readReceiptInfo; } - - // then shift to the rightmost column, - // and then it will drop down to its resting position - startStyles.push({ top: topOffset, left: '0px' }); - enterTransitionOpts.push({ - duration: bounce ? Math.min(Math.log(Math.abs(topOffset)) * 200, 3000) : 300, - easing: bounce ? 'easeOutBounce' : 'easeOutCubic', - }); } - var style = { - left: left+'px', - top: '0px', - visibility: ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) ? 'visible' : 'hidden' - }; - //console.log("i = " + i + ", MAX_READ_AVATARS = " + MAX_READ_AVATARS + ", allReadAvatars = " + this.state.allReadAvatars + " visibility = " + style.visibility); // add to the start so the most recent is on the end (ie. ends up rightmost) avatars.unshift( -