mirror of
				https://github.com/vector-im/element-web.git
				synced 2025-10-25 22:31:51 +02:00 
			
		
		
		
	Merge branch 'develop' into kegan/reg-refactor
This commit is contained in:
		
						commit
						5f57cd9559
					
				| @ -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": { | ||||
|  | ||||
| @ -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); | ||||
|         }; | ||||
|  | ||||
							
								
								
									
										113
									
								
								src/Velociraptor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/Velociraptor.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 ( | ||||
|             <span> | ||||
|                 {childList} | ||||
|             </span> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
							
								
								
									
										15
									
								
								src/VelocityBounce.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/VelocityBounce.js
									
									
									
									
									
										Normal file
									
								
							| @ -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); | ||||
| } | ||||
| @ -169,7 +169,7 @@ module.exports = React.createClass({displayName: 'Login', | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <h2>Sign in</h2> | ||||
|                         {this.componentForStep(this._getCurrentFlowStep())} | ||||
|                         { this.componentForStep(this._getCurrentFlowStep()) } | ||||
|                         <ServerConfig ref="serverConfig" | ||||
|                             withToggleButton={true} | ||||
|                             defaultHsUrl={this.props.homeserverUrl} | ||||
| @ -179,7 +179,7 @@ module.exports = React.createClass({displayName: 'Login', | ||||
|                             delayTimeMs={1000}/> | ||||
|                         <div className="mx_Login_error"> | ||||
|                                 { loader } | ||||
|                                 {this.state.errorText} | ||||
|                                 { this.state.errorText } | ||||
|                         </div> | ||||
|                         <a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#"> | ||||
|                             Create a new account | ||||
|  | ||||
| @ -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( | ||||
|                 <li key={mxEv.getId()}><EventTile mxEvent={mxEv} continuation={continuation} last={last}/></li> | ||||
|                 <li key={mxEv.getId()} ref={this._collectEventNode.bind(this, mxEv.getId())}><EventTile mxEvent={mxEv} continuation={continuation} last={last}/></li> | ||||
|             ); | ||||
|             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; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| @ -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,6 +24,7 @@ limitations under the License. | ||||
|     color: #fff; | ||||
|     text-align: center; | ||||
|     speak: none; | ||||
|     pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| .mx_MemberAvatar_image { | ||||
|  | ||||
| @ -23,4 +23,5 @@ limitations under the License. | ||||
|     text-align: center; | ||||
|     font-weight: normal ! important; | ||||
|     speak: none; | ||||
|     pointer-events: none; | ||||
| } | ||||
| @ -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,8 +114,7 @@ limitations under the License. | ||||
| 
 | ||||
| .mx_EventTile_editButton { | ||||
|     position: absolute; | ||||
|     right: 1px; | ||||
|     top: 15px; | ||||
|     display: inline-block; | ||||
|     visibility: hidden;  | ||||
| } | ||||
| 
 | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								src/skins/vector/img/voice.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/skins/vector/img/voice.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 856 B | 
| @ -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'); | ||||
|  | ||||
| @ -49,12 +49,12 @@ module.exports = React.createClass({ | ||||
|                 initial = this.props.member.name[1].toUpperCase(); | ||||
|           | ||||
|             return ( | ||||
|                 <span className="mx_MemberAvatar"> | ||||
|                 <span className="mx_MemberAvatar" {...this.props}> | ||||
|                     <span className="mx_MemberAvatar_initial" aria-hidden="true" | ||||
|                           style={{ fontSize: (this.props.width * 0.75) + "px", | ||||
|                                    width: this.props.width + "px", | ||||
|                                    lineHeight: this.props.height*1.2 + "px" }}>{ initial }</span> | ||||
|                     <img className="mx_MemberAvatar_image" src={this.state.imageUrl} | ||||
|                     <img className="mx_MemberAvatar_image" src={this.state.imageUrl} title={this.props.member.name} | ||||
|                          onError={this.onError} width={this.props.width} height={this.props.height} /> | ||||
|                 </span> | ||||
|             );             | ||||
| @ -62,7 +62,10 @@ module.exports = React.createClass({ | ||||
|         return ( | ||||
|             <img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl} | ||||
|                 onError={this.onError} | ||||
|                 width={this.props.width} height={this.props.height} /> | ||||
|                 width={this.props.width} height={this.props.height} | ||||
|                 title={this.props.member.name} | ||||
|                 {...this.props} | ||||
|             /> | ||||
|         ); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| @ -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( | ||||
|                 <MemberAvatar key={member.userId} member={member} | ||||
|                     width={14} height={14} resizeMethod="crop" | ||||
|                     style={style} | ||||
|                     startStyle={startStyles} | ||||
|                     enterTransitionOpts={enterTransitionOpts} | ||||
|                     id={'mx_readAvatar'+member.userId} | ||||
|                     onClick={this.toggleAllReadAvatars} | ||||
|                 /> | ||||
|             ); | ||||
|             // 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 = <span className="mx_EventTile_readAvatarRemainder" | ||||
|                     onClick={this.toggleAllReadAvatars} | ||||
|                     style={{ left: left }}>{ remainder }+ | ||||
|                 </span>; | ||||
|                 left -= 15; | ||||
|             } | ||||
|             editButton = ( | ||||
|                 <input style={{ left: left }} | ||||
|                     type="image" src="img/edit.png" alt="Edit" width="14" height="14" | ||||
|                     className="mx_EventTile_editButton" onClick={this.onEditClicked} /> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return <span className="mx_EventTile_readAvatars" ref={this.collectReadAvatarNode}> | ||||
|             { editButton } | ||||
|             { remText } | ||||
|             <Velociraptor transition={ reorderTransitionOpts }> | ||||
|                 { avatars } | ||||
|             </Velociraptor> | ||||
|         </span>; | ||||
|     }, | ||||
| 
 | ||||
|     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 = <MessageTimestamp ts={this.props.mxEvent.getTs()} /> | ||||
|         var editButton = ( | ||||
|             <input | ||||
|                 type="image" src="img/edit.png" alt="Edit" width="14" height="14" | ||||
|                 className="mx_EventTile_editButton" onClick={this.onEditClicked} | ||||
|             /> | ||||
|         ); | ||||
| 
 | ||||
|         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 ( | ||||
|             <div className={classes}> | ||||
|                 <div className="mx_EventTile_msgOption"> | ||||
|                     { timestamp } | ||||
|                     { readAvatars } | ||||
|                 </div> | ||||
|                 { avatar } | ||||
|                 { sender } | ||||
|                 <div className="mx_EventTile_line"> | ||||
|                     { timestamp } | ||||
|                     { editButton } | ||||
|                     <EventTileType mxEvent={this.props.mxEvent} searchTerm={this.props.searchTerm} /> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
| @ -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({ | ||||
|                         <div className="mx_MessageComposer_avatar"> | ||||
|                             <MemberAvatar member={me} width={24} height={24} /> | ||||
|                         </div> | ||||
|                         <div className="mx_MessageComposer_input"> | ||||
|                         <div className="mx_MessageComposer_input" onClick={ this.onInputClick }> | ||||
|                             <textarea ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." /> | ||||
|                         </div> | ||||
|                         <div className="mx_MessageComposer_upload" onClick={this.onUploadClick}> | ||||
|                             <img src="img/upload.png" width="17" height="22"/> | ||||
|                             <input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} /> | ||||
|                         </div> | ||||
|                         <div className="mx_MessageComposer_call" onClick={this.onCallClick}> | ||||
|                         <div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick}> | ||||
|                             <img src="img/voice.png" width="16" height="26"/> | ||||
|                         </div> | ||||
|                         <div className="mx_MessageComposer_videocall" onClick={this.onCallClick}> | ||||
|                             <img src="img/call.png" width="28" height="20"/> | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
| @ -65,6 +65,14 @@ module.exports = React.createClass({ | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     onLogoutClick: function(event) { | ||||
|         dis.dispatch({ | ||||
|             action: 'logout' | ||||
|         }); | ||||
|         event.stopPropagation(); | ||||
|         event.preventDefault(); | ||||
|     }, | ||||
| 
 | ||||
|     handleResize: function(e) { | ||||
|         var hideLhsThreshold = 1000; | ||||
|         var showLhsThreshold = 1000; | ||||
| @ -164,7 +172,10 @@ module.exports = React.createClass({ | ||||
|         } else if (this.state.logged_in) { | ||||
|             var Spinner = sdk.getComponent('atoms.Spinner'); | ||||
|             return ( | ||||
|                 <div className="mx_MatrixChat_splash"> | ||||
|                     <Spinner /> | ||||
|                     <a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>Logout</a> | ||||
|                 </div> | ||||
|             ); | ||||
|         } else if (this.state.screen == 'register') { | ||||
|             /* | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user