mirror of https://github.com/vector-im/riot-web
Merge remote-tracking branch 'origin/develop' into dbkr/email_notifs
commit
c3365f993b
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -1,3 +1,29 @@
|
||||||
|
Changes in [0.5.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.2) (2016-04-22)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.5.1...v0.5.2)
|
||||||
|
|
||||||
|
Performance improvements:
|
||||||
|
|
||||||
|
* Reduce number of events shown in a room to 250
|
||||||
|
[afb301f](https://github.com/matrix-org/matrix-react-sdk/commit/afb301ffb78c019a50e40caa5d9042ad39c117fe)
|
||||||
|
* add heuristics to hide URL previews...
|
||||||
|
[\#284](https://github.com/matrix-org/matrix-react-sdk/pull/284)
|
||||||
|
* Fix bug which stopped us scrolling down after we scrolled up
|
||||||
|
[\#283](https://github.com/matrix-org/matrix-react-sdk/pull/283)
|
||||||
|
* Don't relayout scrollpanels every time something changes
|
||||||
|
[\#280](https://github.com/matrix-org/matrix-react-sdk/pull/280)
|
||||||
|
* Reduce number of renders on received events
|
||||||
|
[\#279](https://github.com/matrix-org/matrix-react-sdk/pull/279)
|
||||||
|
* Avoid rerendering EventTiles when not necessary
|
||||||
|
[\#278](https://github.com/matrix-org/matrix-react-sdk/pull/278)
|
||||||
|
* Speed up processing of TimelinePanel updates on new events
|
||||||
|
[\#277](https://github.com/matrix-org/matrix-react-sdk/pull/277)
|
||||||
|
|
||||||
|
Other bug fixes:
|
||||||
|
* Fix read-receipt animation
|
||||||
|
[\#282](https://github.com/matrix-org/matrix-react-sdk/pull/282),
|
||||||
|
[\#281](https://github.com/matrix-org/matrix-react-sdk/pull/281)
|
||||||
|
|
||||||
Changes in [0.5.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.1) (2016-04-19)
|
Changes in [0.5.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.1) (2016-04-19)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.4.0...v0.5.1)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.4.0...v0.5.1)
|
||||||
|
|
|
@ -51,8 +51,17 @@ module.exports = function (config) {
|
||||||
files: [
|
files: [
|
||||||
testFile,
|
testFile,
|
||||||
gsCss,
|
gsCss,
|
||||||
|
|
||||||
|
// some images to reduce noise from the tests
|
||||||
|
{pattern: 'test/img/*', watched: false, included: false,
|
||||||
|
served: true, nocache: false},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// redirect img links to the karma server
|
||||||
|
proxies: {
|
||||||
|
"/img/": "/base/test/img/",
|
||||||
|
},
|
||||||
|
|
||||||
// list of files to exclude
|
// list of files to exclude
|
||||||
//
|
//
|
||||||
// This doesn't work. It turns out that it's webpack which does the
|
// This doesn't work. It turns out that it's webpack which does the
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.5.1",
|
"version": "0.5.2",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -49,6 +49,7 @@
|
||||||
"babel": "^5.8.23",
|
"babel": "^5.8.23",
|
||||||
"babel-core": "^5.8.38",
|
"babel-core": "^5.8.38",
|
||||||
"babel-loader": "^5.4.0",
|
"babel-loader": "^5.4.0",
|
||||||
|
"babel-polyfill": "^6.5.0",
|
||||||
"expect": "^1.16.0",
|
"expect": "^1.16.0",
|
||||||
"json-loader": "^0.5.3",
|
"json-loader": "^0.5.3",
|
||||||
"karma": "^0.13.22",
|
"karma": "^0.13.22",
|
||||||
|
|
|
@ -13,25 +13,30 @@ module.exports = React.createClass({
|
||||||
displayName: 'Velociraptor',
|
displayName: 'Velociraptor',
|
||||||
|
|
||||||
propTypes: {
|
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,
|
transition: React.PropTypes.object,
|
||||||
container: React.PropTypes.string
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this.children = {};
|
|
||||||
this.nodes = {};
|
this.nodes = {};
|
||||||
var self = this;
|
this._updateChildren(this.props.children);
|
||||||
React.Children.map(this.props.children, function(c) {
|
|
||||||
self.children[c.key] = c;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
this._updateChildren(nextProps.children);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update `this.children` according to the new list of children given
|
||||||
|
*/
|
||||||
|
_updateChildren: function(newChildren) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var oldChildren = this.children;
|
var oldChildren = this.children || {};
|
||||||
this.children = {};
|
this.children = {};
|
||||||
React.Children.map(nextProps.children, function(c) {
|
React.Children.toArray(newChildren).forEach(function(c) {
|
||||||
if (oldChildren[c.key]) {
|
if (oldChildren[c.key]) {
|
||||||
var old = oldChildren[c.key];
|
var old = oldChildren[c.key];
|
||||||
var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
|
var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
|
||||||
|
@ -92,22 +97,23 @@ module.exports = React.createClass({
|
||||||
//console.log("start: "+JSON.stringify(startStyles[i]));
|
//console.log("start: "+JSON.stringify(startStyles[i]));
|
||||||
}
|
}
|
||||||
// and then we animate to the resting state
|
// 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));
|
//console.log("enter: "+JSON.stringify(node.props._restingStyle));
|
||||||
}
|
}
|
||||||
this.nodes[k] = node;
|
this.nodes[k] = node;
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
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 (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{childList}
|
{Object.values(this.children)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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.PasswordLogin'] = require('./components/views/login/PasswordLogin');
|
||||||
module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm');
|
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.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.MFileBody'] = require('./components/views/messages/MFileBody');
|
||||||
module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody');
|
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.MVideoBody'] = require('./components/views/messages/MVideoBody');
|
||||||
module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
|
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.TextualBody'] = require('./components/views/messages/TextualBody');
|
||||||
module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
|
module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
|
||||||
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
|
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.MessageComposer'] = require('./components/views/rooms/MessageComposer');
|
||||||
module.exports.components['views.rooms.MessageComposerInput'] = require('./components/views/rooms/MessageComposerInput');
|
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.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.RoomHeader'] = require('./components/views/rooms/RoomHeader');
|
||||||
module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList');
|
module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList');
|
||||||
module.exports.components['views.rooms.RoomNameEditor'] = require('./components/views/rooms/RoomNameEditor');
|
module.exports.components['views.rooms.RoomNameEditor'] = require('./components/views/rooms/RoomNameEditor');
|
||||||
|
|
|
@ -81,6 +81,10 @@ module.exports = React.createClass({
|
||||||
// the event after which we are showing a disappearing read marker
|
// the event after which we are showing a disappearing read marker
|
||||||
// animation
|
// animation
|
||||||
this.currentGhostEventId = null;
|
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 */
|
/* get the DOM node representing the given event */
|
||||||
|
@ -346,6 +350,7 @@ module.exports = React.createClass({
|
||||||
<EventTile mxEvent={mxEv} continuation={continuation}
|
<EventTile mxEvent={mxEv} continuation={continuation}
|
||||||
onWidgetLoad={this._onWidgetLoad}
|
onWidgetLoad={this._onWidgetLoad}
|
||||||
readReceipts={readReceipts}
|
readReceipts={readReceipts}
|
||||||
|
readReceiptMap={this._readReceiptMap}
|
||||||
eventSendStatus={mxEv.status}
|
eventSendStatus={mxEv.status}
|
||||||
last={last} isSelectedEvent={highlight}/>
|
last={last} isSelectedEvent={highlight}/>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -31,7 +31,7 @@ var KeyCode = require('../../KeyCode');
|
||||||
|
|
||||||
var PAGINATE_SIZE = 20;
|
var PAGINATE_SIZE = 20;
|
||||||
var INITIAL_SIZE = 20;
|
var INITIAL_SIZE = 20;
|
||||||
var TIMELINE_CAP = 500; // the most events to show in a timeline
|
var TIMELINE_CAP = 250; // the most events to show in a timeline
|
||||||
|
|
||||||
var DEBUG = false;
|
var DEBUG = false;
|
||||||
|
|
||||||
|
@ -241,11 +241,25 @@ var TimelinePanel = React.createClass({
|
||||||
if (this.unmounted) { return; }
|
if (this.unmounted) { return; }
|
||||||
|
|
||||||
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
|
||||||
this.setState({
|
|
||||||
|
var newState = {
|
||||||
[paginatingKey]: false,
|
[paginatingKey]: false,
|
||||||
[canPaginateKey]: r,
|
[canPaginateKey]: r,
|
||||||
events: this._getEvents(),
|
events: this._getEvents(),
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// moving the window in this direction may mean that we can now
|
||||||
|
// paginate in the other where we previously could not.
|
||||||
|
var otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
|
||||||
|
var canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
|
||||||
|
if (!this.state[canPaginateOtherWayKey] &&
|
||||||
|
this._timelineWindow.canPaginate(otherDirection)) {
|
||||||
|
debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
|
||||||
|
newState[canPaginateOtherWayKey] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(newState);
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -84,11 +84,12 @@ module.exports = React.createClass({
|
||||||
findLink: function(nodes) {
|
findLink: function(nodes) {
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
var node = nodes[i];
|
var node = nodes[i];
|
||||||
if (node.tagName === "A" && node.getAttribute("href") &&
|
if (node.tagName === "A" && node.getAttribute("href"))
|
||||||
(node.getAttribute("href").startsWith("http://") ||
|
|
||||||
node.getAttribute("href").startsWith("https://")))
|
|
||||||
{
|
{
|
||||||
return node;
|
return this.isLinkPreviewable(node) ? node : undefined;
|
||||||
|
}
|
||||||
|
else if (node.tagName === "PRE" || node.tagName === "CODE") {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else if (node.children && node.children.length) {
|
else if (node.children && node.children.length) {
|
||||||
return this.findLink(node.children)
|
return this.findLink(node.children)
|
||||||
|
@ -96,6 +97,37 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isLinkPreviewable: function(node) {
|
||||||
|
// don't try to preview relative links
|
||||||
|
if (!node.getAttribute("href").startsWith("http://") &&
|
||||||
|
!node.getAttribute("href").startsWith("https://"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// as a random heuristic to avoid highlighting things like "foo.pl"
|
||||||
|
// we require the linked text to either include a / (either from http://
|
||||||
|
// or from a full foo.bar/baz style schemeless URL) - or be a markdown-style
|
||||||
|
// link, in which case we check the target text differs from the link value.
|
||||||
|
// TODO: make this configurable?
|
||||||
|
if (node.textContent.indexOf("/") > -1)
|
||||||
|
{
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var url = node.getAttribute("href");
|
||||||
|
var host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1];
|
||||||
|
if (node.textContent.trim().startsWith(host)) {
|
||||||
|
// it's a "foo.pl" style link
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// it's a [foo bar](http://foo.com) style link
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onCancelClick: function(event) {
|
onCancelClick: function(event) {
|
||||||
this.setState({ widgetHidden: true });
|
this.setState({ widgetHidden: true });
|
||||||
// FIXME: persist this somewhere smarter than local storage
|
// FIXME: persist this somewhere smarter than local storage
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var ReactDom = require('react-dom');
|
|
||||||
var classNames = require("classnames");
|
var classNames = require("classnames");
|
||||||
|
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
|
@ -25,8 +24,6 @@ var MatrixClientPeg = require('../../../MatrixClientPeg')
|
||||||
var TextForEvent = require('../../../TextForEvent');
|
var TextForEvent = require('../../../TextForEvent');
|
||||||
|
|
||||||
var ContextualMenu = require('../../../ContextualMenu');
|
var ContextualMenu = require('../../../ContextualMenu');
|
||||||
var Velociraptor = require('../../../Velociraptor');
|
|
||||||
require('../../../VelocityBounce');
|
|
||||||
var dispatcher = require("../../../dispatcher");
|
var dispatcher = require("../../../dispatcher");
|
||||||
|
|
||||||
var ObjectUtils = require('../../../ObjectUtils');
|
var ObjectUtils = require('../../../ObjectUtils');
|
||||||
|
@ -113,6 +110,12 @@ module.exports = React.createClass({
|
||||||
/* a list of Room Members whose read-receipts we should show */
|
/* a list of Room Members whose read-receipts we should show */
|
||||||
readReceipts: React.PropTypes.arrayOf(React.PropTypes.object),
|
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
|
/* the status of this event - ie, mxEvent.status. Denormalised to here so
|
||||||
* that we can tell when it changes. */
|
* that we can tell when it changes. */
|
||||||
eventSendStatus: React.PropTypes.string,
|
eventSendStatus: React.PropTypes.string,
|
||||||
|
@ -122,6 +125,15 @@ module.exports = React.createClass({
|
||||||
return {menu: false, allReadAvatars: false};
|
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) {
|
shouldComponentUpdate: function (nextProps, nextState) {
|
||||||
if (!ObjectUtils.shallowEqual(this.state, nextState)) {
|
if (!ObjectUtils.shallowEqual(this.state, nextState)) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -217,80 +229,53 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getReadAvatars: function() {
|
getReadAvatars: function() {
|
||||||
|
var ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||||
var avatars = [];
|
var avatars = [];
|
||||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
|
||||||
|
|
||||||
var left = 0;
|
var left = 0;
|
||||||
|
|
||||||
var reorderTransitionOpts = {
|
|
||||||
duration: 100,
|
|
||||||
easing: 'easeOut'
|
|
||||||
};
|
|
||||||
|
|
||||||
var receipts = this.props.readReceipts || [];
|
var receipts = this.props.readReceipts || [];
|
||||||
for (var i = 0; i < receipts.length; ++i) {
|
for (var i = 0; i < receipts.length; ++i) {
|
||||||
var member = receipts[i];
|
var member = receipts[i];
|
||||||
|
|
||||||
// Using react refs here would mean both getting Velociraptor to expose
|
var hidden = true;
|
||||||
// them and making them scoped to the whole RoomView. Not impossible, but
|
if ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) {
|
||||||
// getElementById seems simpler at least for a first cut.
|
hidden = false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.readAvatarNode) {
|
var userId = member.userId;
|
||||||
var topOffset = oldNodeTop - this.readAvatarNode.getBoundingClientRect().top;
|
var readReceiptInfo;
|
||||||
|
|
||||||
if (oldAvatarDomNode && oldAvatarDomNode.style.left !== '0px') {
|
if (this.props.readReceiptMap) {
|
||||||
var leftOffset = oldAvatarDomNode.style.left;
|
readReceiptInfo = this.props.readReceiptMap[userId];
|
||||||
// start at the old height and in the old h pos
|
if (!readReceiptInfo) {
|
||||||
startStyles.push({ top: topOffset, left: leftOffset });
|
readReceiptInfo = {};
|
||||||
enterTransitionOpts.push(reorderTransitionOpts);
|
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);
|
//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)
|
// add to the start so the most recent is on the end (ie. ends up rightmost)
|
||||||
avatars.unshift(
|
avatars.unshift(
|
||||||
<MemberAvatar key={member.userId} member={member}
|
<ReadReceiptMarker key={userId} member={member}
|
||||||
width={14} height={14} resizeMethod="crop"
|
leftOffset={left} hidden={hidden}
|
||||||
style={style}
|
readReceiptInfo={readReceiptInfo}
|
||||||
startStyle={startStyles}
|
suppressAnimation={this._suppressReadReceiptAnimation}
|
||||||
enterTransitionOpts={enterTransitionOpts}
|
|
||||||
id={'mx_readAvatar'+member.userId}
|
|
||||||
onClick={this.toggleAllReadAvatars}
|
onClick={this.toggleAllReadAvatars}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: we keep the extra read avatars in the dom to make animation simpler
|
// TODO: we keep the extra read avatars in the dom to make animation simpler
|
||||||
// we could optimise this to reduce the dom size.
|
// we could optimise this to reduce the dom size.
|
||||||
if (i < MAX_READ_AVATARS - 1 || this.state.allReadAvatars) { // XXX: where does this -1 come from? is it to make the max'th avatar animate properly?
|
if (!hidden) {
|
||||||
left -= 15;
|
left -= 15;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var editButton;
|
var editButton;
|
||||||
|
var remText;
|
||||||
if (!this.state.allReadAvatars) {
|
if (!this.state.allReadAvatars) {
|
||||||
var remainder = receipts.length - MAX_READ_AVATARS;
|
var remainder = receipts.length - MAX_READ_AVATARS;
|
||||||
var remText;
|
|
||||||
if (i >= MAX_READ_AVATARS - 1) left -= 15;
|
|
||||||
if (remainder > 0) {
|
if (remainder > 0) {
|
||||||
remText = <span className="mx_EventTile_readAvatarRemainder"
|
remText = <span className="mx_EventTile_readAvatarRemainder"
|
||||||
onClick={this.toggleAllReadAvatars}
|
onClick={this.toggleAllReadAvatars}
|
||||||
|
@ -305,19 +290,13 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <span className="mx_EventTile_readAvatars" ref={this.collectReadAvatarNode}>
|
return <span className="mx_EventTile_readAvatars">
|
||||||
{ editButton }
|
{ editButton }
|
||||||
{ remText }
|
{ remText }
|
||||||
<Velociraptor transition={ reorderTransitionOpts }>
|
|
||||||
{ avatars }
|
{ avatars }
|
||||||
</Velociraptor>
|
|
||||||
</span>;
|
</span>;
|
||||||
},
|
},
|
||||||
|
|
||||||
collectReadAvatarNode: function(node) {
|
|
||||||
this.readAvatarNode = ReactDom.findDOMNode(node);
|
|
||||||
},
|
|
||||||
|
|
||||||
onMemberAvatarClick: function(event) {
|
onMemberAvatarClick: function(event) {
|
||||||
dispatcher.dispatch({
|
dispatcher.dispatch({
|
||||||
action: 'view_user',
|
action: 'view_user',
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ReactDOM = require('react-dom');
|
||||||
|
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
|
||||||
|
var Velociraptor = require('../../../Velociraptor');
|
||||||
|
require('../../../VelocityBounce');
|
||||||
|
|
||||||
|
var bounce = false;
|
||||||
|
try {
|
||||||
|
if (global.localStorage) {
|
||||||
|
bounce = global.localStorage.getItem('avatar_bounce') == 'true';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'ReadReceiptMarker',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// the RoomMember to show the RR for
|
||||||
|
member: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
// number of pixels to offset the avatar from the right of its parent;
|
||||||
|
// typically a negative value.
|
||||||
|
leftOffset: React.PropTypes.number,
|
||||||
|
|
||||||
|
// true to hide the avatar (it will still be animated)
|
||||||
|
hidden: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// don't animate this RR into position
|
||||||
|
suppressAnimation: React.PropTypes.bool,
|
||||||
|
|
||||||
|
// an opaque object for storing information about this user's RR in
|
||||||
|
// this room
|
||||||
|
readReceiptInfo: React.PropTypes.object,
|
||||||
|
|
||||||
|
// callback for clicks on this RR
|
||||||
|
onClick: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
leftOffset: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
// if we are going to animate the RR, we don't show it on first render,
|
||||||
|
// and instead just add a placeholder to the DOM; once we've been
|
||||||
|
// mounted, we start an animation which moves the RR from its old
|
||||||
|
// position.
|
||||||
|
return {
|
||||||
|
suppressDisplay: !this.props.suppressAnimation,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
// before we remove the rr, store its location in the map, so that if
|
||||||
|
// it reappears, it can be animated from the right place.
|
||||||
|
var rrInfo = this.props.readReceiptInfo;
|
||||||
|
if (!rrInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var avatarNode = ReactDOM.findDOMNode(this);
|
||||||
|
rrInfo.top = avatarNode.offsetTop;
|
||||||
|
rrInfo.left = avatarNode.offsetLeft;
|
||||||
|
rrInfo.parent = avatarNode.offsetParent;
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
if (!this.state.suppressDisplay) {
|
||||||
|
// we've already done our display - nothing more to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// treat new RRs as though they were off the top of the screen
|
||||||
|
var oldTop = -15;
|
||||||
|
|
||||||
|
var oldInfo = this.props.readReceiptInfo;
|
||||||
|
if (oldInfo && oldInfo.parent) {
|
||||||
|
oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newElement = ReactDOM.findDOMNode(this);
|
||||||
|
var startTopOffset = oldTop - newElement.offsetParent.getBoundingClientRect().top;
|
||||||
|
|
||||||
|
var startStyles = [];
|
||||||
|
var enterTransitionOpts = [];
|
||||||
|
|
||||||
|
if (oldInfo && oldInfo.left) {
|
||||||
|
// start at the old height and in the old h pos
|
||||||
|
|
||||||
|
var leftOffset = oldInfo.left;
|
||||||
|
startStyles.push({ top: startTopOffset+"px",
|
||||||
|
left: oldInfo.left+"px" });
|
||||||
|
|
||||||
|
var reorderTransitionOpts = {
|
||||||
|
duration: 100,
|
||||||
|
easing: 'easeOut'
|
||||||
|
};
|
||||||
|
|
||||||
|
enterTransitionOpts.push(reorderTransitionOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// then shift to the rightmost column,
|
||||||
|
// and then it will drop down to its resting position
|
||||||
|
startStyles.push({ top: startTopOffset+'px', left: '0px' });
|
||||||
|
enterTransitionOpts.push({
|
||||||
|
duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
|
||||||
|
easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
suppressDisplay: false,
|
||||||
|
startStyles: startStyles,
|
||||||
|
enterTransitionOpts: enterTransitionOpts,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
if (this.state.suppressDisplay) {
|
||||||
|
return <div/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
var style = {
|
||||||
|
left: this.props.leftOffset+'px',
|
||||||
|
top: '0px',
|
||||||
|
visibility: this.props.hidden ? 'hidden' : 'visible',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Velociraptor>
|
||||||
|
<MemberAvatar
|
||||||
|
member={this.props.member}
|
||||||
|
width={14} height={14} resizeMethod="crop"
|
||||||
|
style={style}
|
||||||
|
startStyle={this.state.startStyles}
|
||||||
|
enterTransitionOpts={this.state.enterTransitionOpts}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
/>
|
||||||
|
</Velociraptor>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -76,7 +76,8 @@ describe('TimelinePanel', function() {
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function() {
|
||||||
if (parentDiv) {
|
if (parentDiv) {
|
||||||
document.body.removeChild(parentDiv);
|
ReactDOM.unmountComponentAtNode(parentDiv);
|
||||||
|
parentDiv.remove();
|
||||||
parentDiv = null;
|
parentDiv = null;
|
||||||
}
|
}
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
|
@ -204,4 +205,72 @@ describe('TimelinePanel', function() {
|
||||||
}, 0);
|
}, 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should let you scroll down again after you've scrolled up", function(done) {
|
||||||
|
var N_EVENTS = 600;
|
||||||
|
|
||||||
|
// sadly, loading all those events takes a while
|
||||||
|
this.timeout(N_EVENTS * 20);
|
||||||
|
|
||||||
|
// client.getRoom is called a /lot/ in this test, so replace
|
||||||
|
// sinon's spy with a fast noop.
|
||||||
|
client.getRoom = function(id) { return null; };
|
||||||
|
|
||||||
|
// fill the timeline with lots of events
|
||||||
|
for (var i = 0; i < N_EVENTS; i++) {
|
||||||
|
timeline.addEvent(mkMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollDefer;
|
||||||
|
var panel = ReactDOM.render(
|
||||||
|
<TimelinePanel room={room} onScroll={()=>{scrollDefer.resolve()}} />,
|
||||||
|
parentDiv
|
||||||
|
);
|
||||||
|
|
||||||
|
var messagePanel = ReactTestUtils.findRenderedComponentWithType(
|
||||||
|
panel, sdk.getComponent('structures.MessagePanel'));
|
||||||
|
var scrollingDiv = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||||
|
panel, "gm-scroll-view");
|
||||||
|
|
||||||
|
// helper function which will return a promise which resolves when
|
||||||
|
// the TimelinePanel fires a scroll event
|
||||||
|
var awaitScroll = function() {
|
||||||
|
scrollDefer = q.defer();
|
||||||
|
return scrollDefer.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
function backPaginate() {
|
||||||
|
scrollingDiv.scrollTop = 0;
|
||||||
|
return awaitScroll().then(() => {
|
||||||
|
if(scrollingDiv.scrollTop > 0) {
|
||||||
|
// need to go further
|
||||||
|
return backPaginate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// hopefully, we got to the start of the timeline
|
||||||
|
expect(messagePanel.props.backPaginating).toBe(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// let the first round of pagination finish off
|
||||||
|
awaitScroll().then(() => {
|
||||||
|
// we should now have loaded the first few events
|
||||||
|
expect(messagePanel.props.backPaginating).toBe(false);
|
||||||
|
expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);
|
||||||
|
|
||||||
|
// back-paginate until we hit the start
|
||||||
|
return backPaginate();
|
||||||
|
}).then(() => {
|
||||||
|
expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
|
||||||
|
var events = scryEventTiles(panel);
|
||||||
|
expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0])
|
||||||
|
|
||||||
|
// we should now be able to scroll down, and paginate in the other
|
||||||
|
// direction.
|
||||||
|
scrollingDiv.scrollTop = scrollingDiv.scrollHeight;
|
||||||
|
return awaitScroll();
|
||||||
|
}).then(() => {
|
||||||
|
console.log("done");
|
||||||
|
}).done(done, done);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 460 B |
|
@ -5,6 +5,14 @@
|
||||||
* application to provide
|
* application to provide
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* this is a convenient place to ensure we load the compatibility libraries we expect our
|
||||||
|
* app to provide
|
||||||
|
*/
|
||||||
|
|
||||||
|
// for ES6 stuff like startsWith() and Object.values() that babel doesn't do by
|
||||||
|
// default
|
||||||
|
require('babel-polyfill');
|
||||||
|
|
||||||
var sdk = require("../src/index");
|
var sdk = require("../src/index");
|
||||||
|
|
||||||
var skin = require('../src/component-index.js');
|
var skin = require('../src/component-index.js');
|
||||||
|
|
|
@ -37,13 +37,14 @@ module.exports.stubClient = function() {
|
||||||
getRoom: sinon.stub(),
|
getRoom: sinon.stub(),
|
||||||
loginFlows: sinon.stub(),
|
loginFlows: sinon.stub(),
|
||||||
on: sinon.stub(),
|
on: sinon.stub(),
|
||||||
|
removeListener: sinon.stub(),
|
||||||
|
|
||||||
paginateEventTimeline: sinon.stub().returns(q()),
|
paginateEventTimeline: sinon.stub().returns(q()),
|
||||||
sendReadReceipt: sinon.stub().returns(q()),
|
sendReadReceipt: sinon.stub().returns(q()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// create the peg
|
// stub out the methods in MatrixClientPeg
|
||||||
|
//
|
||||||
// 'sandbox.restore()' doesn't work correctly on inherited methods,
|
// 'sandbox.restore()' doesn't work correctly on inherited methods,
|
||||||
// so we do this for each method
|
// so we do this for each method
|
||||||
var methods = ['get', 'unset', 'replaceUsingUrls',
|
var methods = ['get', 'unset', 'replaceUsingUrls',
|
||||||
|
@ -51,7 +52,9 @@ module.exports.stubClient = function() {
|
||||||
for (var i = 0; i < methods.length; i++) {
|
for (var i = 0; i < methods.length; i++) {
|
||||||
sandbox.stub(peg, methods[i]);
|
sandbox.stub(peg, methods[i]);
|
||||||
}
|
}
|
||||||
peg.get.returns(client);
|
// MatrixClientPeg.get() is called a /lot/, so implement it with our own
|
||||||
|
// fast stub function rather than a sinon stub
|
||||||
|
peg.get = function() { return client; };
|
||||||
return sandbox;
|
return sandbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue