diff --git a/package.json b/package.json index fb7558ad5..bc6e3496c 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ "filesize": "^3.1.2", "flux": "~2.0.3", "linkifyjs": "^2.0.0-beta.4", - "matrix-js-sdk": "^0.3.0", - "matrix-react-sdk": "^0.0.2", + "matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop", + "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", @@ -37,6 +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", "sanitize-html": "^1.0.0" }, "devDependencies": { diff --git a/src/ContextualMenu.js b/src/ContextualMenu.js index 3327aa948..a7b1849e1 100644 --- a/src/ContextualMenu.js +++ b/src/ContextualMenu.js @@ -43,7 +43,7 @@ module.exports = { var self = this; var closeMenu = function() { - React.unmountComponentAtNode(self.getOrCreateContainer()); + ReactDOM.unmountComponentAtNode(self.getOrCreateContainer()); if (props && props.onFinished) props.onFinished.apply(null, arguments); }; diff --git a/src/Velociraptor.js b/src/Velociraptor.js new file mode 100644 index 000000000..df6b3c95d --- /dev/null +++ b/src/Velociraptor.js @@ -0,0 +1,113 @@ +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).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; + } else { + // 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); + } + }); + }, + + collectNode: function(k, node) { + if ( + this.nodes[k] === undefined && + node.props.startStyle && + Object.keys(node.props.startStyle).length + ) { + var domNode = ReactDom.findDOMNode(node); + var startStyles = node.props.startStyle; + var transitionOpts = node.props.enterTransitionOpts; + if (!Array.isArray(startStyles)) { + startStyles = [ startStyles ]; + transitionOpts = [ transitionOpts ]; + } + // 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; + }, + + 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/VelocityBounce.js b/src/VelocityBounce.js new file mode 100644 index 000000000..c85aa254f --- /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/components/login/Login.js b/src/components/login/Login.js index 414528f1b..80d0314fc 100644 --- a/src/components/login/Login.js +++ b/src/components/login/Login.js @@ -165,11 +165,11 @@ module.exports = React.createClass({displayName: 'Login',
- vector + vector

Sign in

- {this.componentForStep(this._getCurrentFlowStep())} + { this.componentForStep(this._getCurrentFlowStep()) }
{ loader } - {this.state.errorText} + { this.state.errorText }
Create a new account diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index e603198a7..7f46ff988 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -53,6 +53,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); MatrixClientPeg.get().on("sync", this.onSyncStateChange); @@ -71,6 +72,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); MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange); @@ -108,6 +110,9 @@ module.exports = { // the conf this._updateConfCallNotification(); break; + case 'user_activity': + this.sendReadReceipt(); + break; } }, @@ -187,6 +192,12 @@ module.exports = { } }, + onRoomReceipt: function(receiptEvent, room) { + if (room.roomId == this.props.roomId) { + this.forceUpdate(); + } + }, + onRoomMemberTyping: function(ev, member) { this.forceUpdate(); }, @@ -247,6 +258,8 @@ module.exports = { messageWrapperScroll.scrollTop = messageWrapperScroll.scrollHeight; + this.sendReadReceipt(); + this.fillSpace(); } @@ -529,7 +542,7 @@ module.exports = { } ret.unshift( -
  • +
  • ); if (dateSeparator) { ret.unshift(dateSeparator); @@ -624,5 +637,58 @@ 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() { + if (!this.state.room) return; + var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); + var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); + + var lastReadEventIndex = this._getLastDisplayedEventIndexIgnoringOwn(); + if (lastReadEventIndex === null) return; + + if (lastReadEventIndex > currentReadUpToEventIndex) { + MatrixClientPeg.get().sendReadReceipt(this.state.room.timeline[lastReadEventIndex]); + } + }, + + _getLastDisplayedEventIndexIgnoringOwn: function() { + if (this.eventNodes === undefined) return null; + + var messageWrapper = this.refs.messagePanel; + 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]; + + if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { + continue; + } + + var node = this.eventNodes[ev.getId()]; + if (!node) continue; + + var boundingRect = node.getBoundingClientRect(); + + if (boundingRect.bottom < wrapperRect.bottom) { + return i; + } + } + return null; } }; diff --git a/src/skins/vector/css/atoms/MemberAvatar.css b/src/skins/vector/css/atoms/MemberAvatar.css index 34ef13936..e76c2ad10 100644 --- a/src/skins/vector/css/atoms/MemberAvatar.css +++ b/src/skins/vector/css/atoms/MemberAvatar.css @@ -15,7 +15,8 @@ limitations under the License. */ .mx_MemberAvatar { - position: relative; + /* commenting this out as it breaks on FF seemingly */ +/* position: relative; */ } .mx_MemberAvatar_initial { @@ -23,8 +24,9 @@ limitations under the License. color: #fff; text-align: center; speak: none; + pointer-events: none; } .mx_MemberAvatar_image { - border-radius: 20px; + border-radius: 20px; } diff --git a/src/skins/vector/css/atoms/RoomAvatar.css b/src/skins/vector/css/atoms/RoomAvatar.css index 01425190e..70a61eeb0 100644 --- a/src/skins/vector/css/atoms/RoomAvatar.css +++ b/src/skins/vector/css/atoms/RoomAvatar.css @@ -23,4 +23,5 @@ limitations under the License. text-align: center; font-weight: normal ! important; speak: none; + pointer-events: none; } \ 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 d99bd4e1b..f092ba28e 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,18 @@ limitations under the License. .mx_EventTile_msgOption { float: right; + text-align: right; + z-index: 1; + position: relative; + width: 90px; + margin-right: 10px; + margin-top: -6px; } .mx_MessageTimestamp { + display: block; visibility: hidden; + text-align: right; } .mx_EventTile_last .mx_MessageTimestamp { @@ -107,9 +114,8 @@ limitations under the License. .mx_EventTile_editButton { position: absolute; - right: 1px; - top: 15px; - visibility: hidden; + display: inline-block; + visibility: hidden; } .mx_EventTile:hover .mx_EventTile_editButton { @@ -123,3 +129,21 @@ limitations under the License. .mx_EventTile.menu .mx_MessageTimestamp { visibility: visible; } + +.mx_EventTile_readAvatars { + position: relative; + display: inline-block; + width: 14px; + height: 14px; +} + +.mx_EventTile_readAvatars .mx_MemberAvatar { + position: absolute; + display: inline-block; +} + +.mx_EventTile_readAvatarRemainder { + color: #acacac; + font-size: 12px; + position: absolute; +} diff --git a/src/skins/vector/css/molecules/MessageComposer.css b/src/skins/vector/css/molecules/MessageComposer.css index fbbeef645..3fb38c317 100644 --- a/src/skins/vector/css/molecules/MessageComposer.css +++ b/src/skins/vector/css/molecules/MessageComposer.css @@ -72,7 +72,8 @@ limitations under the License. } .mx_MessageComposer_upload, -.mx_MessageComposer_call { +.mx_MessageComposer_voicecall, +.mx_MessageComposer_videocall { display: table-cell; vertical-align: middle; padding-left: 10px; @@ -80,7 +81,12 @@ limitations under the License. cursor: pointer; } -.mx_MessageComposer_call { +.mx_MessageComposer_videocall { + padding-right: 10px; + padding-top: 4px; +} + +.mx_MessageComposer_voicecall { padding-right: 10px; padding-top: 4px; } diff --git a/src/skins/vector/css/pages/MatrixChat.css b/src/skins/vector/css/pages/MatrixChat.css index b95f6a415..2190e4960 100644 --- a/src/skins/vector/css/pages/MatrixChat.css +++ b/src/skins/vector/css/pages/MatrixChat.css @@ -14,6 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_MatrixChat_splash { + position: relative; + height: 100%; +} + +.mx_MatrixChat_splashButtons { + text-align: center; + width: 100%; + position: absolute; + bottom: 30px; +} + .mx_MatrixChat_wrapper { display: -webkit-box; display: -moz-box; diff --git a/src/skins/vector/img/voice.png b/src/skins/vector/img/voice.png new file mode 100644 index 000000000..5ba765b0f Binary files /dev/null and b/src/skins/vector/img/voice.png differ diff --git a/src/skins/vector/skindex.js b/src/skins/vector/skindex.js index b1ac84990..45d60f730 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'); @@ -31,9 +34,6 @@ 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'); skin['atoms.voip.VideoFeed'] = require('./views/atoms/voip/VideoFeed'); skin['molecules.BottomLeftMenu'] = require('./views/molecules/BottomLeftMenu'); skin['molecules.BottomLeftMenuTile'] = require('./views/molecules/BottomLeftMenuTile'); @@ -43,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'); diff --git a/src/skins/vector/views/atoms/MemberAvatar.js b/src/skins/vector/views/atoms/MemberAvatar.js index c8606cd72..c719d70c5 100644 --- a/src/skins/vector/views/atoms/MemberAvatar.js +++ b/src/skins/vector/views/atoms/MemberAvatar.js @@ -49,12 +49,12 @@ module.exports = React.createClass({ initial = this.props.member.name[1].toUpperCase(); return ( - + - ); @@ -62,7 +62,10 @@ module.exports = React.createClass({ return ( + width={this.props.width} height={this.props.height} + title={this.props.member.name} + {...this.props} + /> ); } }); diff --git a/src/skins/vector/views/molecules/EventTile.js b/src/skins/vector/views/molecules/EventTile.js index c5cb81951..1e4c9a63d 100644 --- a/src/skins/vector/views/molecules/EventTile.js +++ b/src/skins/vector/views/molecules/EventTile.js @@ -17,15 +17,28 @@ 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') +var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg') var EventTileController = require('matrix-react-sdk/lib/controllers/molecules/EventTile') var ContextualMenu = require('../../../../ContextualMenu'); var TextForEvent = require('matrix-react-sdk/lib/TextForEvent'); +var Velociraptor = require('../../../../Velociraptor'); +require('../../../../VelocityBounce'); + +var bounce = false; +try { + if (global.localStorage) { + bounce = global.localStorage.getItem('avatar_bounce') == 'true'; + } +} catch (e) { +} + var eventTileTypes = { 'm.room.message': 'molecules.MessageTile', 'm.room.member' : 'molecules.EventAsTextTile', @@ -36,6 +49,8 @@ var eventTileTypes = { 'm.room.topic' : 'molecules.EventAsTextTile', }; +var MAX_READ_AVATARS = 5; + module.exports = React.createClass({ displayName: 'EventTile', mixins: [EventTileController], @@ -52,7 +67,11 @@ module.exports = React.createClass({ }, getInitialState: function() { - return {menu: false}; + return {menu: false, allReadAvatars: false}; + }, + + componentDidUpdate: function() { + this.readAvatarRect = ReactDom.findDOMNode(this.readAvatarNode).getBoundingClientRect(); }, onEditClicked: function(e) { @@ -72,6 +91,123 @@ module.exports = React.createClass({ this.setState({menu: true}); }, + toggleAllReadAvatars: function() { + this.setState({ + allReadAvatars: !this.state.allReadAvatars + }); + }, + + getReadAvatars: function() { + var avatars = []; + + var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + + 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" && r.userId != myUserId; + }).sort(function(r1, r2) { + return r2.data.ts - r1.data.ts; + }); + + var MemberAvatar = sdk.getComponent('atoms.MemberAvatar'); + + var left = 0; + + var reorderTransitionOpts = { + duration: 100, + easing: 'easeOut' + }; + + 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 startStyles = []; + var enterTransitionOpts = []; + if (oldAvatarDomNode && this.readAvatarRect) { + var oldRect = oldAvatarDomNode.getBoundingClientRect(); + var topOffset = oldRect.top - this.readAvatarRect.top; + + if (oldAvatarDomNode.style.left !== '0px') { + var leftOffset = oldAvatarDomNode.style.left; + // start at the old height and in the old h pos + startStyles.push({ top: topOffset, left: leftOffset }); + enterTransitionOpts.push(reorderTransitionOpts); + } + + // then shift to the rightmost column, + // and then it will drop down to its resting position + startStyles.push({ top: topOffset, left: '0px' }); + enterTransitionOpts.push({ + duration: bounce ? Math.min(Math.log(Math.abs(topOffset)) * 200, 3000) : 300, + easing: bounce ? 'easeOutBounce' : 'easeOutCubic', + }); + } + + var style = { + left: left+'px', + top: '0px', + visibility: ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) ? 'visible' : 'hidden' + }; + + //console.log("i = " + i + ", MAX_READ_AVATARS = " + MAX_READ_AVATARS + ", allReadAvatars = " + this.state.allReadAvatars + " visibility = " + style.visibility); + + // add to the start so the most recent is on the end (ie. ends up rightmost) + avatars.unshift( + + ); + // 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) { // XXX: where does this -1 come from? is it to make the max'th avatar animate properly? + left -= 15; + } + } + var editButton; + if (!this.state.allReadAvatars) { + var remainder = receipts.length - MAX_READ_AVATARS; + var remText; + if (i >= MAX_READ_AVATARS - 1) left -= 15; + if (remainder > 0) { + remText = { remainder }+ + ; + left -= 15; + } + editButton = ( + + ); + } + + return + { editButton } + { remText } + + { avatars } + + ; + }, + + collectReadAvatarNode: function(node) { + this.readAvatarNode = node; + }, + render: function() { var MessageTimestamp = sdk.getComponent('atoms.MessageTimestamp'); var SenderProfile = sdk.getComponent('molecules.SenderProfile'); @@ -100,18 +236,14 @@ module.exports = React.createClass({ menu: this.state.menu, }); var timestamp = - var editButton = ( - - ); var aux = null; if (msgtype === 'm.image') aux = "sent an image"; 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) { @@ -127,11 +259,13 @@ module.exports = React.createClass({ } return (
    +
    + { timestamp } + { readAvatars } +
    { avatar } { sender }
    - { timestamp } - { editButton }
    diff --git a/src/skins/vector/views/molecules/MessageComposer.js b/src/skins/vector/views/molecules/MessageComposer.js index 2f0e7ac57..501e2464a 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(); }, @@ -49,6 +53,14 @@ module.exports = React.createClass({ }); }, + onVoiceCallClick: function(ev) { + dis.dispatch({ + action: 'place_call', + type: 'voice', + room_id: this.props.room.roomId + }); + }, + render: function() { var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); var uploadInputStyle = {display: 'none'}; @@ -60,14 +72,17 @@ module.exports = React.createClass({
    -
    +