Merge pull request #5836 from matrix-org/gsouquet-readreceipts-animation
This commit is contained in:
commit
36e729a626
8 changed files with 56 additions and 104 deletions
|
@ -1,7 +1,7 @@
|
||||||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||||
|
|
||||||
src/Markdown.js
|
src/Markdown.js
|
||||||
src/Velociraptor.js
|
src/NodeAnimator.js
|
||||||
src/components/structures/RoomDirectory.js
|
src/components/structures/RoomDirectory.js
|
||||||
src/components/views/rooms/MemberList.js
|
src/components/views/rooms/MemberList.js
|
||||||
src/ratelimitedfunc.js
|
src/ratelimitedfunc.js
|
||||||
|
|
|
@ -102,7 +102,6 @@
|
||||||
"tar-js": "^0.3.0",
|
"tar-js": "^0.3.0",
|
||||||
"text-encoding-utf-8": "^1.0.2",
|
"text-encoding-utf-8": "^1.0.2",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"velocity-animate": "^2.0.6",
|
|
||||||
"what-input": "^5.2.10",
|
"what-input": "^5.2.10",
|
||||||
"zxcvbn": "^4.4.2"
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,6 +28,16 @@ $MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|
||||||
|
--transition-short: .1s;
|
||||||
|
--transition-standard: .3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion) {
|
||||||
|
:root {
|
||||||
|
--transition-short: 0;
|
||||||
|
--transition-standard: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|
|
@ -283,6 +283,10 @@ $left-gutter: 64px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: $font-14px;
|
height: $font-14px;
|
||||||
width: $font-14px;
|
width: $font-14px;
|
||||||
|
|
||||||
|
transition:
|
||||||
|
left var(--transition-short) ease-out,
|
||||||
|
top var(--transition-standard) ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_readAvatarRemainder {
|
.mx_EventTile_readAvatarRemainder {
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDom from "react-dom";
|
import ReactDom from "react-dom";
|
||||||
import Velocity from "velocity-animate";
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Velociraptor contains components and animates transitions with velocity.
|
* The NodeAnimator contains components and animates transitions.
|
||||||
* It will only pick up direct changes to properties ('left', currently), and so
|
* 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
|
* 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
|
* 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.
|
* automatic positional animation, look at react-shuffle or similar libraries.
|
||||||
*/
|
*/
|
||||||
export default class Velociraptor extends React.Component {
|
export default class NodeAnimator extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
// either a list of child nodes, or a single child.
|
// either a list of child nodes, or a single child.
|
||||||
children: PropTypes.any,
|
children: PropTypes.any,
|
||||||
|
@ -20,14 +19,10 @@ export default class Velociraptor extends React.Component {
|
||||||
|
|
||||||
// a list of state objects to apply to each child node in turn
|
// a list of state objects to apply to each child node in turn
|
||||||
startStyles: PropTypes.array,
|
startStyles: PropTypes.array,
|
||||||
|
|
||||||
// a list of transition options from the corresponding startStyle
|
|
||||||
enterTransitionOpts: PropTypes.array,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
startStyles: [],
|
startStyles: [],
|
||||||
enterTransitionOpts: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -41,6 +36,18 @@ export default class Velociraptor extends React.Component {
|
||||||
this._updateChildren(this.props.children);
|
this._updateChildren(this.props.children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} node element to apply styles to
|
||||||
|
* @param {object} styles a key/value pair of CSS properties
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_applyStyles(node, styles) {
|
||||||
|
Object.entries(styles).forEach(([property, value]) => {
|
||||||
|
node.style[property] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_updateChildren(newChildren) {
|
_updateChildren(newChildren) {
|
||||||
const oldChildren = this.children || {};
|
const oldChildren = this.children || {};
|
||||||
this.children = {};
|
this.children = {};
|
||||||
|
@ -50,17 +57,8 @@ export default class Velociraptor extends React.Component {
|
||||||
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
|
const oldNode = ReactDom.findDOMNode(this.nodes[old.key]);
|
||||||
|
|
||||||
if (oldNode && oldNode.style.left !== c.props.style.left) {
|
if (oldNode && oldNode.style.left !== c.props.style.left) {
|
||||||
Velocity(oldNode, { left: c.props.style.left }, this.props.transition).then(() => {
|
this._applyStyles(oldNode, { left: c.props.style.left });
|
||||||
// special case visibility because it's nonsensical to animate an invisible element
|
// console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
||||||
// so we always hidden->visible pre-transition and visible->hidden after
|
|
||||||
if (oldNode.style.visibility === 'visible' && c.props.style.visibility === 'hidden') {
|
|
||||||
oldNode.style.visibility = c.props.style.visibility;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
//console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left);
|
|
||||||
}
|
|
||||||
if (oldNode && oldNode.style.visibility === 'hidden' && c.props.style.visibility === 'visible') {
|
|
||||||
oldNode.style.visibility = c.props.style.visibility;
|
|
||||||
}
|
}
|
||||||
// clone the old element with the props (and children) of the new element
|
// clone the old element with the props (and children) of the new element
|
||||||
// so prop updates are still received by the children.
|
// so prop updates are still received by the children.
|
||||||
|
@ -94,33 +92,22 @@ export default class Velociraptor extends React.Component {
|
||||||
this.props.startStyles.length > 0
|
this.props.startStyles.length > 0
|
||||||
) {
|
) {
|
||||||
const startStyles = this.props.startStyles;
|
const startStyles = this.props.startStyles;
|
||||||
const transitionOpts = this.props.enterTransitionOpts;
|
|
||||||
const domNode = ReactDom.findDOMNode(node);
|
const domNode = ReactDom.findDOMNode(node);
|
||||||
// start from startStyle 1: 0 is the one we gave it
|
// start from startStyle 1: 0 is the one we gave it
|
||||||
// to start with, so now we animate 1 etc.
|
// to start with, so now we animate 1 etc.
|
||||||
for (var i = 1; i < startStyles.length; ++i) {
|
for (let i = 1; i < startStyles.length; ++i) {
|
||||||
Velocity(domNode, startStyles[i], transitionOpts[i-1]);
|
this._applyStyles(domNode, startStyles[i]);
|
||||||
/*
|
// console.log("start:"
|
||||||
console.log("start:",
|
// JSON.stringify(startStyles[i]),
|
||||||
JSON.stringify(transitionOpts[i-1]),
|
// );
|
||||||
"->",
|
|
||||||
JSON.stringify(startStyles[i]),
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// and then we animate to the resting state
|
// and then we animate to the resting state
|
||||||
Velocity(domNode, restingStyle,
|
setTimeout(() => {
|
||||||
transitionOpts[i-1])
|
this._applyStyles(domNode, restingStyle);
|
||||||
.then(() => {
|
}, 0);
|
||||||
// once we've reached the resting state, hide the element if
|
|
||||||
// appropriate
|
|
||||||
domNode.style.visibility = restingStyle.visibility;
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log("enter:",
|
// console.log("enter:",
|
||||||
// JSON.stringify(transitionOpts[i-1]),
|
|
||||||
// "->",
|
|
||||||
// JSON.stringify(restingStyle));
|
// JSON.stringify(restingStyle));
|
||||||
}
|
}
|
||||||
this.nodes[k] = node;
|
this.nodes[k] = node;
|
||||||
|
@ -128,9 +115,7 @@ export default class Velociraptor extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<span>
|
<>{ Object.values(this.children) }</>
|
||||||
{ Object.values(this.children) }
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
import Velocity from "velocity-animate";
|
|
||||||
|
|
||||||
// courtesy of https://github.com/julianshapiro/velocity/issues/283
|
|
||||||
// We only use easeOutBounce (easeInBounce is just sort of nonsensical)
|
|
||||||
function bounce( p ) {
|
|
||||||
let pow2;
|
|
||||||
let bounce = 4;
|
|
||||||
|
|
||||||
while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {
|
|
||||||
// just sets pow2
|
|
||||||
}
|
|
||||||
return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 );
|
|
||||||
}
|
|
||||||
|
|
||||||
Velocity.Easings.easeOutBounce = function(p) {
|
|
||||||
return 1 - bounce(1 - p);
|
|
||||||
};
|
|
|
@ -17,22 +17,13 @@ limitations under the License.
|
||||||
|
|
||||||
import React, {createRef} from 'react';
|
import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import '../../../VelocityBounce';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {formatDate} from '../../../DateUtils';
|
import {formatDate} from '../../../DateUtils';
|
||||||
import Velociraptor from "../../../Velociraptor";
|
import NodeAnimator from "../../../NodeAnimator";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {toPx} from "../../../utils/units";
|
import {toPx} from "../../../utils/units";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
let bounce = false;
|
|
||||||
try {
|
|
||||||
if (global.localStorage) {
|
|
||||||
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.ReadReceiptMarker")
|
@replaceableComponent("views.rooms.ReadReceiptMarker")
|
||||||
export default class ReadReceiptMarker extends React.PureComponent {
|
export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -115,7 +106,18 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
// we've already done our display - nothing more to do.
|
// we've already done our display - nothing more to do.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._animateMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const differentLeftOffset = prevProps.leftOffset !== this.props.leftOffset;
|
||||||
|
const visibilityChanged = prevProps.hidden !== this.props.hidden;
|
||||||
|
if (differentLeftOffset || visibilityChanged) {
|
||||||
|
this._animateMarker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_animateMarker() {
|
||||||
// treat new RRs as though they were off the top of the screen
|
// treat new RRs as though they were off the top of the screen
|
||||||
let oldTop = -15;
|
let oldTop = -15;
|
||||||
|
|
||||||
|
@ -139,42 +141,18 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const startStyles = [];
|
const startStyles = [];
|
||||||
const enterTransitionOpts = [];
|
|
||||||
|
|
||||||
if (oldInfo && oldInfo.left) {
|
if (oldInfo && oldInfo.left) {
|
||||||
// start at the old height and in the old h pos
|
// start at the old height and in the old h pos
|
||||||
|
|
||||||
startStyles.push({ top: startTopOffset+"px",
|
startStyles.push({ top: startTopOffset+"px",
|
||||||
left: toPx(oldInfo.left) });
|
left: toPx(oldInfo.left) });
|
||||||
|
|
||||||
const reorderTransitionOpts = {
|
|
||||||
duration: 100,
|
|
||||||
easing: 'easeOut',
|
|
||||||
};
|
|
||||||
|
|
||||||
enterTransitionOpts.push(reorderTransitionOpts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// then shift to the rightmost column,
|
startStyles.push({ top: startTopOffset+'px', left: '0' });
|
||||||
// and then it will drop down to its resting position
|
|
||||||
//
|
|
||||||
// XXX: We use a small left value to trick velocity-animate into actually animating.
|
|
||||||
// This is a very annoying bug where if it thinks there's no change to `left` then it'll
|
|
||||||
// skip applying it, thus making our read receipt at +14px instead of +0px like it
|
|
||||||
// should be. This does cause a tiny amount of drift for read receipts, however with a
|
|
||||||
// value so small it's not perceived by a user.
|
|
||||||
// Note: Any smaller values (or trying to interchange units) might cause read receipts to
|
|
||||||
// fail to fall down or cause gaps.
|
|
||||||
startStyles.push({ top: startTopOffset+'px', left: '1px' });
|
|
||||||
enterTransitionOpts.push({
|
|
||||||
duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
|
|
||||||
easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
suppressDisplay: false,
|
suppressDisplay: false,
|
||||||
startStyles: startStyles,
|
startStyles: startStyles,
|
||||||
enterTransitionOpts: enterTransitionOpts,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +165,6 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
const style = {
|
const style = {
|
||||||
left: toPx(this.props.leftOffset),
|
left: toPx(this.props.leftOffset),
|
||||||
top: '0px',
|
top: '0px',
|
||||||
visibility: this.props.hidden ? 'hidden' : 'visible',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let title;
|
let title;
|
||||||
|
@ -210,9 +187,8 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Velociraptor
|
<NodeAnimator
|
||||||
startStyles={this.state.startStyles}
|
startStyles={this.state.startStyles} >
|
||||||
enterTransitionOpts={this.state.enterTransitionOpts} >
|
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
member={this.props.member}
|
member={this.props.member}
|
||||||
fallbackUserId={this.props.fallbackUserId}
|
fallbackUserId={this.props.fallbackUserId}
|
||||||
|
@ -223,7 +199,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
inputRef={this._avatar}
|
inputRef={this._avatar}
|
||||||
/>
|
/>
|
||||||
</Velociraptor>
|
</NodeAnimator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8144,11 +8144,6 @@ validate-npm-package-license@^3.0.1:
|
||||||
spdx-correct "^3.0.0"
|
spdx-correct "^3.0.0"
|
||||||
spdx-expression-parse "^3.0.0"
|
spdx-expression-parse "^3.0.0"
|
||||||
|
|
||||||
velocity-animate@^2.0.6:
|
|
||||||
version "2.0.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/velocity-animate/-/velocity-animate-2.0.6.tgz#1811ca14df7fbbef05740256f6cec0fd1b76575f"
|
|
||||||
integrity sha512-tU+/UtSo3GkIjEfk2KM4e24DvpgX0+FzfLr7XqNwm9BCvZUtbCHPq/AFutx/Mkp2bXlUS9EcX8yxu8XmzAv2Kw==
|
|
||||||
|
|
||||||
verror@1.10.0:
|
verror@1.10.0:
|
||||||
version "1.10.0"
|
version "1.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
|
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue