Merge pull request #95 from matrix-org/kegan/base-avatar
Factor out common Avatar codepull/21833/head
						commit
						6e42d00727
					
				|  | @ -32,6 +32,7 @@ module.exports.components['structures.RoomView'] = require('./components/structu | |||
| module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel'); | ||||
| module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar'); | ||||
| module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings'); | ||||
| module.exports.components['views.avatars.BaseAvatar'] = require('./components/views/avatars/BaseAvatar'); | ||||
| module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar'); | ||||
| module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar'); | ||||
| module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton'); | ||||
|  |  | |||
|  | @ -0,0 +1,140 @@ | |||
| /* | ||||
| Copyright 2015, 2016 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 AvatarLogic = require("../../../Avatar"); | ||||
| 
 | ||||
| module.exports = React.createClass({ | ||||
|     displayName: 'BaseAvatar', | ||||
| 
 | ||||
|     propTypes: { | ||||
|         name: React.PropTypes.string.isRequired, // The name (first initial used as default)
 | ||||
|         idName: React.PropTypes.string, // ID for generating hash colours
 | ||||
|         title: React.PropTypes.string, // onHover title text
 | ||||
|         url: React.PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
 | ||||
|         urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
 | ||||
|         width: React.PropTypes.number, | ||||
|         height: React.PropTypes.number, | ||||
|         resizeMethod: React.PropTypes.string, | ||||
|         defaultToInitialLetter: React.PropTypes.bool // true to add default url
 | ||||
|     }, | ||||
| 
 | ||||
|     getDefaultProps: function() { | ||||
|         return { | ||||
|             width: 40, | ||||
|             height: 40, | ||||
|             resizeMethod: 'crop', | ||||
|             defaultToInitialLetter: true | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState: function() { | ||||
|         return this._getState(this.props); | ||||
|     }, | ||||
| 
 | ||||
|     componentWillReceiveProps: function(nextProps) { | ||||
|         // work out if we need to call setState (if the image URLs array has changed)
 | ||||
|         var newState = this._getState(nextProps); | ||||
|         var newImageUrls = newState.imageUrls; | ||||
|         var oldImageUrls = this.state.imageUrls; | ||||
|         if (newImageUrls.length !== oldImageUrls.length) { | ||||
|             this.setState(newState); // detected a new entry
 | ||||
|         } | ||||
|         else { | ||||
|             // check each one to see if they are the same
 | ||||
|             for (var i = 0; i < newImageUrls.length; i++) { | ||||
|                 if (oldImageUrls[i] !== newImageUrls[i]) { | ||||
|                     this.setState(newState); // detected a diff
 | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     _getState: function(props) { | ||||
|         // work out the full set of urls to try to load. This is formed like so:
 | ||||
|         // imageUrls: [ props.url, props.urls, default image ]
 | ||||
| 
 | ||||
|         var urls = props.urls || []; | ||||
|         if (props.url) { | ||||
|             urls.unshift(props.url); // put in urls[0]
 | ||||
|         } | ||||
| 
 | ||||
|         var defaultImageUrl = null; | ||||
|         if (props.defaultToInitialLetter) { | ||||
|             defaultImageUrl = AvatarLogic.defaultAvatarUrlForString( | ||||
|                 props.idName || props.name | ||||
|             ); | ||||
|             urls.push(defaultImageUrl); // lowest priority
 | ||||
|         } | ||||
|         return { | ||||
|             imageUrls: urls, | ||||
|             defaultImageUrl: defaultImageUrl, | ||||
|             urlsIndex: 0 | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     onError: function(ev) { | ||||
|         var nextIndex = this.state.urlsIndex + 1; | ||||
|         if (nextIndex < this.state.imageUrls.length) { | ||||
|             // try the next one
 | ||||
|             this.setState({ | ||||
|                 urlsIndex: nextIndex | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     _getInitialLetter: function() { | ||||
|         var name = this.props.name; | ||||
|         var initial = name[0]; | ||||
|         if ((initial === '@' || initial === '#') && name[1]) { | ||||
|             initial = name[1]; | ||||
|         } | ||||
|         return initial.toUpperCase(); | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         var name = this.props.name; | ||||
| 
 | ||||
|         var imageUrl = this.state.imageUrls[this.state.urlsIndex]; | ||||
| 
 | ||||
|         if (imageUrl === this.state.defaultImageUrl) { | ||||
|             var initialLetter = this._getInitialLetter(); | ||||
|             return ( | ||||
|                 <span className="mx_BaseAvatar" {...this.props}> | ||||
|                     <span className="mx_BaseAvatar_initial" aria-hidden="true" | ||||
|                             style={{ fontSize: (this.props.width * 0.65) + "px", | ||||
|                                     width: this.props.width + "px", | ||||
|                                     lineHeight: this.props.height + "px" }}> | ||||
|                         { initialLetter } | ||||
|                     </span> | ||||
|                     <img className="mx_BaseAvatar_image" src={imageUrl} | ||||
|                         title={this.props.title} onError={this.onError} | ||||
|                         width={this.props.width} height={this.props.height} /> | ||||
|                 </span> | ||||
|             );             | ||||
|         } | ||||
|         return ( | ||||
|             <img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl} | ||||
|                 onError={this.onError} | ||||
|                 width={this.props.width} height={this.props.height} | ||||
|                 title={this.props.title} | ||||
|                 {...this.props} /> | ||||
|         ); | ||||
|     } | ||||
| }); | ||||
|  | @ -18,22 +18,16 @@ limitations under the License. | |||
| 
 | ||||
| var React = require('react'); | ||||
| var Avatar = require('../../../Avatar'); | ||||
| var MatrixClientPeg = require('../../../MatrixClientPeg'); | ||||
| var sdk = require("../../../index"); | ||||
| 
 | ||||
| module.exports = React.createClass({ | ||||
|     displayName: 'MemberAvatar', | ||||
| 
 | ||||
|     propTypes: { | ||||
|         member: React.PropTypes.object, | ||||
|         member: React.PropTypes.object.isRequired, | ||||
|         width: React.PropTypes.number, | ||||
|         height: React.PropTypes.number, | ||||
|         resizeMethod: React.PropTypes.string, | ||||
|         /** | ||||
|          * The custom display name to use for this member. This can serve as a | ||||
|          * drop in replacement for RoomMember objects, or as a clobber name on | ||||
|          * an existing RoomMember. Used for 3pid invites. | ||||
|          */ | ||||
|         customDisplayName: React.PropTypes.string | ||||
|         resizeMethod: React.PropTypes.string | ||||
|     }, | ||||
| 
 | ||||
|     getDefaultProps: function() { | ||||
|  | @ -45,77 +39,29 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     getInitialState: function() { | ||||
|         var defaultImageUrl = Avatar.defaultAvatarUrlForString( | ||||
|             this.props.customDisplayName || this.props.member.userId | ||||
|         ) | ||||
|         return { | ||||
|             imageUrl: this._getMemberImageUrl() || defaultImageUrl, | ||||
|             defaultImageUrl: defaultImageUrl | ||||
|         }; | ||||
|         return this._getState(this.props); | ||||
|     }, | ||||
| 
 | ||||
|     componentWillReceiveProps: function(nextProps) { | ||||
|         this.refreshUrl(); | ||||
|         this.setState(this._getState(nextProps)); | ||||
|     }, | ||||
| 
 | ||||
|     onError: function(ev) { | ||||
|         // don't tightloop if the browser can't load a data url
 | ||||
|         if (ev.target.src == this.state.defaultImageUrl) { | ||||
|             return; | ||||
|         } | ||||
|         this.setState({ | ||||
|             imageUrl: this.state.defaultImageUrl | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     _getMemberImageUrl: function() { | ||||
|         if (!this.props.member) { return null; } | ||||
| 
 | ||||
|         return Avatar.avatarUrlForMember(this.props.member, | ||||
|                                          this.props.width, | ||||
|                                          this.props.height, | ||||
|                                          this.props.resizeMethod); | ||||
|     }, | ||||
| 
 | ||||
|     _getInitialLetter: function() { | ||||
|         var name = this.props.customDisplayName || this.props.member.name; | ||||
|         var initial = name[0]; | ||||
|         if (initial === '@' && name[1]) { | ||||
|             initial = name[1]; | ||||
|         } | ||||
|         return initial.toUpperCase(); | ||||
|     }, | ||||
| 
 | ||||
|     refreshUrl: function() { | ||||
|         var newUrl = this._getMemberImageUrl(); | ||||
|         if (newUrl != this.currentUrl) { | ||||
|             this.currentUrl = newUrl; | ||||
|             this.setState({imageUrl: newUrl}); | ||||
|     _getState: function(props) { | ||||
|         return { | ||||
|             name: props.member.name, | ||||
|             title: props.member.userId, | ||||
|             imageUrl: Avatar.avatarUrlForMember(props.member, | ||||
|                                          props.width, | ||||
|                                          props.height, | ||||
|                                          props.resizeMethod) | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         var name = this.props.customDisplayName || this.props.member.name; | ||||
| 
 | ||||
|         if (this.state.imageUrl === this.state.defaultImageUrl) { | ||||
|             var initialLetter = this._getInitialLetter(); | ||||
|             return ( | ||||
|                 <span className="mx_MemberAvatar" {...this.props}> | ||||
|                     <span className="mx_MemberAvatar_initial" aria-hidden="true" | ||||
|                           style={{ fontSize: (this.props.width * 0.65) + "px", | ||||
|                                    width: this.props.width + "px", | ||||
|                                    lineHeight: this.props.height + "px" }}>{ initialLetter }</span> | ||||
|                     <img className="mx_MemberAvatar_image" src={this.state.imageUrl} title={name} | ||||
|                          onError={this.onError} width={this.props.width} height={this.props.height} /> | ||||
|                 </span> | ||||
|             );             | ||||
|         } | ||||
|         var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); | ||||
|         return ( | ||||
|             <img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl} | ||||
|                 onError={this.onError} | ||||
|                 width={this.props.width} height={this.props.height} | ||||
|                 title={name} | ||||
|                 {...this.props} /> | ||||
|             <BaseAvatar {...this.props} name={this.state.name} title={this.state.title} | ||||
|                 idName={this.props.member.userId} url={this.state.imageUrl} /> | ||||
|         ); | ||||
|     } | ||||
| }); | ||||
|  |  | |||
|  | @ -16,10 +16,18 @@ limitations under the License. | |||
| var React = require('react'); | ||||
| var MatrixClientPeg = require('../../../MatrixClientPeg'); | ||||
| var Avatar = require('../../../Avatar'); | ||||
| var sdk = require("../../../index"); | ||||
| 
 | ||||
| module.exports = React.createClass({ | ||||
|     displayName: 'RoomAvatar', | ||||
| 
 | ||||
|     propTypes: { | ||||
|         room: React.PropTypes.object.isRequired, | ||||
|         width: React.PropTypes.number, | ||||
|         height: React.PropTypes.number, | ||||
|         resizeMethod: React.PropTypes.string | ||||
|     }, | ||||
| 
 | ||||
|     getDefaultProps: function() { | ||||
|         return { | ||||
|             width: 36, | ||||
|  | @ -29,84 +37,54 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     getInitialState: function() { | ||||
|         this._update(); | ||||
|         return { | ||||
|             imageUrl: this._nextUrl() | ||||
|             urls: this.getImageUrls(this.props) | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     componentWillReceiveProps: function(nextProps) { | ||||
|         this.refreshImageUrl(); | ||||
|     componentWillReceiveProps: function(newProps) { | ||||
|         this.setState({ | ||||
|             urls: this.getImageUrls(newProps) | ||||
|         }) | ||||
|     }, | ||||
| 
 | ||||
|     refreshImageUrl: function(nextProps) { | ||||
|         // If the list has changed, we start from scratch and re-check, but
 | ||||
|         // don't do so unless the list has changed or we'd re-try fetching
 | ||||
|         // images each time we re-rendered
 | ||||
|         var newList = this.getUrlList(); | ||||
|         var differs = false; | ||||
|         for (var i = 0; i < newList.length && i < this.urlList.length; ++i) { | ||||
|             if (this.urlList[i] != newList[i]) differs = true; | ||||
|         } | ||||
|         if (this.urlList.length != newList.length) differs = true; | ||||
| 
 | ||||
|         if (differs) { | ||||
|             this._update(); | ||||
|             this.setState({ | ||||
|                 imageUrl: this._nextUrl() | ||||
|             }); | ||||
|         } | ||||
|     getImageUrls: function(props) { | ||||
|         return [ | ||||
|             this.getRoomAvatarUrl(props), // highest priority
 | ||||
|             this.getOneToOneAvatar(props), | ||||
|             this.getFallbackAvatar(props) // lowest priority
 | ||||
|         ].filter(function(url) { | ||||
|             return url != null; | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     _update: function() { | ||||
|         this.urlList = this.getUrlList(); | ||||
|         this.urlListIndex = -1; | ||||
|     }, | ||||
| 
 | ||||
|     _nextUrl: function() { | ||||
|         do { | ||||
|             ++this.urlListIndex; | ||||
|         } while ( | ||||
|             this.urlList[this.urlListIndex] === null && | ||||
|             this.urlListIndex < this.urlList.length | ||||
|         ); | ||||
|         if (this.urlListIndex < this.urlList.length) { | ||||
|             return this.urlList[this.urlListIndex]; | ||||
|         } else { | ||||
|             return null; | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     // provided to the view class for convenience
 | ||||
|     roomAvatarUrl: function() { | ||||
|         var url = this.props.room.getAvatarUrl( | ||||
|     getRoomAvatarUrl: function(props) { | ||||
|         return props.room.getAvatarUrl( | ||||
|             MatrixClientPeg.get().getHomeserverUrl(), | ||||
|             this.props.width, this.props.height, this.props.resizeMethod, | ||||
|             props.width, props.height, props.resizeMethod, | ||||
|             false | ||||
|         ); | ||||
|         return url; | ||||
|     }, | ||||
| 
 | ||||
|     // provided to the view class for convenience
 | ||||
|     getOneToOneAvatar: function() { | ||||
|         var userIds = Object.keys(this.props.room.currentState.members); | ||||
|     getOneToOneAvatar: function(props) { | ||||
|         var userIds = Object.keys(props.room.currentState.members); | ||||
| 
 | ||||
|         if (userIds.length == 2) { | ||||
|             var theOtherGuy = null; | ||||
|             if (this.props.room.currentState.members[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) { | ||||
|                 theOtherGuy = this.props.room.currentState.members[userIds[1]]; | ||||
|             if (props.room.currentState.members[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) { | ||||
|                 theOtherGuy = props.room.currentState.members[userIds[1]]; | ||||
|             } else { | ||||
|                 theOtherGuy = this.props.room.currentState.members[userIds[0]]; | ||||
|                 theOtherGuy = props.room.currentState.members[userIds[0]]; | ||||
|             } | ||||
|             return theOtherGuy.getAvatarUrl( | ||||
|                 MatrixClientPeg.get().getHomeserverUrl(), | ||||
|                 this.props.width, this.props.height, this.props.resizeMethod, | ||||
|                 props.width, props.height, props.resizeMethod, | ||||
|                 false | ||||
|             ); | ||||
|         } else if (userIds.length == 1) { | ||||
|             return this.props.room.currentState.members[userIds[0]].getAvatarUrl( | ||||
|             return props.room.currentState.members[userIds[0]].getAvatarUrl( | ||||
|                 MatrixClientPeg.get().getHomeserverUrl(), | ||||
|                 this.props.width, this.props.height, this.props.resizeMethod, | ||||
|                 props.width, props.height, props.resizeMethod, | ||||
|                     false | ||||
|             ); | ||||
|         } else { | ||||
|  | @ -114,58 +92,15 @@ module.exports = React.createClass({ | |||
|         } | ||||
|     }, | ||||
| 
 | ||||
| 
 | ||||
|     onError: function(ev) { | ||||
|         this.setState({ | ||||
|             imageUrl: this._nextUrl() | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     ////////////
 | ||||
| 
 | ||||
| 
 | ||||
|     getUrlList: function() { | ||||
|         return [ | ||||
|             this.roomAvatarUrl(), | ||||
|             this.getOneToOneAvatar(), | ||||
|             this.getFallbackAvatar() | ||||
|         ]; | ||||
|     }, | ||||
| 
 | ||||
|     getFallbackAvatar: function() { | ||||
|         return Avatar.defaultAvatarUrlForString(this.props.room.roomId); | ||||
|     getFallbackAvatar: function(props) { | ||||
|         return Avatar.defaultAvatarUrlForString(props.room.roomId); | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         var style = { | ||||
|             width: this.props.width, | ||||
|             height: this.props.height, | ||||
|         }; | ||||
| 
 | ||||
|         // XXX: recalculates fallback avatar constantly
 | ||||
|         if (this.state.imageUrl === this.getFallbackAvatar()) { | ||||
|             var initial; | ||||
|             if (this.props.room.name[0]) | ||||
|                 initial = this.props.room.name[0].toUpperCase(); | ||||
|             if ((initial === '@' || initial === '#') && this.props.room.name[1]) | ||||
|                 initial = this.props.room.name[1].toUpperCase(); | ||||
|           | ||||
|             return ( | ||||
|                 <span> | ||||
|                     <span className="mx_RoomAvatar_initial" aria-hidden="true" | ||||
|                           style={{ fontSize: (this.props.width * 0.65) + "px", | ||||
|                                    width: this.props.width + "px", | ||||
|                                    lineHeight: this.props.height + "px" }}>{ initial }</span> | ||||
|                     <img className="mx_RoomAvatar" src={this.state.imageUrl} | ||||
|                             onError={this.onError} style={style} /> | ||||
|                 </span> | ||||
|             ); | ||||
|         } | ||||
|         else { | ||||
|             return <img className="mx_RoomAvatar" src={this.state.imageUrl} | ||||
|                         onError={this.onError} style={style} /> | ||||
|         } | ||||
|         var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); | ||||
|         return ( | ||||
|             <BaseAvatar {...this.props} name={this.props.room.name} | ||||
|                 idName={this.props.room.roomId} urls={this.state.urls} /> | ||||
|         ); | ||||
|     } | ||||
| }); | ||||
|  |  | |||
|  | @ -151,14 +151,26 @@ module.exports = React.createClass({ | |||
|         } | ||||
| 
 | ||||
|         var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); | ||||
|         var BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); | ||||
| 
 | ||||
|         var av; | ||||
|         if (member) { | ||||
|             av = ( | ||||
|                 <MemberAvatar member={this.props.member} width={36} height={36} /> | ||||
|             ); | ||||
|         } | ||||
|         else { | ||||
|             av = ( | ||||
|                 <BaseAvatar name={name} width={36} height={36} /> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <div className={mainClassName} title={ this.getPowerLabel() } | ||||
|                     onClick={ this.onClick } onMouseEnter={ this.mouseEnter } | ||||
|                     onMouseLeave={ this.mouseLeave }> | ||||
|                 <div className="mx_MemberTile_avatar"> | ||||
|                     <MemberAvatar member={this.props.member} width={36} height={36} | ||||
|                         customDisplayName={this.props.customDisplayName} /> | ||||
|                     {av} | ||||
|                 </div> | ||||
|                 { nameEl } | ||||
|             </div> | ||||
|  |  | |||
|  | @ -129,7 +129,7 @@ module.exports = React.createClass({ | |||
|             var roomAvatar = null; | ||||
|             if (this.props.room) { | ||||
|                 roomAvatar = ( | ||||
|                     <RoomAvatar room={this.props.room} width="48" height="48" /> | ||||
|                     <RoomAvatar room={this.props.room} width={48} height={48} /> | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|  |  | |||
|  | @ -123,7 +123,7 @@ module.exports = React.createClass({ | |||
|         return connectDragSource(connectDropTarget( | ||||
|             <div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> | ||||
|                 <div className="mx_RoomTile_avatar"> | ||||
|                     <RoomAvatar room={this.props.room} width="24" height="24" /> | ||||
|                     <RoomAvatar room={this.props.room} width={24} height={24} /> | ||||
|                     { badge } | ||||
|                 </div> | ||||
|                 { label } | ||||
|  |  | |||
|  | @ -111,7 +111,7 @@ module.exports = React.createClass({ | |||
|         // Having just set an avatar we just display that since it will take a little
 | ||||
|         // time to propagate through to the RoomAvatar.
 | ||||
|         if (this.props.room && !this.avatarSet) { | ||||
|             avatarImg = <RoomAvatar room={this.props.room} width='240' height='240' resizeMethod='crop' />; | ||||
|             avatarImg = <RoomAvatar room={this.props.room} width={240} height={240} resizeMethod='crop' />; | ||||
|         } else { | ||||
|             var style = { | ||||
|                 maxWidth: 240, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Kegsay
						Kegsay