From b8fc9262556a4423f3110305ecd8e2fc882d2b91 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 27 Oct 2015 14:38:46 +0000 Subject: [PATCH 001/192] 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 21027cbf..ff36d4a1 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 002/192] 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 c5cb8195..caaada62 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 003/192] 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 746dd98a..ff62a41b 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 eb59711e..25fe9646 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 caaada62..0cbeb864 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 5b773b99c02c6c4c48109a55bc45cba6ad795982 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Nov 2015 18:56:55 +0000 Subject: [PATCH 004/192] Add basic m.video view support --- src/skins/vector/skindex.js | 3 + .../vector/views/molecules/MVideoTile.js | 88 +++++++++++++++++++ .../vector/views/molecules/MessageTile.js | 3 +- 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/skins/vector/views/molecules/MVideoTile.js diff --git a/src/skins/vector/skindex.js b/src/skins/vector/skindex.js index e715656c..69fe6117 100644 --- a/src/skins/vector/skindex.js +++ b/src/skins/vector/skindex.js @@ -30,6 +30,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.Spinner'] = require('./views/atoms/Spinner'); 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'); @@ -48,6 +49,7 @@ 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.MVideoTile'] = require('./views/molecules/MVideoTile'); skin['molecules.MatrixToolbar'] = require('./views/molecules/MatrixToolbar'); skin['molecules.MemberInfo'] = require('./views/molecules/MemberInfo'); skin['molecules.MemberTile'] = require('./views/molecules/MemberTile'); @@ -83,6 +85,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/MVideoTile.js b/src/skins/vector/views/molecules/MVideoTile.js new file mode 100644 index 00000000..3b33ea49 --- /dev/null +++ b/src/skins/vector/views/molecules/MVideoTile.js @@ -0,0 +1,88 @@ +/* +Copyright 2015 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 filesize = require('filesize'); + +var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); +var Modal = require('matrix-react-sdk/lib/Modal'); +var sdk = require('matrix-react-sdk') + +module.exports = React.createClass({ + displayName: 'MVideoTile', + + thumbScale: function(fullWidth, fullHeight, thumbWidth, thumbHeight) { + if (!fullWidth || !fullHeight) { + // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even + // log this because it's spammy + return undefined; + } + if (fullWidth < thumbWidth && fullHeight < thumbHeight) { + // no scaling needs to be applied + return fullHeight; + } + var widthMulti = thumbWidth / fullWidth; + var heightMulti = thumbHeight / fullHeight; + if (widthMulti < heightMulti) { + // width is the dominant dimension so scaling will be fixed on that + return widthMulti; + } + else { + // height is the dominant dimension so scaling will be fixed on that + return heightMulti; + } + }, + + render: function() { + var content = this.props.mxEvent.getContent(); + var cli = MatrixClientPeg.get(); + + var videoStyle = { + maxHeight: "500px", + maxWidth: "100%", + }; + + var height = null; + var width = null; + var poster = null; + var preload = "metadata"; + if (content.info) { + var scale = this.thumbScale(content.info.w, content.info.h, 480, 360); + if (scale) { + width = Math.floor(content.info.w * scale); + height = Math.floor(content.info.h * scale); + } + + if (content.info.thumbnail_url) { + poster = cli.mxcUrlToHttp(content.info.thumbnail_url); + preload = "none"; + } + } + + + + return ( + + + + ); + }, +}); diff --git a/src/skins/vector/views/molecules/MessageTile.js b/src/skins/vector/views/molecules/MessageTile.js index 6ea44413..4b63e971 100644 --- a/src/skins/vector/views/molecules/MessageTile.js +++ b/src/skins/vector/views/molecules/MessageTile.js @@ -40,7 +40,8 @@ module.exports = React.createClass({ 'm.notice': sdk.getComponent('molecules.MNoticeTile'), 'm.emote': sdk.getComponent('molecules.MEmoteTile'), 'm.image': sdk.getComponent('molecules.MImageTile'), - 'm.file': sdk.getComponent('molecules.MFileTile') + 'm.file': sdk.getComponent('molecules.MFileTile'), + 'm.video': sdk.getComponent('molecules.MVideoTile') }; var content = this.props.mxEvent.getContent(); From 7dc5f91fadb4eb8dc4ede753183796a0a234a2a3 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 3 Nov 2015 18:59:45 +0000 Subject: [PATCH 005/192] Remove unused code --- src/skins/vector/views/molecules/MVideoTile.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/skins/vector/views/molecules/MVideoTile.js b/src/skins/vector/views/molecules/MVideoTile.js index 3b33ea49..ec7ac6f8 100644 --- a/src/skins/vector/views/molecules/MVideoTile.js +++ b/src/skins/vector/views/molecules/MVideoTile.js @@ -52,11 +52,6 @@ module.exports = React.createClass({ var content = this.props.mxEvent.getContent(); var cli = MatrixClientPeg.get(); - var videoStyle = { - maxHeight: "500px", - maxWidth: "100%", - }; - var height = null; var width = null; var poster = null; From 4bf69923986a6feaeb80a27d24b2777817086a5b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 5 Nov 2015 14:16:15 +0000 Subject: [PATCH 006/192] 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 ff62a41b..5af8220b 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 e715656c..54dbad88 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 0cbeb864..5f3d981e 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 007/192] 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 eb9c3aff..ff93588b 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 008/192] 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 5af8220b..c305a9c9 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 009/192] 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 d2d87976..d03b31b1 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 5f3d981e..5fd3ebe2 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 010/192] 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 97dae35f..b8ecdef6 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 d03b31b1..7d4acada 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 c4153b85..e7d6b65d 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 5fd3ebe2..695240d0 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 05eda88ea264e73d6669b0efd20c6d19ae7428b8 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 Nov 2015 11:57:33 +0000 Subject: [PATCH 011/192] Split out logic/UI for logging in - Add 'PasswordLogin' UI component - Add 'LoginPage' wire component which, along with Signup from react SDK, replaces the 'Login' page. - Move UI code (state/props) from ServerConfig which was lobotomoised in the React SDK. Unfinished. --- src/skins/vector/skindex.js | 2 + .../vector/views/molecules/ServerConfig.js | 133 +++++++++++-- .../vector/views/organisms/PasswordLogin.js | 64 +++++++ src/skins/vector/views/pages/LoginPage.js | 176 ++++++++++++++++++ src/skins/vector/views/pages/MatrixChat.js | 7 +- 5 files changed, 367 insertions(+), 15 deletions(-) create mode 100644 src/skins/vector/views/organisms/PasswordLogin.js create mode 100644 src/skins/vector/views/pages/LoginPage.js diff --git a/src/skins/vector/skindex.js b/src/skins/vector/skindex.js index cf279c87..e05d3e65 100644 --- a/src/skins/vector/skindex.js +++ b/src/skins/vector/skindex.js @@ -71,6 +71,7 @@ skin['molecules.voip.CallView'] = require('./views/molecules/voip/CallView'); skin['molecules.voip.IncomingCallBox'] = require('./views/molecules/voip/IncomingCallBox'); skin['molecules.voip.VideoView'] = require('./views/molecules/voip/VideoView'); skin['organisms.CasLogin'] = require('./views/organisms/CasLogin'); +skin['organisms.PasswordLogin'] = require('./views/organisms/PasswordLogin'); skin['organisms.CreateRoom'] = require('./views/organisms/CreateRoom'); skin['organisms.ErrorDialog'] = require('./views/organisms/ErrorDialog'); skin['organisms.LeftPanel'] = require('./views/organisms/LeftPanel'); @@ -87,6 +88,7 @@ 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['pages.LoginPage'] = require('./views/pages/LoginPage'); skin['templates.Login'] = require('./views/templates/Login'); skin['templates.Register'] = require('./views/templates/Register'); diff --git a/src/skins/vector/views/molecules/ServerConfig.js b/src/skins/vector/views/molecules/ServerConfig.js index d6947727..fcc8bfc5 100644 --- a/src/skins/vector/views/molecules/ServerConfig.js +++ b/src/skins/vector/views/molecules/ServerConfig.js @@ -20,37 +20,142 @@ var React = require('react'); var Modal = require('matrix-react-sdk/lib/Modal'); var sdk = require('matrix-react-sdk') -var ServerConfigController = require('matrix-react-sdk/lib/controllers/molecules/ServerConfig') - +/** + * A pure UI component which displays the HS and IS to use. + */ module.exports = React.createClass({ displayName: 'ServerConfig', - mixins: [ServerConfigController], + + propTypes: { + onHsUrlChanged: React.PropTypes.func, + onIsUrlChanged: React.PropTypes.func, + defaultHsUrl: React.PropTypes.string, + defaultIsUrl: React.PropTypes.string, + withToggleButton: React.PropTypes.bool, + delayTimeMs: React.PropTypes.number // time to wait before invoking onChanged + }, + + getDefaultProps: function() { + return { + onHsUrlChanged: function() {}, + onIsUrlChanged: function() {}, + withToggleButton: false, + delayTimeMs: 0 + }; + }, + + getInitialState: function() { + return { + hs_url: this.props.defaultHsUrl, + is_url: this.props.defaultIsUrl, + original_hs_url: this.props.defaultHsUrl, + original_is_url: this.props.defaultIsUrl, + // no toggle button = show, toggle button = hide + configVisible: !this.props.withToggleButton + } + }, + + onHomeserverChanged: function(ev) { + this.setState({hs_url: ev.target.value}, function() { + this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, function() { + this.props.onHsUrlChanged(this.state.hs_url); + }); + }); + }, + + onIdentityServerChanged: function(ev) { + this.setState({is_url: ev.target.value}, function() { + this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, function() { + this.props.onIsUrlChanged(this.state.is_url); + }); + }); + }, + + _waitThenInvoke: function(existingTimeoutId, fn) { + if (existingTimeoutId) { + clearTimeout(existingTimeoutId); + } + return setTimeout(fn.bind(this), this.props.delayTimeMs); + }, + + getHsUrl: function() { + return this.state.hs_url; + }, + + getIsUrl: function() { + return this.state.is_url; + }, + + onServerConfigVisibleChange: function(ev) { + this.setState({ + configVisible: ev.target.checked + }); + }, showHelpPopup: function() { var ErrorDialog = sdk.getComponent('organisms.ErrorDialog'); Modal.createDialog(ErrorDialog, { title: 'Custom Server Options', description: - You can use the custom server options to log into other Matrix servers by specifying a different Home server URL.
    - This allows you to use Vector with an existing Matrix account on a different Home server.
    + You can use the custom server options to log into other Matrix + servers by specifying a different Home server URL.
    - You can also set a custom Identity server but this will affect people's ability to find you - if you use a server in a group other than the main Matrix.org group. + This allows you to use Vector with an existing Matrix account on + a different Home server. +
    +
    + You can also set a custom Identity server but this will affect + people's ability to find you if you use a server in a group other + than the main Matrix.org group.
    , button: "Dismiss", - focus: true, + focus: true }); }, render: function() { + var serverConfigStyle = {}; + serverConfigStyle.display = this.state.configVisible ? 'block' : 'none'; + + var toggleButton; + if (this.props.withToggleButton) { + toggleButton = ( +
    + + +
    + ); + } + return ( -
    - - - - - What does this mean? +
    + {toggleButton} +
    +
    + + + + + + What does this mean? + +
    +
    ); } }); diff --git a/src/skins/vector/views/organisms/PasswordLogin.js b/src/skins/vector/views/organisms/PasswordLogin.js new file mode 100644 index 00000000..ff277ced --- /dev/null +++ b/src/skins/vector/views/organisms/PasswordLogin.js @@ -0,0 +1,64 @@ +/* +Copyright 2015 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. +*/ + +var React = require('react'); +var ReactDOM = require('react-dom'); + +/** + * A pure UI component which displays a username/password form. + */ +module.exports = React.createClass({displayName: 'PasswordLogin', + propTypes: { + onSubmit: React.PropTypes.func.isRequired // fn(username, password) + }, + + getInitialState: function() { + return { + username: "", + password: "" + }; + }, + + onSubmitForm: function() { + this.props.onSubmit(this.state.username, this.state.password); + }, + + onUsernameChanged: function(ev) { + this.setState({username: ev.target.value}); + }, + + onPasswordChanged: function(ev) { + this.setState({password: ev.target.value}); + }, + + render: function() { + return ( +
    +
    + +
    + +
    + +
    +
    + ); + } +}); \ No newline at end of file diff --git a/src/skins/vector/views/pages/LoginPage.js b/src/skins/vector/views/pages/LoginPage.js new file mode 100644 index 00000000..7b4f636f --- /dev/null +++ b/src/skins/vector/views/pages/LoginPage.js @@ -0,0 +1,176 @@ +/* +Copyright 2015 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('matrix-react-sdk'); +var Signup = require("matrix-react-sdk/lib/Signup"); + +/** + * A wire component which glues together login UI components and Signup logic + */ +module.exports = React.createClass({displayName: 'LoginPage', + propTypes: { + onLoggedIn: React.PropTypes.func.isRequired, + homeserverUrl: React.PropTypes.string, + identityServerUrl: React.PropTypes.string, + // login shouldn't know or care how registration is done. + onRegisterClick: React.PropTypes.func.isRequired + }, + + getDefaultProps: function() { + return { + homeserverUrl: 'https://matrix.org/', + identityServerUrl: 'https://matrix.org/' + }; + }, + + getInitialState: function() { + return { + busy: false, + errorText: null, + enteredHomeserverUrl: this.props.homeserverUrl, + enteredIdentityServerUrl: this.props.identityServerUrl + }; + }, + + componentWillMount: function() { + this._initLoginLogic(); + }, + + onPasswordLogin: function(username, password) { + // TODO + console.log("onPasswordLogin %s %s", username, password); + }, + + onHsUrlChanged: function(newHsUrl) { + console.log("onHsUrlChanged %s", newHsUrl); + this._initLoginLogic(newHsUrl); + }, + + _initLoginLogic: function(hsUrl, isUrl) { + var self = this; + hsUrl = hsUrl || this.state.enteredHomeserverUrl; + isUrl = isUrl || this.state.enteredIdentityServerUrl; + + var loginLogic = new Signup.Login(hsUrl, isUrl); + this._loginLogic = loginLogic; + + loginLogic.getFlows().then(function(flows) { + // old behaviour was to always use the first flow without presenting + // options. This works in most cases (we don't have a UI for multiple + // logins so let's skip that for now). + loginLogic.chooseFlow(0); + }, function(err) { + self._setErrorTextFromError(err); + }).finally(function() { + self.setState({ + busy: false + }); + }); + + this.setState({ + enteredHomeserverUrl: hsUrl, + enteredIdentityServerUrl: isUrl, + busy: true, + errorText: null // reset err messages + }); + }, + + _getCurrentFlowStep: function() { + return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null + }, + + _setErrorTextFromError: function(err) { + var errCode = err.errcode; + if (!errCode && err.httpStatus) { + errCode = "HTTP " + err.httpStatus; + } + this.setState({ + errorText: ( + "Error: Problem communicating with the given homeserver " + + (errCode ? "(" + errCode + ")" : "") + ) + }); + }, + + componentForStep: function(step) { + switch (step) { + case 'm.login.password': + var PasswordLogin = sdk.getComponent('organisms.PasswordLogin'); + return ( + + ); + case 'm.login.cas': + var CasLogin = sdk.getComponent('organisms.CasLogin'); + return ( + + ); + default: + if (!step) { + return; + } + return ( +
    + Sorry, this homeserver is using a login which is not + recognised by Vector ({step}) +
    + ); + } + }, + + render: function() { + var Loader = sdk.getComponent("atoms.Spinner"); + var loader = this.state.busy ?
    : null; + var ServerConfig = sdk.getComponent("molecules.ServerConfig"); + + return ( +
    +
    +
    + vector +
    +
    +

    Sign in

    + {this.componentForStep(this._getCurrentFlowStep())} + +
    + { loader } + {this.state.errorText} +
    + + Create a new account + +
    +
    + blog  ·   + twitter  ·   + github  ·   + powered by Matrix +
    +
    +
    +
    + ); + } +}); diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 0553c25a..0fea1f36 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -89,6 +89,10 @@ module.exports = React.createClass({ }); }, + onRegisterClick: function() { + this.showScreen("register"); + }, + render: function() { var LeftPanel = sdk.getComponent('organisms.LeftPanel'); var RoomView = sdk.getComponent('organisms.RoomView'); @@ -164,8 +168,9 @@ module.exports = React.createClass({ /> ); } else { + var LoginPage = sdk.getComponent("pages.LoginPage"); return ( - + ); } } From 8826eb60cc0a4401adf59380c58def4c081aca63 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 Nov 2015 15:16:29 +0000 Subject: [PATCH 012/192] Call through to password login --- .../vector/views/organisms/PasswordLogin.js | 3 +- src/skins/vector/views/pages/LoginPage.js | 29 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/skins/vector/views/organisms/PasswordLogin.js b/src/skins/vector/views/organisms/PasswordLogin.js index ff277ced..fabd71d6 100644 --- a/src/skins/vector/views/organisms/PasswordLogin.js +++ b/src/skins/vector/views/organisms/PasswordLogin.js @@ -32,7 +32,8 @@ module.exports = React.createClass({displayName: 'PasswordLogin', }; }, - onSubmitForm: function() { + onSubmitForm: function(ev) { + ev.preventDefault(); this.props.onSubmit(this.state.username, this.state.password); }, diff --git a/src/skins/vector/views/pages/LoginPage.js b/src/skins/vector/views/pages/LoginPage.js index 7b4f636f..00f46a37 100644 --- a/src/skins/vector/views/pages/LoginPage.js +++ b/src/skins/vector/views/pages/LoginPage.js @@ -54,15 +54,30 @@ module.exports = React.createClass({displayName: 'LoginPage', }, onPasswordLogin: function(username, password) { - // TODO - console.log("onPasswordLogin %s %s", username, password); + var self = this; + self.setState({ + busy: true + }); + + this._loginLogic.loginViaPassword(username, password).then(function(data) { + self.props.onLoggedIn(data); + }, function(error) { + self._setErrorTextFromError(error); + }).finally(function() { + self.setState({ + busy: false + }); + }); }, onHsUrlChanged: function(newHsUrl) { - console.log("onHsUrlChanged %s", newHsUrl); this._initLoginLogic(newHsUrl); }, + onIsUrlChanged: function(newIsUrl) { + this._initLoginLogic(null, newIsUrl); + }, + _initLoginLogic: function(hsUrl, isUrl) { var self = this; hsUrl = hsUrl || this.state.enteredHomeserverUrl; @@ -97,6 +112,13 @@ module.exports = React.createClass({displayName: 'LoginPage', }, _setErrorTextFromError: function(err) { + if (err.friendlyText) { + this.setState({ + errorText: err.friendlyText + }); + return; + } + var errCode = err.errcode; if (!errCode && err.httpStatus) { errCode = "HTTP " + err.httpStatus; @@ -153,6 +175,7 @@ module.exports = React.createClass({displayName: 'LoginPage', defaultHsUrl={this.props.homeserverUrl} defaultIsUrl={this.props.identityServerUrl} onHsUrlChanged={this.onHsUrlChanged} + onIsUrlChanged={this.onIsUrlChanged} delayTimeMs={1000}/>
    { loader } From 58472b82516f015d3cc5cd7ff3f1027928157df0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 Nov 2015 15:38:04 +0000 Subject: [PATCH 013/192] Move Cas/PasswordLogin to react-sdk. Use them as normal components. --- src/skins/vector/skindex.js | 2 - src/skins/vector/views/organisms/CasLogin.js | 35 ---------- .../vector/views/organisms/PasswordLogin.js | 65 ------------------- src/skins/vector/views/pages/LoginPage.js | 4 +- 4 files changed, 2 insertions(+), 104 deletions(-) delete mode 100644 src/skins/vector/views/organisms/CasLogin.js delete mode 100644 src/skins/vector/views/organisms/PasswordLogin.js diff --git a/src/skins/vector/skindex.js b/src/skins/vector/skindex.js index e05d3e65..3ee4caec 100644 --- a/src/skins/vector/skindex.js +++ b/src/skins/vector/skindex.js @@ -70,8 +70,6 @@ skin['molecules.UserSelector'] = require('./views/molecules/UserSelector'); skin['molecules.voip.CallView'] = require('./views/molecules/voip/CallView'); skin['molecules.voip.IncomingCallBox'] = require('./views/molecules/voip/IncomingCallBox'); skin['molecules.voip.VideoView'] = require('./views/molecules/voip/VideoView'); -skin['organisms.CasLogin'] = require('./views/organisms/CasLogin'); -skin['organisms.PasswordLogin'] = require('./views/organisms/PasswordLogin'); skin['organisms.CreateRoom'] = require('./views/organisms/CreateRoom'); skin['organisms.ErrorDialog'] = require('./views/organisms/ErrorDialog'); skin['organisms.LeftPanel'] = require('./views/organisms/LeftPanel'); diff --git a/src/skins/vector/views/organisms/CasLogin.js b/src/skins/vector/views/organisms/CasLogin.js deleted file mode 100644 index ad9dbed9..00000000 --- a/src/skins/vector/views/organisms/CasLogin.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2015 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 CasLoginController = require('matrix-react-sdk/lib/controllers/organisms/CasLogin'); - -module.exports = React.createClass({ - displayName: 'CasLogin', - mixins: [CasLoginController], - - render: function() { - return ( -
    - -
    - ); - }, - -}); diff --git a/src/skins/vector/views/organisms/PasswordLogin.js b/src/skins/vector/views/organisms/PasswordLogin.js deleted file mode 100644 index fabd71d6..00000000 --- a/src/skins/vector/views/organisms/PasswordLogin.js +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright 2015 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. -*/ - -var React = require('react'); -var ReactDOM = require('react-dom'); - -/** - * A pure UI component which displays a username/password form. - */ -module.exports = React.createClass({displayName: 'PasswordLogin', - propTypes: { - onSubmit: React.PropTypes.func.isRequired // fn(username, password) - }, - - getInitialState: function() { - return { - username: "", - password: "" - }; - }, - - onSubmitForm: function(ev) { - ev.preventDefault(); - this.props.onSubmit(this.state.username, this.state.password); - }, - - onUsernameChanged: function(ev) { - this.setState({username: ev.target.value}); - }, - - onPasswordChanged: function(ev) { - this.setState({password: ev.target.value}); - }, - - render: function() { - return ( -
    -
    - -
    - -
    - -
    -
    - ); - } -}); \ No newline at end of file diff --git a/src/skins/vector/views/pages/LoginPage.js b/src/skins/vector/views/pages/LoginPage.js index 00f46a37..bfd2d4b9 100644 --- a/src/skins/vector/views/pages/LoginPage.js +++ b/src/skins/vector/views/pages/LoginPage.js @@ -20,6 +20,8 @@ var React = require('react'); var ReactDOM = require('react-dom'); var sdk = require('matrix-react-sdk'); var Signup = require("matrix-react-sdk/lib/Signup"); +var PasswordLogin = require("matrix-react-sdk/lib/components/PasswordLogin"); +var CasLogin = require("matrix-react-sdk/lib/components/CasLogin"); /** * A wire component which glues together login UI components and Signup logic @@ -134,12 +136,10 @@ module.exports = React.createClass({displayName: 'LoginPage', componentForStep: function(step) { switch (step) { case 'm.login.password': - var PasswordLogin = sdk.getComponent('organisms.PasswordLogin'); return ( ); case 'm.login.cas': - var CasLogin = sdk.getComponent('organisms.CasLogin'); return ( ); From 726afd30bb1c5823f284f6bf2342101880ed5fee Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 Nov 2015 15:49:32 +0000 Subject: [PATCH 014/192] Swap old login for new --- src/skins/vector/skindex.js | 3 +- .../views/pages/{LoginPage.js => Login.js} | 2 +- src/skins/vector/views/pages/MatrixChat.js | 5 +- src/skins/vector/views/templates/Login.js | 219 ------------------ 4 files changed, 4 insertions(+), 225 deletions(-) rename src/skins/vector/views/pages/{LoginPage.js => Login.js} (99%) delete mode 100644 src/skins/vector/views/templates/Login.js diff --git a/src/skins/vector/skindex.js b/src/skins/vector/skindex.js index 3ee4caec..84f26499 100644 --- a/src/skins/vector/skindex.js +++ b/src/skins/vector/skindex.js @@ -86,8 +86,7 @@ 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['pages.LoginPage'] = require('./views/pages/LoginPage'); -skin['templates.Login'] = require('./views/templates/Login'); +skin['pages.Login'] = require('./views/pages/Login'); skin['templates.Register'] = require('./views/templates/Register'); module.exports = skin; \ No newline at end of file diff --git a/src/skins/vector/views/pages/LoginPage.js b/src/skins/vector/views/pages/Login.js similarity index 99% rename from src/skins/vector/views/pages/LoginPage.js rename to src/skins/vector/views/pages/Login.js index bfd2d4b9..26c243a7 100644 --- a/src/skins/vector/views/pages/LoginPage.js +++ b/src/skins/vector/views/pages/Login.js @@ -26,7 +26,7 @@ var CasLogin = require("matrix-react-sdk/lib/components/CasLogin"); /** * A wire component which glues together login UI components and Signup logic */ -module.exports = React.createClass({displayName: 'LoginPage', +module.exports = React.createClass({displayName: 'Login', propTypes: { onLoggedIn: React.PropTypes.func.isRequired, homeserverUrl: React.PropTypes.string, diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 0fea1f36..f8708f0e 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -97,7 +97,6 @@ module.exports = React.createClass({ var LeftPanel = sdk.getComponent('organisms.LeftPanel'); var RoomView = sdk.getComponent('organisms.RoomView'); var RightPanel = sdk.getComponent('organisms.RightPanel'); - var Login = sdk.getComponent('templates.Login'); var UserSettings = sdk.getComponent('organisms.UserSettings'); var Register = sdk.getComponent('templates.Register'); var CreateRoom = sdk.getComponent('organisms.CreateRoom'); @@ -168,9 +167,9 @@ module.exports = React.createClass({ /> ); } else { - var LoginPage = sdk.getComponent("pages.LoginPage"); + var Login = sdk.getComponent("pages.Login"); return ( - + ); } } diff --git a/src/skins/vector/views/templates/Login.js b/src/skins/vector/views/templates/Login.js deleted file mode 100644 index 8bd9334e..00000000 --- a/src/skins/vector/views/templates/Login.js +++ /dev/null @@ -1,219 +0,0 @@ -/* -Copyright 2015 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('matrix-react-sdk') -var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); - -var LoginController = require('matrix-react-sdk/lib/controllers/templates/Login') - -var config = require('../../../../../config.json'); - -module.exports = React.createClass({ - displayName: 'Login', - mixins: [LoginController], - - getInitialState: function() { - // TODO: factor out all localstorage stuff into its own home. - // This is common to Login, Register and MatrixClientPeg - var localStorage = window.localStorage; - var hs_url, is_url; - if (localStorage) { - hs_url = localStorage.getItem("mx_hs_url"); - is_url = localStorage.getItem("mx_is_url"); - } - - return { - customHsUrl: hs_url || config.default_hs_url, - customIsUrl: is_url || config.default_is_url, - serverConfigVisible: (hs_url && hs_url !== config.default_hs_url || - is_url && is_url !== config.default_is_url) - }; - }, - - componentDidMount: function() { - this.onHSChosen(); - }, - - componentDidUpdate: function() { - if (!this.state.focusFired && this.refs.user) { - this.refs.user.focus(); - this.setState({ focusFired: true }); - } - }, - - getHsUrl: function() { - if (this.state.serverConfigVisible) { - return this.state.customHsUrl; - } else { - return config.default_hs_url; - } - }, - - getIsUrl: function() { - if (this.state.serverConfigVisible) { - return this.state.customIsUrl; - } else { - return config.default_is_url; - } - }, - - onServerConfigVisibleChange: function(ev) { - this.setState({ - serverConfigVisible: ev.target.checked - }, this.onHSChosen); - }, - - /** - * Gets the form field values for the current login stage - */ - getFormVals: function() { - return { - 'username': this.refs.user.value.trim(), - 'password': this.refs.pass.value.trim() - }; - }, - - onHsUrlChanged: function() { - var newHsUrl = this.refs.serverConfig.getHsUrl().trim(); - var newIsUrl = this.refs.serverConfig.getIsUrl().trim(); - - if (newHsUrl == this.state.customHsUrl && - newIsUrl == this.state.customIsUrl) - { - return; - } - else { - this.setState({ - customHsUrl: newHsUrl, - customIsUrl: newIsUrl, - }); - } - - // XXX: why are we replacing the MatrixClientPeg here when we're about - // to do it again 1s later in the setTimeout to onHSChosen? -- matthew - // Commenting it out for now to see what breaks. - /* - MatrixClientPeg.replaceUsingUrls( - this.getHsUrl(), - this.getIsUrl() - ); - this.setState({ - hs_url: this.getHsUrl(), - is_url: this.getIsUrl() - }); - */ - - // XXX: HSes do not have to offer password auth, so we - // need to update and maybe show a different component - // when a new HS is entered. - if (this.updateHsTimeout) { - clearTimeout(this.updateHsTimeout); - } - var self = this; - this.updateHsTimeout = setTimeout(function() { - self.onHSChosen(); - }, 1000); - }, - - componentForStep: function(step) { - switch (step) { - case 'choose_hs': - case 'fetch_stages': - var serverConfigStyle = {}; - serverConfigStyle.display = this.state.serverConfigVisible ? 'block' : 'none'; - var ServerConfig = sdk.getComponent("molecules.ServerConfig"); - - return ( -
    - - -
    - -
    -
    - ); - // XXX: clearly these should be separate organisms - case 'stage_m.login.password': - return ( -
    -
    -
    -
    - { this.componentForStep('choose_hs') } - -
    -
    - ); - case 'stage_m.login.cas': - var CasLogin = sdk.getComponent('organisms.CasLogin'); - return ( - - ); - } - }, - - onUsernameChanged: function(ev) { - this.setState({username: ev.target.value}); - }, - - onPasswordChanged: function(ev) { - this.setState({password: ev.target.value}); - }, - - loginContent: function() { - var Loader = sdk.getComponent("atoms.Spinner"); - var loader = this.state.busy ?
    : null; - return ( -
    -

    Sign in

    - {this.componentForStep(this.state.step)} -
    - { loader } - {this.state.errorText} -
    - Create a new account -
    -
    - blog  ·   - twitter  ·   - github  ·   - powered by Matrix -
    -
    - ); - }, - - render: function() { - return ( -
    -
    -
    - vector -
    - {this.loginContent()} -
    -
    - ); - } -}); From 021eaf5c29c21a75fa4aed66f5c905f0bf5a5c60 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 Nov 2015 15:54:07 +0000 Subject: [PATCH 015/192] Vector is the default IS in Vector --- src/skins/vector/views/pages/Login.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/skins/vector/views/pages/Login.js b/src/skins/vector/views/pages/Login.js index 26c243a7..b8f0e43e 100644 --- a/src/skins/vector/views/pages/Login.js +++ b/src/skins/vector/views/pages/Login.js @@ -38,7 +38,7 @@ module.exports = React.createClass({displayName: 'Login', getDefaultProps: function() { return { homeserverUrl: 'https://matrix.org/', - identityServerUrl: 'https://matrix.org/' + identityServerUrl: 'https://vector.im' }; }, From b1438355e28ff543d528b19f86a491cdad4dc771 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 Nov 2015 15:58:12 +0000 Subject: [PATCH 016/192] Github and Sublime don't like this not being escaped. Displays fine though in React like this. --- src/skins/vector/views/molecules/ServerConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/skins/vector/views/molecules/ServerConfig.js b/src/skins/vector/views/molecules/ServerConfig.js index fcc8bfc5..8f1eab38 100644 --- a/src/skins/vector/views/molecules/ServerConfig.js +++ b/src/skins/vector/views/molecules/ServerConfig.js @@ -105,7 +105,7 @@ module.exports = React.createClass({

    You can also set a custom Identity server but this will affect - people's ability to find you if you use a server in a group other + people's ability to find you if you use a server in a group other than the main Matrix.org group. , button: "Dismiss", From bc2c744bed4d35befa5051ee5fda3a656446bee3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 Nov 2015 11:42:51 +0000 Subject: [PATCH 017/192] 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 d71fdac5..7151aef8 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 00000000..1a14381d --- /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 e7d6b65d..9d632d72 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 695240d0..e99f227f 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 018/192] 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 1a14381d..029dcf8d 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 d8832fa3..a7833b3d 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 e99f227f..4a0879f1 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 019/192] 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 029dcf8d..81ecd9e5 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 00000000..c85aa254 --- /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 4a0879f1..0a97d3ce 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 020/192] comma --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 532a5383..700e00c8 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 021/192] 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 0a97d3ce..068397be 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 022/192] 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 e128d07e..cdc0638b 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 8602e0665db076cd18d463380594eb34ea64ccad Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 Nov 2015 10:57:44 +0000 Subject: [PATCH 023/192] PR feedback from #355 --- src/{skins/vector/views/pages => components/login}/Login.js | 2 +- .../vector/views/molecules => components/login}/ServerConfig.js | 0 src/skins/vector/skindex.js | 2 -- src/skins/vector/views/pages/MatrixChat.js | 2 +- src/skins/vector/views/templates/Register.js | 2 +- 5 files changed, 3 insertions(+), 5 deletions(-) rename src/{skins/vector/views/pages => components/login}/Login.js (98%) rename src/{skins/vector/views/molecules => components/login}/ServerConfig.js (100%) diff --git a/src/skins/vector/views/pages/Login.js b/src/components/login/Login.js similarity index 98% rename from src/skins/vector/views/pages/Login.js rename to src/components/login/Login.js index b8f0e43e..f9434092 100644 --- a/src/skins/vector/views/pages/Login.js +++ b/src/components/login/Login.js @@ -22,6 +22,7 @@ var sdk = require('matrix-react-sdk'); var Signup = require("matrix-react-sdk/lib/Signup"); var PasswordLogin = require("matrix-react-sdk/lib/components/PasswordLogin"); var CasLogin = require("matrix-react-sdk/lib/components/CasLogin"); +var ServerConfig = require("./ServerConfig"); /** * A wire component which glues together login UI components and Signup logic @@ -159,7 +160,6 @@ module.exports = React.createClass({displayName: 'Login', render: function() { var Loader = sdk.getComponent("atoms.Spinner"); var loader = this.state.busy ?
    : null; - var ServerConfig = sdk.getComponent("molecules.ServerConfig"); return (
    diff --git a/src/skins/vector/views/molecules/ServerConfig.js b/src/components/login/ServerConfig.js similarity index 100% rename from src/skins/vector/views/molecules/ServerConfig.js rename to src/components/login/ServerConfig.js diff --git a/src/skins/vector/skindex.js b/src/skins/vector/skindex.js index 84f26499..b1ac8499 100644 --- a/src/skins/vector/skindex.js +++ b/src/skins/vector/skindex.js @@ -64,7 +64,6 @@ skin['molecules.RoomTile'] = require('./views/molecules/RoomTile'); skin['molecules.RoomTooltip'] = require('./views/molecules/RoomTooltip'); skin['molecules.SearchBar'] = require('./views/molecules/SearchBar'); skin['molecules.SenderProfile'] = require('./views/molecules/SenderProfile'); -skin['molecules.ServerConfig'] = require('./views/molecules/ServerConfig'); skin['molecules.UnknownMessageTile'] = require('./views/molecules/UnknownMessageTile'); skin['molecules.UserSelector'] = require('./views/molecules/UserSelector'); skin['molecules.voip.CallView'] = require('./views/molecules/voip/CallView'); @@ -86,7 +85,6 @@ 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['pages.Login'] = require('./views/pages/Login'); skin['templates.Register'] = require('./views/templates/Register'); module.exports = skin; \ No newline at end of file diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index f8708f0e..5ed96c37 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -167,7 +167,7 @@ module.exports = React.createClass({ /> ); } else { - var Login = sdk.getComponent("pages.Login"); + var Login = require("../../../../components/login/Login"); return ( ); diff --git a/src/skins/vector/views/templates/Register.js b/src/skins/vector/views/templates/Register.js index 945d607c..8d6bbf42 100644 --- a/src/skins/vector/views/templates/Register.js +++ b/src/skins/vector/views/templates/Register.js @@ -22,6 +22,7 @@ var sdk = require('matrix-react-sdk') var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg') var RegisterController = require('../../../../controllers/templates/Register') +var ServerConfig = require("../../../../components/login/ServerConfig"); var config = require('../../../../../config.json'); @@ -102,7 +103,6 @@ module.exports = React.createClass({ case 'initial': var serverConfigStyle = {}; serverConfigStyle.display = this.state.serverConfigVisible ? 'block' : 'none'; - var ServerConfig = sdk.getComponent("molecules.ServerConfig"); return (
    From c57fb44c71686a13217a1ae0f6019b0aede9ca9b Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 Nov 2015 13:26:23 +0000 Subject: [PATCH 024/192] Fix path resolution --- src/components/login/Login.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/login/Login.js b/src/components/login/Login.js index f9434092..414528f1 100644 --- a/src/components/login/Login.js +++ b/src/components/login/Login.js @@ -20,8 +20,8 @@ var React = require('react'); var ReactDOM = require('react-dom'); var sdk = require('matrix-react-sdk'); var Signup = require("matrix-react-sdk/lib/Signup"); -var PasswordLogin = require("matrix-react-sdk/lib/components/PasswordLogin"); -var CasLogin = require("matrix-react-sdk/lib/components/CasLogin"); +var PasswordLogin = require("matrix-react-sdk/lib/components/login/PasswordLogin"); +var CasLogin = require("matrix-react-sdk/lib/components/login/CasLogin"); var ServerConfig = require("./ServerConfig"); /** From 714c96283e21f19f1d071f4e695b44651ae1c669 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 Nov 2015 15:12:55 +0000 Subject: [PATCH 025/192] Setting defaults from config.json got lost --- src/skins/vector/views/pages/MatrixChat.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 5ed96c37..e4e031d8 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -24,6 +24,7 @@ var MatrixChatController = require('matrix-react-sdk/lib/controllers/pages/Matri var dis = require('matrix-react-sdk/lib/dispatcher'); var Matrix = require("matrix-js-sdk"); var ContextualMenu = require("../../../../ContextualMenu"); +var config = require("../../../../../config.json"); module.exports = React.createClass({ displayName: 'MatrixChat', @@ -169,7 +170,11 @@ module.exports = React.createClass({ } else { var Login = require("../../../../components/login/Login"); return ( - + ); } } From 80c2bd0c7f8b17e2ceb58c028d247c14d8a1b0dc Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 17 Nov 2015 15:51:00 +0000 Subject: [PATCH 026/192] 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 c85aa254..00000000 --- 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 068397be..39722c7c 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 027/192] 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 26b66004..c719d70c 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 028/192] 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 b7ea015b..95ce8201 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; } From 8e8b27c893ab8797e6100ecdfca37b73ccfe3aba Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 Nov 2015 17:40:31 +0000 Subject: [PATCH 029/192] Add RegistrationForm UI component and new Registration wire component Hook it up to MatrixChat instead of the existing logic (this breaks reg). WIP. --- src/components/login/Registration.js | 147 +++++++++++++++++++++ src/components/login/RegistrationForm.js | 126 ++++++++++++++++++ src/skins/vector/views/pages/MatrixChat.js | 20 ++- 3 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 src/components/login/Registration.js create mode 100644 src/components/login/RegistrationForm.js diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js new file mode 100644 index 00000000..a1ebe57c --- /dev/null +++ b/src/components/login/Registration.js @@ -0,0 +1,147 @@ +/* +Copyright 2015 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 sdk = require('matrix-react-sdk'); +var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); +var ServerConfig = require("./ServerConfig"); +var RegistrationForm = require("./RegistrationForm"); + +module.exports = React.createClass({ + displayName: 'Registration', + + propTypes: { + onLoggedIn: React.PropTypes.func.isRequired, + registerLogic: React.PropTypes.any.isRequired, + // registration shouldn't know or care how login is done. + onLoginClick: React.PropTypes.func.isRequired + }, + + getInitialState: function() { + return { + busy: false, + errorText: null, + enteredHomeserverUrl: this.props.registerLogic.getHomeserverUrl(), + enteredIdentityServerUrl: this.props.registerLogic.getIdentityServerUrl() + }; + }, + + componentWillMount: function() { + + }, + + onHsUrlChanged: function(newHsUrl) { + this.props.registerLogic.setHomeserverUrl(newHsUrl); + this.forceUpdate(); // registration state may have changed. + }, + + onIsUrlChanged: function(newIsUrl) { + this.props.registerLogic.setIdentityServerUrl(newIsUrl); + this.forceUpdate(); // registration state may have changed. + }, + + onFormSubmit: function(formVals) { + console.log("Form vals: %s", formVals); + }, + + onFormValidationFailed: function(errCode) { + console.error("Ruh roh: %s", errCode); + }, + + _getRegisterContentJsx: function() { + var currState = this.props.registerLogic.getState(); + var registerStep; + switch (currState) { + case "Register.COMPLETE": + return this._getPostRegisterJsx(); + case "Register.START": + registerStep = ( + + ); + break; + case "Register.STEP_m.login.email.identity": + registerStep = ( +
    + Please check your email to continue registration. +
    + ); + break; + case "Register.STEP_m.login.recaptcha": + registerStep = ( +
    + This Home Server would like to make sure you are not a robot +
    +
    + ); + break; + default: + console.error("Unknown register state: %s", currState); + break; + } + return ( +
    +

    Create an account

    + {registerStep} + +
    {this.state.errorText}
    + + I already have an account + +
    + ); + }, + + _getPostRegisterJsx: function() { + var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName'); + var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar'); + return ( +
    + Set a display name: + + Upload an avatar: + + +
    + ); + }, + + render: function() { + return ( +
    +
    +
    + vector +
    + {this._getRegisterContentJsx()} +
    +
    + ); + } +}); diff --git a/src/components/login/RegistrationForm.js b/src/components/login/RegistrationForm.js new file mode 100644 index 00000000..3f9fb6ae --- /dev/null +++ b/src/components/login/RegistrationForm.js @@ -0,0 +1,126 @@ +/* +Copyright 2015 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 sdk = require('matrix-react-sdk') + +/** + * A pure UI component which displays a registration form. + */ +module.exports = React.createClass({ + displayName: 'RegistrationForm', + + propTypes: { + defaultEmail: React.PropTypes.string, + defaultUsername: React.PropTypes.string, + showEmail: React.PropTypes.bool, + minPasswordLength: React.PropTypes.number, + onError: React.PropTypes.func, + onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise + }, + + getDefaultProps: function() { + return { + showEmail: false, + minPasswordLength: 6, + onError: function(e) { + console.error(e); + } + }; + }, + + getInitialState: function() { + return { + email: this.props.defaultEmail, + username: this.props.defaultUsername, + password: null, + passwordConfirm: null + }; + }, + + onSubmit: function(ev) { + ev.preventDefault(); + + var pwd1 = this.refs.password.value.trim(); + var pwd2 = this.refs.passwordConfirm.value.trim() + + var errCode; + if (!pwd1 || !pwd2) { + errCode = "RegistrationForm.ERR_PASSWORD_MISSING"; + } + else if (pwd1 !== pwd2) { + errCode = "RegistrationForm.ERR_PASSWORD_MISMATCH"; + } + else if (pwd1.length < this.props.minPasswordLength) { + errCode = "RegistrationForm.ERR_PASSWORD_LENGTH"; + } + if (errCode) { + this.props.onError(errCode); + return; + } + + var promise = this.props.onRegisterClick({ + username: this.refs.username.value.trim(), + password: pwd1, + email: this.refs.email.value.trim() + }); + + if (promise) { + ev.target.disabled = true; + promise.finally(function() { + ev.target.disabled = false; + }); + } + }, + + render: function() { + var emailSection, registerButton; + if (this.props.showEmail) { + emailSection = ( + + ); + } + if (this.props.onRegisterClick) { + registerButton = ( + + ); + } + + return ( +
    + + {emailSection} +
    + +
    + +
    + +
    + {registerButton} + +
    + ); + } +}); diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 5ed96c37..9c8872e0 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -23,7 +23,10 @@ var MatrixChatController = require('matrix-react-sdk/lib/controllers/pages/Matri var dis = require('matrix-react-sdk/lib/dispatcher'); var Matrix = require("matrix-js-sdk"); -var ContextualMenu = require("../../../../ContextualMenu"); +var ContextualMenu = require("../../../../ContextualMenu") +var Login = require("../../../../components/login/Login"); +var Registration = require("../../../../components/login/Registration"); +var Signup = require("matrix-react-sdk/lib/Signup"); module.exports = React.createClass({ displayName: 'MatrixChat', @@ -93,12 +96,15 @@ module.exports = React.createClass({ this.showScreen("register"); }, + onLoginClick: function() { + this.showScreen("login"); + }, + render: function() { var LeftPanel = sdk.getComponent('organisms.LeftPanel'); var RoomView = sdk.getComponent('organisms.RoomView'); var RightPanel = sdk.getComponent('organisms.RightPanel'); var UserSettings = sdk.getComponent('organisms.UserSettings'); - var Register = sdk.getComponent('templates.Register'); var CreateRoom = sdk.getComponent('organisms.CreateRoom'); var RoomDirectory = sdk.getComponent('organisms.RoomDirectory'); var MatrixToolbar = sdk.getComponent('molecules.MatrixToolbar'); @@ -159,15 +165,23 @@ module.exports = React.createClass({ ); } else if (this.state.screen == 'register') { + /* return ( + ); */ + return ( + ); } else { - var Login = require("../../../../components/login/Login"); return ( ); From c42d4f901bdfd4e44ca9b6cc523e78e5d7975c54 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Wed, 18 Nov 2015 10:46:32 +0000 Subject: [PATCH 030/192] Don't mark rooms as unread on m.room.member changes A quick and hacky fix to issue #169. --- package.json | 3 ++- src/controllers/organisms/RoomList.js | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index fb7558ad..83010b1e 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "parallelshell": "^1.2.0", "rimraf": "^2.4.3", "source-map-loader": "^0.1.5", - "uglifycss": "0.0.15" + "uglifycss": "0.0.15", + "webpack": "^1.12.6" } } diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index 37d4a4e4..2a01527e 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -88,24 +88,33 @@ module.exports = { onRoomTimeline: function(ev, room, toStartOfTimeline) { if (toStartOfTimeline) return; - var newState = this.getRoomLists(); + var hl = 0; if ( room.roomId != this.props.selectedRoom && ev.getSender() != MatrixClientPeg.get().credentials.userId) { - var hl = 1; + // don't mark rooms as unread for just member changes + if (ev.getType() != "m.room.member") { + hl = 1; + } var actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions && actions.tweaks && actions.tweaks.highlight) { hl = 2; } + } + + if (hl > 0) { + var newState = this.getRoomLists(); + // obviously this won't deep copy but this shouldn't be necessary var amap = this.state.activityMap; amap[room.roomId] = Math.max(amap[room.roomId] || 0, hl); newState.activityMap = amap; + + this.setState(newState); } - this.setState(newState); }, onRoomName: function(room) { From 025b9e2fc8b7f8a4ea427f09bea94ffd03d8387f Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Nov 2015 14:54:32 +0000 Subject: [PATCH 031/192] depend on react sdk dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 700e00c8..bc6e3496 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "flux": "~2.0.3", "linkifyjs": "^2.0.0-beta.4", "matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop", - "matrix-react-sdk": "^0.0.2", + "matrix-react-sdk": "https://github.com/matrix-org/matrix-react-sdk.git#develop", "modernizr": "^3.1.0", "q": "^1.4.1", "react": "^0.14.2", From b4c0625961c3c95302d0cc666e237af7b59eecae Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 18 Nov 2015 15:32:44 +0000 Subject: [PATCH 032/192] Show validation errors --- src/components/login/Registration.js | 53 +++++++++++++++------- src/skins/vector/views/pages/MatrixChat.js | 11 +++-- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index a1ebe57c..c44d1368 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -22,6 +22,7 @@ var sdk = require('matrix-react-sdk'); var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); var ServerConfig = require("./ServerConfig"); var RegistrationForm = require("./RegistrationForm"); +var MIN_PASSWORD_LENGTH = 6; module.exports = React.createClass({ displayName: 'Registration', @@ -61,7 +62,40 @@ module.exports = React.createClass({ }, onFormValidationFailed: function(errCode) { - console.error("Ruh roh: %s", errCode); + var errMsg; + switch (errCode) { + case "RegistrationForm.ERR_PASSWORD_MISSING": + errMsg = "Missing password."; + break; + case "RegistrationForm.ERR_PASSWORD_MISMATCH": + errMsg = "Passwords don't match."; + break; + case "RegistrationForm.ERR_PASSWORD_LENGTH": + errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`; + break; + default: + console.error("Unknown error code: %s", errCode); + errMsg = "An unknown error occurred."; + break; + } + this.setState({ + errorText: errMsg + }); + }, + + _getPostRegisterJsx: function() { + var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName'); + var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar'); + return ( +
    + Set a display name: + + Upload an avatar: + + +
    + ); }, _getRegisterContentJsx: function() { @@ -74,7 +108,7 @@ module.exports = React.createClass({ registerStep = ( ); @@ -117,21 +151,6 @@ module.exports = React.createClass({ ); }, - _getPostRegisterJsx: function() { - var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName'); - var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar'); - return ( -
    - Set a display name: - - Upload an avatar: - - -
    - ); - }, - render: function() { return (
    diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 51de92a7..85506c21 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -176,12 +176,17 @@ module.exports = React.createClass({ /> ); */ return ( + var registerLogic = new Signup.Register( + config.default_hs_url, config.default_is_url + ); + registerLogic.setClientSecret(this.state.register_client_secret); + registerLogic.setSessionId(this.state.register_session_id); + registerLogic.setRegistrationUrl(this.props.registrationUrl); + registerLogic.setIdSid(this.state.register_id_sid); + registerLogic={registerLogic} /> ); } else { return ( From f0df3f29b9b896dca0d7483cd2fd0ea3269110d7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 18 Nov 2015 17:12:17 +0000 Subject: [PATCH 033/192] Show all read avatars on click --- src/Velociraptor.js | 11 +++++- src/skins/vector/views/molecules/EventTile.js | 38 ++++++++++++++----- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 81ecd9e5..df6b3c95 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -37,7 +37,16 @@ module.exports = React.createClass({ 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); + Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() { + // special case visibility because it's nonsensical to animate an invisible element + // so we always hidden->visible pre-transition and visible->hidden after + if (oldNode.style.visibility == 'visible' && c.props.style.visibility == 'hidden') { + oldNode.style.visibility = c.props.style.visibility; + } + }); + if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') { + oldNode.style.visibility = c.props.style.visibility; + } //console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left); } self.children[c.key] = old; diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index 39722c7c..fa1604f9 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -58,7 +58,7 @@ module.exports = React.createClass({ }, getInitialState: function() { - return {menu: false}; + return {menu: false, allReadAvatars: false}; }, componentDidUpdate: function() { @@ -82,6 +82,12 @@ module.exports = React.createClass({ this.setState({menu: true}); }, + toggleAllReadAvatars: function() { + this.setState({ + allReadAvatars: !this.state.allReadAvatars + }); + }, + getReadAvatars: function() { var avatars = []; @@ -136,25 +142,39 @@ module.exports = React.createClass({ }); } + var style = { + left: left+'px', + top: '0px', + visibility: i < MAX_READ_AVATARS || this.state.allReadAvatars ? 'visible' : 'hidden' + }; + // 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; + // TODO: we keep the extra read avatars in the dom to make animation simpler + // we could optimise this to reduce the dom size. + if (i < MAX_READ_AVATARS - 1 || this.state.allReadAvatars) { + left -= 15; } } - var remainder = receipts.length - MAX_READ_AVATARS; - var remText; - if (remainder > 0) { - remText = +{ remainder }; + if (!this.state.allReadAvatars) { + var remainder = receipts.length - MAX_READ_AVATARS; + var remText; + left -= 15; + if (remainder > 0) { + remText = +{ remainder } + ; + } } return From 5424567a66b9a1ecc6a1808abce4bb9d85e0e547 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 18 Nov 2015 17:15:20 +0000 Subject: [PATCH 034/192] Hook up onFormSubmit to make registration (dummy only) work again. --- src/components/login/Registration.js | 46 +++++++++++++++++++--- src/skins/vector/views/pages/MatrixChat.js | 12 +++--- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index c44d1368..05960c61 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -20,6 +20,7 @@ var React = require('react'); var sdk = require('matrix-react-sdk'); var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); +var dis = require('matrix-react-sdk/lib/dispatcher'); var ServerConfig = require("./ServerConfig"); var RegistrationForm = require("./RegistrationForm"); var MIN_PASSWORD_LENGTH = 6; @@ -44,7 +45,11 @@ module.exports = React.createClass({ }, componentWillMount: function() { + this.dispatcherRef = dis.register(this.onAction); + }, + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); }, onHsUrlChanged: function(newHsUrl) { @@ -57,8 +62,38 @@ module.exports = React.createClass({ this.forceUpdate(); // registration state may have changed. }, + onAction: function(payload) { + if (payload.action !== "registration_step_update") { + return; + } + this.forceUpdate(); + }, + onFormSubmit: function(formVals) { - console.log("Form vals: %s", formVals); + var self = this; + this.props.registerLogic.register(formVals).done(function(response) { + if (!response || !response.access_token) { + console.warn( + "FIXME: Register fulfilled without a final response, " + + "did you break the promise chain?" + ); + // no matter, we'll grab it direct + response = self.props.registerLogic.getCredentials(); + } + self.props.onLoggedIn({ + userId: response.user_id, + homeserverUrl: self.props.registerLogic.getHomeserverUrl(), + identityServerUrl: self.props.registerLogic.getIdentityServerUrl(), + accessToken: response.access_token + }); + }, function(err) { + if (err.message) { + self.setState({ + errorText: err.message + }); + } + console.log(err); + }); }, onFormValidationFailed: function(errCode) { @@ -99,12 +134,13 @@ module.exports = React.createClass({ }, _getRegisterContentJsx: function() { - var currState = this.props.registerLogic.getState(); + var currStep = this.props.registerLogic.getStep(); var registerStep; - switch (currState) { + switch (currStep) { case "Register.COMPLETE": return this._getPostRegisterJsx(); case "Register.START": + case "Register.STEP_m.login.dummy": registerStep = (

    Create an account

    {registerStep} +
    {this.state.errorText}
    -
    {this.state.errorText}
    I already have an account diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 85506c21..7dafff91 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -175,14 +175,14 @@ module.exports = React.createClass({ registrationUrl={this.props.registrationUrl} /> ); */ - return ( - var registerLogic = new Signup.Register( + var registerLogic = new Signup.Register( config.default_hs_url, config.default_is_url ); - registerLogic.setClientSecret(this.state.register_client_secret); - registerLogic.setSessionId(this.state.register_session_id); - registerLogic.setRegistrationUrl(this.props.registrationUrl); - registerLogic.setIdSid(this.state.register_id_sid); + registerLogic.setClientSecret(this.state.register_client_secret); + registerLogic.setSessionId(this.state.register_session_id); + registerLogic.setRegistrationUrl(this.props.registrationUrl); + registerLogic.setIdSid(this.state.register_id_sid); + return ( Date: Wed, 18 Nov 2015 17:46:17 +0000 Subject: [PATCH 035/192] Load the Recaptcha script if we have a container for it This is complex enough that the Registration component shouldn't have to care about it, so it should probably be split into a pure UI component. --- src/components/login/Registration.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/login/Registration.js b/src/components/login/Registration.js index 05960c61..ea121ba0 100644 --- a/src/components/login/Registration.js +++ b/src/components/login/Registration.js @@ -48,25 +48,41 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); }, + componentDidUpdate: function() { + // Just putting a script tag into the returned jsx doesn't work, annoyingly, + // so we do this instead. + var self = this; + if (this.refs.recaptchaContainer) { + console.log("Loading recaptcha script..."); + var scriptTag = document.createElement('script'); + window.mx_on_recaptcha_loaded = function() { + console.log("Loaded recaptcha script."); + self.props.registerLogic.tellStage("m.login.recaptcha", "loaded"); + }; + scriptTag.setAttribute( + 'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit" + ); + this.refs.recaptchaContainer.appendChild(scriptTag); + } + }, + componentWillUnmount: function() { dis.unregister(this.dispatcherRef); }, onHsUrlChanged: function(newHsUrl) { this.props.registerLogic.setHomeserverUrl(newHsUrl); - this.forceUpdate(); // registration state may have changed. }, onIsUrlChanged: function(newIsUrl) { this.props.registerLogic.setIdentityServerUrl(newIsUrl); - this.forceUpdate(); // registration state may have changed. }, onAction: function(payload) { if (payload.action !== "registration_step_update") { return; } - this.forceUpdate(); + this.forceUpdate(); // registration state has changed. }, onFormSubmit: function(formVals) { From 742ae354e57c3a9c2a4a58d2766fa6977636e553 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 18 Nov 2015 20:15:15 +0000 Subject: [PATCH 036/192] clicking anywhere in the composer pane should focus on the textarea --- src/skins/vector/views/molecules/MessageComposer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/skins/vector/views/molecules/MessageComposer.js b/src/skins/vector/views/molecules/MessageComposer.js index 2f0e7ac5..51f3a115 100644 --- a/src/skins/vector/views/molecules/MessageComposer.js +++ b/src/skins/vector/views/molecules/MessageComposer.js @@ -28,6 +28,10 @@ module.exports = React.createClass({ displayName: 'MessageComposer', mixins: [MessageComposerController], + onInputClick: function(ev) { + this.refs.textarea.focus(); + }, + onUploadClick: function(ev) { this.refs.uploadInput.click(); }, @@ -60,7 +64,7 @@ module.exports = React.createClass({
    -
    +
    - cancel_button =
    Cancel
    - save_button =
    Save Changes
    - } else { - // - name = -
    -
    { this.props.room.name }
    -
    - -
    -
    - if (topic) topic_el =
    { topic.getContent().topic }
    ; - } - - var roomAvatar = null; - if (this.props.room) { - roomAvatar = ( - - ); - } - - var zoom_button, video_button, voice_button; - if (activeCall) { - if (activeCall.type == "video") { - zoom_button = ( -
    - Fullscreen -
    - ); - } - video_button = -
    - Video call -
    ; - voice_button = -
    - VoIP call -
    ; - } - - header = -
    -
    -
    - { roomAvatar } -
    -
    - { name } - { topic_el } -
    -
    - {call_buttons} - {cancel_button} - {save_button} -
    - { video_button } - { voice_button } - { zoom_button } -
    - Search -
    -
    -
    - } - - return ( -
    - { header } -
    - ); - }, -}); diff --git a/src/skins/vector/views/molecules/RoomSettings.js b/src/skins/vector/views/molecules/RoomSettings.js deleted file mode 100644 index 11d80aee..00000000 --- a/src/skins/vector/views/molecules/RoomSettings.js +++ /dev/null @@ -1,232 +0,0 @@ -/* -Copyright 2015 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 MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); -var sdk = require('matrix-react-sdk'); - -var RoomSettingsController = require('matrix-react-sdk/lib/controllers/molecules/RoomSettings') - -module.exports = React.createClass({ - displayName: 'RoomSettings', - mixins: [RoomSettingsController], - - getTopic: function() { - return this.refs.topic.value; - }, - - getJoinRules: function() { - return this.refs.is_private.checked ? "invite" : "public"; - }, - - getHistoryVisibility: function() { - return this.refs.share_history.checked ? "shared" : "invited"; - }, - - getPowerLevels: function() { - if (!this.state.power_levels_changed) return undefined; - - var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', ''); - power_levels = power_levels.getContent(); - - var new_power_levels = { - ban: parseInt(this.refs.ban.value), - kick: parseInt(this.refs.kick.value), - redact: parseInt(this.refs.redact.value), - invite: parseInt(this.refs.invite.value), - events_default: parseInt(this.refs.events_default.value), - state_default: parseInt(this.refs.state_default.value), - users_default: parseInt(this.refs.users_default.value), - users: power_levels.users, - events: power_levels.events, - }; - - return new_power_levels; - }, - - onPowerLevelsChanged: function() { - this.setState({ - power_levels_changed: true - }); - }, - - render: function() { - var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); - - var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); - if (topic) topic = topic.getContent().topic; - - var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', ''); - if (join_rule) join_rule = join_rule.getContent().join_rule; - - var history_visibility = this.props.room.currentState.getStateEvents('m.room.history_visibility', ''); - if (history_visibility) history_visibility = history_visibility.getContent().history_visibility; - - var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', ''); - - var events_levels = power_levels.events || {}; - - if (power_levels) { - power_levels = power_levels.getContent(); - - var ban_level = parseInt(power_levels.ban); - var kick_level = parseInt(power_levels.kick); - var redact_level = parseInt(power_levels.redact); - var invite_level = parseInt(power_levels.invite || 0); - var send_level = parseInt(power_levels.events_default || 0); - var state_level = parseInt(power_levels.state_default || 0); - var default_user_level = parseInt(power_levels.users_default || 0); - - if (power_levels.ban == undefined) ban_level = 50; - if (power_levels.kick == undefined) kick_level = 50; - if (power_levels.redact == undefined) redact_level = 50; - - var user_levels = power_levels.users || {}; - - var user_id = MatrixClientPeg.get().credentials.userId; - - var current_user_level = user_levels[user_id]; - if (current_user_level == undefined) current_user_level = default_user_level; - - var power_level_level = events_levels["m.room.power_levels"]; - if (power_level_level == undefined) { - power_level_level = state_level; - } - - var can_change_levels = current_user_level >= power_level_level; - } else { - var ban_level = 50; - var kick_level = 50; - var redact_level = 50; - var invite_level = 0; - var send_level = 0; - var state_level = 0; - var default_user_level = 0; - - var user_levels = []; - var events_levels = []; - - var current_user_level = 0; - - var power_level_level = 0; - - var can_change_levels = false; - } - - var room_avatar_level = parseInt(power_levels.state_default || 0); - if (events_levels['m.room.avatar'] !== undefined) { - room_avatar_level = events_levels['m.room.avatar']; - } - var can_set_room_avatar = current_user_level >= room_avatar_level; - - var change_avatar; - if (can_set_room_avatar) { - change_avatar =
    -

    Room Icon

    - -
    ; - } - - var banned = this.props.room.getMembersWithMembership("ban"); - - return ( -
    -