diff --git a/src/component-index.js b/src/component-index.js index f75dfa5d67..b391016fcd 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -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'); diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js new file mode 100644 index 0000000000..2a7dcc1c01 --- /dev/null +++ b/src/components/views/avatars/BaseAvatar.js @@ -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 ( + + + + + ); + } + return ( + + ); + } +}); diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index f209006b1c..5e2dbbb23a 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -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 ( - - - - - ); - } + var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); return ( - + ); } }); diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index a83bc799a2..72ca5f7f21 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -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 ( - - - - - ); - } - else { - return - } + var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + return ( + + ); } }); diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 58d9d59276..a5dbea65f6 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -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 = ( + + ); + } + else { + av = ( + + ); + } return (
- + {av}
{ nameEl }
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 8d419f7be6..5340798875 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -264,7 +264,7 @@ module.exports = React.createClass({ else { roomAvatar = (
- +
); } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 16d798cb87..29b35c040c 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -123,7 +123,7 @@ module.exports = React.createClass({ return connectDragSource(connectDropTarget(
- + { badge }
{ label }