Define component directories. Merge MemberAvatar and RoomAvatar to new-style components.

Spoken to @ara4n about names/conventions. Settled on the following layout:

  src/components
      |_____________views
      |               |____ tiles
      |               |       |___ MTextTile.js
      |               |       |___ MNoticeTile.js
      |               |       |___ ...
      |               |
      |               |____ avatars
      |               |       |____ RoomAvatar.js
      |               |       |____ MemberAvatar.js
      |               |       |____ ...
      |               |
      |               |____ ...
      |
      |_____________structures
                      |____ RoomView.js
                      |____ UserSettings.js
                      |____ CreateRoom.js
                      |____ ...

Views are the "pure UI" components which can be reused. Structures are the
wire components which give important contextual information to the views e.g.
a view may be MemberList, but it's where it is in the structure that defines
that it is a *Room* MemberList.
This commit is contained in:
Kegan Dougal 2015-11-26 12:02:31 +00:00
parent f5e2a54603
commit b69fff5b01
3 changed files with 111 additions and 40 deletions

View file

@ -0,0 +1,142 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var React = require('react');
var Avatar = require('../../../../Avatar');
var MatrixClientPeg = require('../../MatrixClientPeg');
module.exports = React.createClass({
displayName: 'MemberAvatar',
propTypes: {
member: React.PropTypes.object.isRequired,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string,
},
getDefaultProps: function() {
return {
width: 40,
height: 40,
resizeMethod: 'crop'
}
},
componentWillReceiveProps: function(nextProps) {
this.refreshUrl();
},
defaultAvatarUrl: function(member, width, height, resizeMethod) {
if (this.skinnedDefaultAvatarUrl) {
return this.skinnedDefaultAvatarUrl(member, width, height, resizeMethod);
}
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9QjNbxSKP4eagAFnTseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAABSwCRWJw31gAAAAASUVORK5CYII=";
},
onError: function(ev) {
// don't tightloop if the browser can't load a data url
if (ev.target.src == this.defaultAvatarUrl(this.props.member)) {
return;
}
this.setState({
imageUrl: this.defaultAvatarUrl(this.props.member)
});
},
_computeUrl: function() {
var url = this.props.member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
this.props.width,
this.props.height,
this.props.resizeMethod,
false
);
if (!url) {
url = this.defaultAvatarUrl(
this.props.member,
this.props.width,
this.props.height,
this.props.resizeMethod
);
}
return url;
},
refreshUrl: function() {
var newUrl = this._computeUrl();
if (newUrl != this.currentUrl) {
this.currentUrl = newUrl;
this.setState({imageUrl: newUrl});
}
},
getInitialState: function() {
return {
imageUrl: this._computeUrl()
};
},
///////////////
avatarUrlForMember: function(member) {
return Avatar.avatarUrlForMember(
member,
this.props.member,
this.props.width,
this.props.height,
this.props.resizeMethod
);
},
skinnedDefaultAvatarUrl: function(member, width, height, resizeMethod) {
return Avatar.defaultAvatarUrlForString(member.userId);
},
render: function() {
// XXX: recalculates default avatar url constantly
if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) {
var initial;
if (this.props.member.name[0])
initial = this.props.member.name[0].toUpperCase();
if (initial === '@' && this.props.member.name[1])
initial = this.props.member.name[1].toUpperCase();
return (
<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} title={this.props.member.name}
onError={this.onError} width={this.props.width} height={this.props.height} />
</span>
);
}
return (
<img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl}
onError={this.onError}
width={this.props.width} height={this.props.height}
title={this.props.member.name}
{...this.props}
/>
);
}
});

View file

@ -0,0 +1,175 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var MatrixClientPeg = require('../../MatrixClientPeg');
module.exports = React.createClass({
displayName: 'RoomAvatar',
getDefaultProps: function() {
return {
width: 36,
height: 36,
resizeMethod: 'crop'
}
},
getInitialState: function() {
this._update();
return {
imageUrl: this._nextUrl()
};
},
componentWillReceiveProps: function(nextProps) {
this.refreshImageUrl();
},
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()
});
}
},
_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(
MatrixClientPeg.get().getHomeserverUrl(),
this.props.width, this.props.height, this.props.resizeMethod,
false
);
return url;
},
// provided to the view class for convenience
getOneToOneAvatar: function() {
var userIds = Object.keys(this.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]];
} else {
theOtherGuy = this.props.room.currentState.members[userIds[0]];
}
return theOtherGuy.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
this.props.width, this.props.height, this.props.resizeMethod,
false
);
} else if (userIds.length == 1) {
return this.props.room.currentState.members[userIds[0]].getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
this.props.width, this.props.height, this.props.resizeMethod,
false
);
} else {
return null;
}
},
onError: function(ev) {
this.setState({
imageUrl: this._nextUrl()
});
},
////////////
getUrlList: function() {
return [
this.roomAvatarUrl(),
this.getOneToOneAvatar(),
this.getFallbackAvatar()
];
},
getFallbackAvatar: function() {
var images = [ '76cfa6', '50e2c2', 'f4c371' ];
var total = 0;
for (var i = 0; i < this.props.room.roomId.length; ++i) {
total += this.props.room.roomId.charCodeAt(i);
}
return 'img/' + images[total % images.length] + '.png';
},
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.75) + "px",
width: this.props.width + "px",
lineHeight: this.props.height*1.2 + "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} />
}
}
});