From b8fc9262556a4423f3110305ecd8e2fc882d2b91 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 27 Oct 2015 14:38:46 +0000 Subject: [PATCH 01/17] Send read receipts --- src/controllers/organisms/RoomView.js | 55 ++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 21027cbfa8..ff36d4a13e 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -96,6 +96,9 @@ module.exports = { // the conf this._updateConfCallNotification(); break; + case 'user_activity': + this.sendReadReceipt(); + break; } }, @@ -203,6 +206,8 @@ module.exports = { messageWrapper.scrollTop = messageWrapper.scrollHeight; + this.sendReadReceipt(); + this.fillSpace(); } @@ -404,7 +409,7 @@ module.exports = { } ret.unshift( -
  • +
  • ); if (dateSeparator) { ret.unshift(dateSeparator); @@ -499,5 +504,53 @@ module.exports = { uploadingRoomSettings: false, }); } + }, + + _collectEventNode: function(eventId, node) { + if (this.eventNodes == undefined) this.eventNodes = {}; + this.eventNodes[eventId] = node; + }, + + _indexForEventId(evId) { + for (var i = 0; i < this.state.room.timeline.length; ++i) { + if (evId == this.state.room.timeline[i].getId()) { + return i; + } + } + return null; + }, + + sendReadReceipt: function() { + var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); + var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); + + var lastReadEventIndex = this._getLastDisplayedEventIndex(); + if (lastReadEventIndex === null) return; + + if (lastReadEventIndex > currentReadUpToEventIndex) { + MatrixClientPeg.get().sendReadReceipt(this.state.room.timeline[lastReadEventIndex]); + } + }, + + _getLastDisplayedEventIndex: function() { + if (this.eventNodes === undefined) return null; + + var messageWrapper = this.refs.messageWrapper; + if (messageWrapper === undefined) return null; + var wrapperRect = messageWrapper.getDOMNode().getBoundingClientRect(); + + for (var i = this.state.room.timeline.length-1; i >= 0; --i) { + var ev = this.state.room.timeline[i]; + var node = this.eventNodes[ev.getId()]; + if (node === undefined) continue; + + var domNode = node.getDOMNode(); + var boundingRect = domNode.getBoundingClientRect(); + + if (boundingRect.bottom < wrapperRect.bottom) { + return i; + } + } + return null; } }; From 11c38014e57c59689be3b13a411e6b90fec9897e Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 2 Nov 2015 18:55:28 +0000 Subject: [PATCH 02/17] Sort of display read avatars but without live updating --- src/skins/vector/views/molecules/EventTile.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index c5cb81951b..caaada62bb 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -20,6 +20,7 @@ var React = require('react'); var classNames = require("classnames"); var sdk = require('matrix-react-sdk') +var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg') var EventTileController = require('matrix-react-sdk/lib/controllers/molecules/EventTile') var ContextualMenu = require('../../../../ContextualMenu'); @@ -72,6 +73,25 @@ module.exports = React.createClass({ this.setState({menu: true}); }, + getReadAvatars: function() { + var avatars = []; + + var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + + var userIds = room.getUsersReadUpTo(this.props.mxEvent); + + var MemberAvatar = sdk.getComponent('atoms.MemberAvatar'); + + for (var i = 0; i < userIds.length; ++i) { + var member = room.getMember(userIds[i]); + avatars.push( + + ); + } + + return { avatars }; + }, + render: function() { var MessageTimestamp = sdk.getComponent('atoms.MessageTimestamp'); var SenderProfile = sdk.getComponent('molecules.SenderProfile'); @@ -112,6 +132,8 @@ module.exports = React.createClass({ else if (msgtype === 'm.video') aux = "sent a video"; else if (msgtype === 'm.file') aux = "uploaded a file"; + var readAvatars = this.getReadAvatars(); + var avatar, sender; if (!this.props.continuation) { if (this.props.mxEvent.sender) { @@ -132,6 +154,7 @@ module.exports = React.createClass({
    { timestamp } { editButton } + { readAvatars }
    From 2a4a02f36e6263eb85b92ccb73c257feb8c670f6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 3 Nov 2015 13:44:40 +0000 Subject: [PATCH 03/17] More on read receipts: listen for events, add keys & class / very minimal css. --- src/controllers/organisms/RoomView.js | 8 ++++++++ src/skins/vector/css/molecules/EventTile.css | 9 +++++++++ src/skins/vector/views/molecules/EventTile.js | 4 ++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 746dd98ac3..ff62a41b7d 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -48,6 +48,7 @@ module.exports = { this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.name", this.onRoomName); + MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); this.atBottom = true; @@ -65,6 +66,7 @@ module.exports = { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); + MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); } @@ -164,6 +166,12 @@ module.exports = { } }, + onRoomReceipt: function(receiptEvent, room) { + if (room.roomId == this.props.roomId) { + this.forceUpdate(); + } + }, + onRoomMemberTyping: function(ev, member) { this.forceUpdate(); }, diff --git a/src/skins/vector/css/molecules/EventTile.css b/src/skins/vector/css/molecules/EventTile.css index eb59711e81..25fe9646d0 100644 --- a/src/skins/vector/css/molecules/EventTile.css +++ b/src/skins/vector/css/molecules/EventTile.css @@ -123,3 +123,12 @@ limitations under the License. .mx_EventTile.menu .mx_MessageTimestamp { visibility: visible; } + +.mx_EventTile_readAvatars { + float: right; +} + +.mx_EventTile_readAvatars .mx_MemberAvatar { + margin-left: 1px; + margin-right: 1px; +} diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index caaada62bb..0cbeb86442 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -85,11 +85,11 @@ module.exports = React.createClass({ for (var i = 0; i < userIds.length; ++i) { var member = room.getMember(userIds[i]); avatars.push( - + ); } - return { avatars }; + return { avatars }; }, render: function() { From 4bf69923986a6feaeb80a27d24b2777817086a5b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 5 Nov 2015 14:16:15 +0000 Subject: [PATCH 04/17] Don't send read receipts for our own events and null check in a few places. --- src/controllers/organisms/RoomView.js | 10 ++++++++-- src/skins/vector/skindex.js | 20 ++++++++++--------- src/skins/vector/views/molecules/EventTile.js | 2 ++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index ff62a41b7d..5af8220bc3 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -597,10 +597,11 @@ module.exports = { }, sendReadReceipt: function() { + if (!this.state.room) return; var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); - var lastReadEventIndex = this._getLastDisplayedEventIndex(); + var lastReadEventIndex = this._getLastDisplayedEventIndexIgnoringOwn(); if (lastReadEventIndex === null) return; if (lastReadEventIndex > currentReadUpToEventIndex) { @@ -608,7 +609,7 @@ module.exports = { } }, - _getLastDisplayedEventIndex: function() { + _getLastDisplayedEventIndexIgnoringOwn: function() { if (this.eventNodes === undefined) return null; var messageWrapper = this.refs.messageWrapper; @@ -617,6 +618,11 @@ module.exports = { for (var i = this.state.room.timeline.length-1; i >= 0; --i) { var ev = this.state.room.timeline[i]; + + if (ev.sender.userId == MatrixClientPeg.get().credentials.userId) { + continue; + } + var node = this.eventNodes[ev.getId()]; if (node === undefined) continue; diff --git a/src/skins/vector/skindex.js b/src/skins/vector/skindex.js index e715656c0e..54dbad88f8 100644 --- a/src/skins/vector/skindex.js +++ b/src/skins/vector/skindex.js @@ -23,6 +23,9 @@ limitations under the License. var skin = {}; +skin['atoms.create_room.CreateRoomButton'] = require('./views/atoms/create_room/CreateRoomButton'); +skin['atoms.create_room.Presets'] = require('./views/atoms/create_room/Presets'); +skin['atoms.create_room.RoomAlias'] = require('./views/atoms/create_room/RoomAlias'); skin['atoms.EditableText'] = require('./views/atoms/EditableText'); skin['atoms.EnableNotificationsButton'] = require('./views/atoms/EnableNotificationsButton'); skin['atoms.ImageView'] = require('./views/atoms/ImageView'); @@ -30,9 +33,7 @@ skin['atoms.LogoutButton'] = require('./views/atoms/LogoutButton'); skin['atoms.MemberAvatar'] = require('./views/atoms/MemberAvatar'); skin['atoms.MessageTimestamp'] = require('./views/atoms/MessageTimestamp'); skin['atoms.RoomAvatar'] = require('./views/atoms/RoomAvatar'); -skin['atoms.create_room.CreateRoomButton'] = require('./views/atoms/create_room/CreateRoomButton'); -skin['atoms.create_room.Presets'] = require('./views/atoms/create_room/Presets'); -skin['atoms.create_room.RoomAlias'] = require('./views/atoms/create_room/RoomAlias'); +skin['atoms.Spinner'] = require('./views/atoms/Spinner'); skin['atoms.voip.VideoFeed'] = require('./views/atoms/voip/VideoFeed'); skin['molecules.BottomLeftMenu'] = require('./views/molecules/BottomLeftMenu'); skin['molecules.BottomLeftMenuTile'] = require('./views/molecules/BottomLeftMenuTile'); @@ -42,18 +43,18 @@ skin['molecules.ChangePassword'] = require('./views/molecules/ChangePassword'); skin['molecules.DateSeparator'] = require('./views/molecules/DateSeparator'); skin['molecules.EventAsTextTile'] = require('./views/molecules/EventAsTextTile'); skin['molecules.EventTile'] = require('./views/molecules/EventTile'); +skin['molecules.MatrixToolbar'] = require('./views/molecules/MatrixToolbar'); +skin['molecules.MemberInfo'] = require('./views/molecules/MemberInfo'); +skin['molecules.MemberTile'] = require('./views/molecules/MemberTile'); skin['molecules.MEmoteTile'] = require('./views/molecules/MEmoteTile'); +skin['molecules.MessageComposer'] = require('./views/molecules/MessageComposer'); +skin['molecules.MessageContextMenu'] = require('./views/molecules/MessageContextMenu'); +skin['molecules.MessageTile'] = require('./views/molecules/MessageTile'); skin['molecules.MFileTile'] = require('./views/molecules/MFileTile'); skin['molecules.MImageTile'] = require('./views/molecules/MImageTile'); skin['molecules.MNoticeTile'] = require('./views/molecules/MNoticeTile'); skin['molecules.MRoomMemberTile'] = require('./views/molecules/MRoomMemberTile'); skin['molecules.MTextTile'] = require('./views/molecules/MTextTile'); -skin['molecules.MatrixToolbar'] = require('./views/molecules/MatrixToolbar'); -skin['molecules.MemberInfo'] = require('./views/molecules/MemberInfo'); -skin['molecules.MemberTile'] = require('./views/molecules/MemberTile'); -skin['molecules.MessageComposer'] = require('./views/molecules/MessageComposer'); -skin['molecules.MessageContextMenu'] = require('./views/molecules/MessageContextMenu'); -skin['molecules.MessageTile'] = require('./views/molecules/MessageTile'); skin['molecules.ProgressBar'] = require('./views/molecules/ProgressBar'); skin['molecules.RoomCreate'] = require('./views/molecules/RoomCreate'); skin['molecules.RoomDropTarget'] = require('./views/molecules/RoomDropTarget'); @@ -83,6 +84,7 @@ skin['organisms.RoomList'] = require('./views/organisms/RoomList'); skin['organisms.RoomView'] = require('./views/organisms/RoomView'); skin['organisms.UserSettings'] = require('./views/organisms/UserSettings'); skin['organisms.ViewSource'] = require('./views/organisms/ViewSource'); +skin['pages.CompatibilityPage'] = require('./views/pages/CompatibilityPage'); skin['pages.MatrixChat'] = require('./views/pages/MatrixChat'); skin['templates.Login'] = require('./views/templates/Login'); skin['templates.Register'] = require('./views/templates/Register'); diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index 0cbeb86442..5f3d981e93 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -78,6 +78,8 @@ module.exports = React.createClass({ var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + if (!room) return []; + var userIds = room.getUsersReadUpTo(this.props.mxEvent); var MemberAvatar = sdk.getComponent('atoms.MemberAvatar'); From 0aa90d918cc1c1f44e0e3b239d517e52585b6eae Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 5 Nov 2015 14:45:16 +0000 Subject: [PATCH 05/17] bump js-sdk dep to develop --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eb9c3aff95..ff93588be8 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "flux": "~2.0.3", "linkifyjs": "^2.0.0-beta.4", "modernizr": "^3.1.0", - "matrix-js-sdk": "^0.3.0", + "matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop", "matrix-react-sdk": "^0.0.2", "q": "^1.4.1", "react": "^0.13.3", From e20388388eac450dbbc91e515ac74c0f2252c696 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 5 Nov 2015 17:40:37 +0000 Subject: [PATCH 06/17] null check --- src/controllers/organisms/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 5af8220bc3..c305a9c956 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -619,7 +619,7 @@ module.exports = { for (var i = this.state.room.timeline.length-1; i >= 0; --i) { var ev = this.state.room.timeline[i]; - if (ev.sender.userId == MatrixClientPeg.get().credentials.userId) { + if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { continue; } From c9823d07fd0be2658d5756d31d9b817941c6af04 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 10 Nov 2015 13:51:11 +0000 Subject: [PATCH 07/17] Limit number of read avatars, lay them out as per the design & order them. --- src/skins/vector/css/molecules/EventTile.css | 20 ++++++++--- src/skins/vector/views/molecules/EventTile.js | 33 +++++++++++++++---- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/skins/vector/css/molecules/EventTile.css b/src/skins/vector/css/molecules/EventTile.css index d2d8797677..d03b31b127 100644 --- a/src/skins/vector/css/molecules/EventTile.css +++ b/src/skins/vector/css/molecules/EventTile.css @@ -49,7 +49,6 @@ limitations under the License. .mx_EventTile .mx_MessageTimestamp { color: #acacac; font-size: 12px; - float: right; } .mx_EventTile_line { @@ -91,10 +90,16 @@ limitations under the License. .mx_EventTile_msgOption { float: right; + text-align: right; + margin-right: 10px; + z-index: 1; + position: relative; } .mx_MessageTimestamp { + display: block; visibility: hidden; + text-align: right; } .mx_EventTile_last .mx_MessageTimestamp { @@ -106,10 +111,10 @@ limitations under the License. } .mx_EventTile_editButton { - position: absolute; - right: 1px; - top: 15px; + display: block; visibility: hidden; + margin-left: auto; + margin-right: 0px; } .mx_EventTile:hover .mx_EventTile_editButton { @@ -125,10 +130,15 @@ limitations under the License. } .mx_EventTile_readAvatars { - float: right; } .mx_EventTile_readAvatars .mx_MemberAvatar { margin-left: 1px; margin-right: 1px; + vertical-align: middle; +} + +.mx_EventTile_readAvatarRemainder { + color: #acacac; + font-size: 12px; } diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index 5f3d981e93..5fd3ebe267 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -37,6 +37,8 @@ var eventTileTypes = { 'm.room.topic' : 'molecules.EventAsTextTile', }; +var MAX_READ_AVATARS = 5; + module.exports = React.createClass({ displayName: 'EventTile', mixins: [EventTileController], @@ -80,15 +82,30 @@ module.exports = React.createClass({ if (!room) return []; - var userIds = room.getUsersReadUpTo(this.props.mxEvent); + // get list of read receipts, sorted most recent first + var receipts = room.getReceiptsForEvent(this.props.mxEvent).filter(function(r) { + return r.type === "m.read"; + }).sort(function(r1, r2) { + return r2.data.ts - r1.data.ts; + }); var MemberAvatar = sdk.getComponent('atoms.MemberAvatar'); - for (var i = 0; i < userIds.length; ++i) { - var member = room.getMember(userIds[i]); - avatars.push( + for (var i = 0; i < receipts.length; ++i) { + var member = room.getMember(receipts[i].userId); + // add to the start so the most recent is on the end (ie. ends up rightmost) + avatars.unshift( ); + if (i + 1 >= MAX_READ_AVATARS) { + break; + } + } + var remainder = receipts.length - MAX_READ_AVATARS; + if (remainder > 0) { + avatars.unshift( + +{ remainder } + ); } return { avatars }; @@ -151,12 +168,14 @@ module.exports = React.createClass({ } return (
    +
    + { editButton } + { timestamp } + { readAvatars } +
    { avatar } { sender }
    - { timestamp } - { editButton } - { readAvatars }
    From 9a6624d1c76f92cb59d3336b463919c4faca14f4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 10 Nov 2015 17:44:59 +0000 Subject: [PATCH 08/17] Do read receipt avatars with absolute positioning: this should be a lot easier to animate. Also mess around with the MemberAvatar a bit so it's easier to style. --- src/skins/vector/css/atoms/MemberAvatar.css | 6 +++--- src/skins/vector/css/molecules/EventTile.css | 10 +++++++--- src/skins/vector/views/atoms/MemberAvatar.js | 10 ++++++---- src/skins/vector/views/molecules/EventTile.js | 18 +++++++++++++----- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/skins/vector/css/atoms/MemberAvatar.css b/src/skins/vector/css/atoms/MemberAvatar.css index 97dae35f7c..b8ecdef6fd 100644 --- a/src/skins/vector/css/atoms/MemberAvatar.css +++ b/src/skins/vector/css/atoms/MemberAvatar.css @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MemberAvatar { +.mx_MemberAvatar_image { z-index: 20; border-radius: 20px; } @@ -25,6 +25,6 @@ limitations under the License. text-align: center; } -.mx_MemberAvatar_wrapper { +.mx_MemberAvatar { position: relative; -} \ No newline at end of file +} diff --git a/src/skins/vector/css/molecules/EventTile.css b/src/skins/vector/css/molecules/EventTile.css index d03b31b127..7d4acadab6 100644 --- a/src/skins/vector/css/molecules/EventTile.css +++ b/src/skins/vector/css/molecules/EventTile.css @@ -130,15 +130,19 @@ limitations under the License. } .mx_EventTile_readAvatars { + position: relative; + display: inline-block; + width: 14px; + height: 14px; } .mx_EventTile_readAvatars .mx_MemberAvatar { - margin-left: 1px; - margin-right: 1px; - vertical-align: middle; + position: absolute; + display: inline-block; } .mx_EventTile_readAvatarRemainder { color: #acacac; font-size: 12px; + position: absolute; } diff --git a/src/skins/vector/views/atoms/MemberAvatar.js b/src/skins/vector/views/atoms/MemberAvatar.js index c4153b85c3..e7d6b65d78 100644 --- a/src/skins/vector/views/atoms/MemberAvatar.js +++ b/src/skins/vector/views/atoms/MemberAvatar.js @@ -49,20 +49,22 @@ module.exports = React.createClass({ initial = this.props.member.name[1].toUpperCase(); return ( - + { initial } - ); } return ( - + width={this.props.width} height={this.props.height} + style={this.props.style} + /> ); } }); diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index 5fd3ebe267..695240d00e 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -91,24 +91,32 @@ module.exports = React.createClass({ var MemberAvatar = sdk.getComponent('atoms.MemberAvatar'); + var left = 0; + for (var i = 0; i < receipts.length; ++i) { var member = room.getMember(receipts[i].userId); // add to the start so the most recent is on the end (ie. ends up rightmost) avatars.unshift( - + ); + left -= 15; if (i + 1 >= MAX_READ_AVATARS) { break; } } var remainder = receipts.length - MAX_READ_AVATARS; + var remText; if (remainder > 0) { - avatars.unshift( - +{ remainder } - ); + remText = +{ remainder }; } - return { avatars }; + return + {remText} + {avatars} + ; }, render: function() { From bc2c744bed4d35befa5051ee5fda3a656446bee3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 Nov 2015 11:42:51 +0000 Subject: [PATCH 09/17] 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'); From 9d620dfb1d34aa567a8db0cfef98091a9332c216 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 Nov 2015 16:43:54 +0000 Subject: [PATCH 10/17] Hopefully now mostly complete animations: we iterate through zero or more start states and then settle on the final place. --- src/Velociraptor.js | 39 ++++++++++++++----- src/controllers/organisms/RoomView.js | 2 +- src/skins/vector/views/molecules/EventTile.js | 19 +++++---- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 1a14381d4d..029dcf8d1a 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -38,10 +38,26 @@ module.exports = React.createClass({ if (oldNode.style.left != c.props.style.left) { Velocity(oldNode, { left: c.props.style.left }, self.props.transition); + console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left); } self.children[c.key] = old; } else { - self.children[c.key] = c; + // new element. If it has a startStyle, use that as the style and go through + // the enter animations + var newProps = { + ref: self.collectNode.bind(self, c.key) + }; + if (c.props.startStyle && Object.keys(c.props.startStyle).length) { + var startStyle = c.props.startStyle; + if (Array.isArray(startStyle)) { + startStyle = startStyle[0]; + } + newProps._restingStyle = c.props.style; + newProps.style = startStyle; + console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); + // apply the enter animations once it's mounted + } + self.children[c.key] = React.cloneElement(c, newProps); } }); }, @@ -49,20 +65,25 @@ module.exports = React.createClass({ collectNode: function(k, node) { if ( this.nodes[k] === undefined && - node.props.enterTransition && - Object.keys(node.props.enterTransition).length + node.props.startStyle && + Object.keys(node.props.startStyle).length ) { var domNode = ReactDom.findDOMNode(node); - var transitions = node.props.enterTransition; + var startStyles = node.props.startStyle; var transitionOpts = node.props.enterTransitionOpts; - if (!Array.isArray(transitions)) { - transitions = [ transitions ]; + if (!Array.isArray(startStyles)) { + startStyles = [ startStyles ]; transitionOpts = [ transitionOpts ]; } - for (var i = 0; i < transitions.length; ++i) { - Velocity(domNode, transitions[i], transitionOpts[i]); - console.log("enter: "+JSON.stringify(transitions[i])); + // start from startStyle 1: 0 is the one we gave it + // to start with, so now we animate 1 etc. + for (var i = 1; i < startStyles.length; ++i) { + Velocity(domNode, startStyles[i], transitionOpts[i-1]); + console.log("start: "+JSON.stringify(startStyles[i])); } + // and then we animate to the resting state + Velocity(domNode, node.props._restingStyle, transitionOpts[i-1]); + console.log("enter: "+JSON.stringify(node.props._restingStyle)); } this.nodes[k] = node; }, diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index d8832fa3cd..a7833b3d03 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -671,7 +671,7 @@ module.exports = { } var node = this.eventNodes[ev.getId()]; - if (node === undefined) continue; + if (!node) continue; var domNode = node.getDOMNode(); var boundingRect = domNode.getBoundingClientRect(); diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index e99f227fa8..4a0879f124 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -112,19 +112,22 @@ module.exports = React.createClass({ // 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 startStyles = []; var enterTransitionOpts = []; if (oldAvatarDomNode && this.readAvatarRect) { var oldRect = oldAvatarDomNode.getBoundingClientRect(); - startStyle.top = oldRect.top - this.readAvatarRect.top; + var topOffset = oldRect.top - this.readAvatarRect.top; if (oldAvatarDomNode.style.left !== '0px') { - startStyle.left = oldAvatarDomNode.style.left; - enterTransitions.push({ left: left+'px' }); + var leftOffset = oldAvatarDomNode.style.left; + // start at the old height and in the old h pos + startStyles.push({ top: topOffset, left: leftOffset }); enterTransitionOpts.push(transitionOpts); } - enterTransitions.push({ top: '0px' }); + + // then shift to the rightmost column, + // and then it will drop down to its resting position + startStyles.push({ top: topOffset, left: '0px' }); enterTransitionOpts.push(transitionOpts); } @@ -132,8 +135,8 @@ module.exports = React.createClass({ avatars.unshift( From d6b86598e540859dd57c5592fad1b623bd7bce3f Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Nov 2015 16:13:21 +0000 Subject: [PATCH 11/17] Bouncy bouncy! --- src/Velociraptor.js | 8 ++++---- src/VelocityBounce.js | 15 +++++++++++++++ src/skins/vector/views/molecules/EventTile.js | 19 +++++++++++++------ 3 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 src/VelocityBounce.js diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 029dcf8d1a..81ecd9e556 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -38,7 +38,7 @@ module.exports = React.createClass({ if (oldNode.style.left != c.props.style.left) { Velocity(oldNode, { left: c.props.style.left }, self.props.transition); - console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left); + //console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left); } self.children[c.key] = old; } else { @@ -54,7 +54,7 @@ module.exports = React.createClass({ } newProps._restingStyle = c.props.style; newProps.style = startStyle; - console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); + //console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); // apply the enter animations once it's mounted } self.children[c.key] = React.cloneElement(c, newProps); @@ -79,11 +79,11 @@ module.exports = React.createClass({ // to start with, so now we animate 1 etc. for (var i = 1; i < startStyles.length; ++i) { Velocity(domNode, startStyles[i], transitionOpts[i-1]); - console.log("start: "+JSON.stringify(startStyles[i])); + //console.log("start: "+JSON.stringify(startStyles[i])); } // and then we animate to the resting state Velocity(domNode, node.props._restingStyle, transitionOpts[i-1]); - console.log("enter: "+JSON.stringify(node.props._restingStyle)); + //console.log("enter: "+JSON.stringify(node.props._restingStyle)); } this.nodes[k] = node; }, diff --git a/src/VelocityBounce.js b/src/VelocityBounce.js new file mode 100644 index 0000000000..c85aa254fa --- /dev/null +++ b/src/VelocityBounce.js @@ -0,0 +1,15 @@ +var Velocity = require('velocity-animate'); + +// courtesy of https://github.com/julianshapiro/velocity/issues/283 +// We only use easeOutBounce (easeInBounce is just sort of nonsensical) +function bounce( p ) { + var pow2, + bounce = 4; + + while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {} + 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); +} diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index 4a0879f124..0a97d3ce5c 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -29,6 +29,7 @@ var ContextualMenu = require('../../../../ContextualMenu'); var TextForEvent = require('matrix-react-sdk/lib/TextForEvent'); var Velociraptor = require('../../../../Velociraptor'); +require('../../../../VelocityBounce'); var eventTileTypes = { 'm.room.message': 'molecules.MessageTile', @@ -100,9 +101,9 @@ module.exports = React.createClass({ var left = 0; - var transitionOpts = { - duration: 1000, - easing: 'linear' + var reorderTransitionOpts = { + duration: 100, + easing: 'easeOut' }; for (var i = 0; i < receipts.length; ++i) { @@ -122,13 +123,19 @@ module.exports = React.createClass({ var leftOffset = oldAvatarDomNode.style.left; // start at the old height and in the old h pos startStyles.push({ top: topOffset, left: leftOffset }); - enterTransitionOpts.push(transitionOpts); + enterTransitionOpts.push(reorderTransitionOpts); } // then shift to the rightmost column, // and then it will drop down to its resting position startStyles.push({ top: topOffset, left: '0px' }); - enterTransitionOpts.push(transitionOpts); + console.log(topOffset+': '+Math.min(Math.log(Math.abs(topOffset)) * 200, 3000)); + enterTransitionOpts.push({ + // Sort of make it take a bit longer to fall in a way + // that would make my A level physics teacher cry. + duration: Math.min(Math.log(Math.abs(topOffset)) * 200, 3000), + easing: 'easeOutBounce' + }); } // add to the start so the most recent is on the end (ie. ends up rightmost) @@ -154,7 +161,7 @@ module.exports = React.createClass({ return {remText} - + {avatars} ; From 816f20e06861072e2f1a988aedf7f9194b2690bc Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Nov 2015 16:36:01 +0000 Subject: [PATCH 12/17] comma --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 532a538366..700e00c8b9 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "react-dnd-html5-backend": "^2.0.0", "react-dom": "^0.14.2", "react-gemini-scrollbar": "^2.0.1", - "velocity-animate": "^1.2.3" + "velocity-animate": "^1.2.3", "sanitize-html": "^1.0.0" }, "devDependencies": { From 7f61a0252f79c786b86174470a66d251d890a249 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Nov 2015 16:45:28 +0000 Subject: [PATCH 13/17] remove logging --- src/skins/vector/views/molecules/EventTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index 0a97d3ce5c..068397be65 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -129,7 +129,6 @@ module.exports = React.createClass({ // then shift to the rightmost column, // and then it will drop down to its resting position startStyles.push({ top: topOffset, left: '0px' }); - console.log(topOffset+': '+Math.min(Math.log(Math.abs(topOffset)) * 200, 3000)); enterTransitionOpts.push({ // Sort of make it take a bit longer to fall in a way // that would make my A level physics teacher cry. From e23b90abd5e8b965d5c1bdc14eec569f00884eb5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 16 Nov 2015 16:52:07 +0000 Subject: [PATCH 14/17] More s/messageWrapper/messagePanel/ --- src/controllers/organisms/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index e128d07e41..cdc0638bd5 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -669,7 +669,7 @@ module.exports = { _getLastDisplayedEventIndexIgnoringOwn: function() { if (this.eventNodes === undefined) return null; - var messageWrapper = this.refs.messageWrapper; + var messageWrapper = this.refs.messagePanel; if (messageWrapper === undefined) return null; var wrapperRect = messageWrapper.getDOMNode().getBoundingClientRect(); From 80c2bd0c7f8b17e2ceb58c028d247c14d8a1b0dc Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 17 Nov 2015 15:51:00 +0000 Subject: [PATCH 15/17] Remove bouncing, set animation time to be constant (prevents temporary overalpping) and exclude ourselves. --- src/VelocityBounce.js | 15 --------------- src/skins/vector/views/molecules/EventTile.js | 11 +++++------ 2 files changed, 5 insertions(+), 21 deletions(-) delete mode 100644 src/VelocityBounce.js diff --git a/src/VelocityBounce.js b/src/VelocityBounce.js deleted file mode 100644 index c85aa254fa..0000000000 --- a/src/VelocityBounce.js +++ /dev/null @@ -1,15 +0,0 @@ -var Velocity = require('velocity-animate'); - -// courtesy of https://github.com/julianshapiro/velocity/issues/283 -// We only use easeOutBounce (easeInBounce is just sort of nonsensical) -function bounce( p ) { - var pow2, - bounce = 4; - - while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {} - 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); -} diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index 068397be65..39722c7c68 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -29,7 +29,6 @@ var ContextualMenu = require('../../../../ContextualMenu'); var TextForEvent = require('matrix-react-sdk/lib/TextForEvent'); var Velociraptor = require('../../../../Velociraptor'); -require('../../../../VelocityBounce'); var eventTileTypes = { 'm.room.message': 'molecules.MessageTile', @@ -90,9 +89,11 @@ module.exports = React.createClass({ if (!room) return []; + var myUserId = MatrixClientPeg.get().credentials.userId; + // get list of read receipts, sorted most recent first var receipts = room.getReceiptsForEvent(this.props.mxEvent).filter(function(r) { - return r.type === "m.read"; + return r.type === "m.read" && r.userId != myUserId; }).sort(function(r1, r2) { return r2.data.ts - r1.data.ts; }); @@ -130,10 +131,8 @@ module.exports = React.createClass({ // and then it will drop down to its resting position startStyles.push({ top: topOffset, left: '0px' }); enterTransitionOpts.push({ - // Sort of make it take a bit longer to fall in a way - // that would make my A level physics teacher cry. - duration: Math.min(Math.log(Math.abs(topOffset)) * 200, 3000), - easing: 'easeOutBounce' + duration: 300, + easing: 'easeOutCubic', }); } From da55081c68fd2514084bff53f5b90fc56911e3f2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 17 Nov 2015 15:59:44 +0000 Subject: [PATCH 16/17] Add member name to avatars as the title since if displayed without accompanying text (as with read receipts) they can be somewhat unhelpful. May as well have them all the time I think. --- src/skins/vector/views/atoms/MemberAvatar.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/skins/vector/views/atoms/MemberAvatar.js b/src/skins/vector/views/atoms/MemberAvatar.js index 26b660045d..c719d70c59 100644 --- a/src/skins/vector/views/atoms/MemberAvatar.js +++ b/src/skins/vector/views/atoms/MemberAvatar.js @@ -54,7 +54,7 @@ module.exports = React.createClass({ style={{ fontSize: (this.props.width * 0.75) + "px", width: this.props.width + "px", lineHeight: this.props.height*1.2 + "px" }}>{ initial } - ); @@ -63,6 +63,7 @@ module.exports = React.createClass({ ); From c63dd376d878c7a929fcc98923022a3f1e233572 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 17 Nov 2015 17:31:03 +0000 Subject: [PATCH 17/17] Fix member avatar initials (I failed at git conflict merging) --- src/skins/vector/css/atoms/MemberAvatar.css | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/skins/vector/css/atoms/MemberAvatar.css b/src/skins/vector/css/atoms/MemberAvatar.css index b7ea015b68..95ce820136 100644 --- a/src/skins/vector/css/atoms/MemberAvatar.css +++ b/src/skins/vector/css/atoms/MemberAvatar.css @@ -14,9 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_MemberAvatar_image { - z-index: 20; - border-radius: 20px; +.mx_MemberAvatar { position: relative; } @@ -27,6 +25,6 @@ limitations under the License. speak: none; } -.mx_MemberAvatar { - position: relative; +.mx_MemberAvatar_image { + border-radius: 20px; }