From bc2c744bed4d35befa5051ee5fda3a656446bee3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 Nov 2015 11:42:51 +0000 Subject: [PATCH] more bits of read receipt animation implemented --- package.json | 4 +- src/Velociraptor.js | 83 +++++++++++++++++++ src/skins/vector/views/atoms/MemberAvatar.js | 4 +- src/skins/vector/views/molecules/EventTile.js | 48 ++++++++++- 4 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 src/Velociraptor.js diff --git a/package.json b/package.json index d71fdac527..7151aef8cc 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "filesize": "^3.1.2", "flux": "~2.0.3", "linkifyjs": "^2.0.0-beta.4", - "modernizr": "^3.1.0", "matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop", "matrix-react-sdk": "^0.0.2", "modernizr": "^3.1.0", @@ -39,7 +38,8 @@ "react-dom": "^0.14.2", "react-gemini-scrollbar": "^2.0.1", "react-loader": "^1.4.0", - "sanitize-html": "^1.0.0" + "sanitize-html": "^1.0.0", + "velocity-animate": "^1.2.3" }, "devDependencies": { "babel": "^5.8.23", diff --git a/src/Velociraptor.js b/src/Velociraptor.js new file mode 100644 index 0000000000..1a14381d4d --- /dev/null +++ b/src/Velociraptor.js @@ -0,0 +1,83 @@ +var React = require('react'); +var ReactDom = require('react-dom'); +var Velocity = require('velocity-animate'); + +/** + * The Velociraptor contains components and animates transitions with velocity. + * It will only pick up direct changes to properties ('left', currently), and so + * will not work for animating positional changes where the position is implicit + * from DOM order. This makes it a lot simpler and lighter: if you need fully + * automatic positional animation, look at react-shuffle or similar libraries. + */ +module.exports = React.createClass({ + displayName: 'Velociraptor', + + propTypes: { + children: React.PropTypes.array, + 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.props.key] = c; + }); + }, + + componentWillReceiveProps: function(nextProps) { + var self = this; + var oldChildren = this.children; + this.children = {}; + React.Children.map(nextProps.children, function(c) { + if (oldChildren[c.key]) { + var old = oldChildren[c.key]; + var oldNode = ReactDom.findDOMNode(self.nodes[old.key]); + + if (oldNode.style.left != c.props.style.left) { + Velocity(oldNode, { left: c.props.style.left }, self.props.transition); + } + self.children[c.key] = old; + } else { + self.children[c.key] = c; + } + }); + }, + + collectNode: function(k, node) { + if ( + this.nodes[k] === undefined && + node.props.enterTransition && + Object.keys(node.props.enterTransition).length + ) { + var domNode = ReactDom.findDOMNode(node); + var transitions = node.props.enterTransition; + var transitionOpts = node.props.enterTransitionOpts; + if (!Array.isArray(transitions)) { + transitions = [ transitions ]; + transitionOpts = [ transitionOpts ]; + } + for (var i = 0; i < transitions.length; ++i) { + Velocity(domNode, transitions[i], transitionOpts[i]); + console.log("enter: "+JSON.stringify(transitions[i])); + } + } + 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} + + ); + }, +}); diff --git a/src/skins/vector/views/atoms/MemberAvatar.js b/src/skins/vector/views/atoms/MemberAvatar.js index e7d6b65d78..9d632d7257 100644 --- a/src/skins/vector/views/atoms/MemberAvatar.js +++ b/src/skins/vector/views/atoms/MemberAvatar.js @@ -49,7 +49,7 @@ module.exports = React.createClass({ initial = this.props.member.name[1].toUpperCase(); return ( - + ); } diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index 695240d00e..e99f227fa8 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; var React = require('react'); +var ReactDom = require('react-dom'); var classNames = require("classnames"); var sdk = require('matrix-react-sdk') @@ -27,6 +28,8 @@ var ContextualMenu = require('../../../../ContextualMenu'); var TextForEvent = require('matrix-react-sdk/lib/TextForEvent'); +var Velociraptor = require('../../../../Velociraptor'); + var eventTileTypes = { 'm.room.message': 'molecules.MessageTile', 'm.room.member' : 'molecules.EventAsTextTile', @@ -58,6 +61,10 @@ module.exports = React.createClass({ return {menu: false}; }, + componentDidUpdate: function() { + this.readAvatarRect = ReactDom.findDOMNode(this.readAvatarNode).getBoundingClientRect(); + }, + onEditClicked: function(e) { var MessageContextMenu = sdk.getComponent('molecules.MessageContextMenu'); var buttonRect = e.target.getBoundingClientRect() @@ -93,13 +100,42 @@ module.exports = React.createClass({ var left = 0; + var transitionOpts = { + duration: 1000, + easing: 'linear' + }; + for (var i = 0; i < receipts.length; ++i) { var member = room.getMember(receipts[i].userId); + + // 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 startStyle = { left: left+'px' }; + var enterTransitions = []; + var enterTransitionOpts = []; + if (oldAvatarDomNode && this.readAvatarRect) { + var oldRect = oldAvatarDomNode.getBoundingClientRect(); + startStyle.top = oldRect.top - this.readAvatarRect.top; + + if (oldAvatarDomNode.style.left !== '0px') { + startStyle.left = oldAvatarDomNode.style.left; + enterTransitions.push({ left: left+'px' }); + enterTransitionOpts.push(transitionOpts); + } + enterTransitions.push({ top: '0px' }); + enterTransitionOpts.push(transitionOpts); + } + // add to the start so the most recent is on the end (ie. ends up rightmost) avatars.unshift( ); left -= 15; @@ -113,12 +149,18 @@ module.exports = React.createClass({ remText = +{ remainder }; } - return + return {remText} - {avatars} + + {avatars} + ; }, + collectReadAvatarNode: function(node) { + this.readAvatarNode = node; + }, + render: function() { var MessageTimestamp = sdk.getComponent('atoms.MessageTimestamp'); var SenderProfile = sdk.getComponent('molecules.SenderProfile');