diff --git a/CHANGELOG.md b/CHANGELOG.md index 9086006376..262d55c6da 100644 --- a/CHANGELOG.md +++ b/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) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.4.0...v0.5.1) diff --git a/karma.conf.js b/karma.conf.js index 52eea45f12..1ae2494add 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -51,8 +51,17 @@ module.exports = function (config) { files: [ testFile, 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 // // This doesn't work. It turns out that it's webpack which does the diff --git a/package.json b/package.json index c956560739..3f4a862f6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.5.1", + "version": "0.5.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -49,6 +49,7 @@ "babel": "^5.8.23", "babel-core": "^5.8.38", "babel-loader": "^5.4.0", + "babel-polyfill": "^6.5.0", "expect": "^1.16.0", "json-loader": "^0.5.3", "karma": "^0.13.22", diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 066b1e2d05..ad12d1323b 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -13,25 +13,30 @@ 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._updateChildren(this.props.children); }, 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 oldChildren = this.children; + var oldChildren = this.children || {}; this.children = {}; - React.Children.map(nextProps.children, function(c) { + React.Children.toArray(newChildren).forEach(function(c) { if (oldChildren[c.key]) { var old = oldChildren[c.key]; var oldNode = ReactDom.findDOMNode(self.nodes[old.key]); @@ -92,22 +97,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)} ); }, 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/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 5367b4208a..289dd4be25 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -31,7 +31,7 @@ var KeyCode = require('../../KeyCode'); var PAGINATE_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; @@ -241,11 +241,25 @@ var TimelinePanel = React.createClass({ if (this.unmounted) { return; } debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r); - this.setState({ + + var newState = { [paginatingKey]: false, [canPaginateKey]: r, 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; }); }, diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 631caadac2..223eabdc36 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -84,11 +84,12 @@ module.exports = React.createClass({ findLink: function(nodes) { for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; - if (node.tagName === "A" && node.getAttribute("href") && - (node.getAttribute("href").startsWith("http://") || - node.getAttribute("href").startsWith("https://"))) + if (node.tagName === "A" && node.getAttribute("href")) { - return node; + return this.isLinkPreviewable(node) ? node : undefined; + } + else if (node.tagName === "PRE" || node.tagName === "CODE") { + return; } else if (node.children && node.children.length) { 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) { this.setState({ widgetHidden: true }); // FIXME: persist this somewhere smarter than local storage 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( -