Move avatars into their own components so I can add functionality like custom default avatars and onerror sources without having to add it in 13 separate places. Add the aforementioned features.

This commit is contained in:
David Baker 2015-08-13 19:30:02 +01:00
parent b580fba7db
commit fec266f1c0
20 changed files with 314 additions and 23 deletions

View file

@ -0,0 +1,22 @@
/*
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.
*/
.mx_MemberAvatar {
z-index: 20;
border-radius: 20px;
background-color: #dbdbdb;
}

View file

@ -31,12 +31,6 @@ limitations under the License.
position: relative;
}
.mx_MemberTile_avatarImg {
z-index: 20;
border-radius: 20px;
background-color: #dbdbdb;
}
.mx_MemberTile_inviteEditing {
display: initial ! important;
}

View file

@ -0,0 +1,34 @@
/*
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 MemberAvatarController = require("../../../../src/controllers/atoms/MemberAvatar");
module.exports = React.createClass({
displayName: 'MemberAvatar',
mixins: [MemberAvatarController],
render: function() {
return (
<img className="mx_MemberAvatar" src={this.state.imageUrl}
onerror={this.onError}
width={this.props.width} height={this.props.height} />
);
}
});

View file

@ -0,0 +1,34 @@
/*
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 RoomAvatarController = require("../../../../src/controllers/atoms/RoomAvatar");
module.exports = React.createClass({
displayName: 'RoomAvatar',
mixins: [RoomAvatarController],
render: function() {
return (
<img className="mx_RoomAvatar" src={this.state.imageUrl} onerror={this.onError}
width={this.props.width} height={this.props.height}
/>
);
}
});

View file

@ -22,6 +22,7 @@ var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var EventAsTextTileController = require("../../../../src/controllers/molecules/EventAsTextTile");
var ComponentBroker = require('../../../../src/ComponentBroker');
var MessageTimestamp = ComponentBroker.get('atoms/MessageTimestamp');
var MemberAvatar = ComponentBroker.get('atoms/MemberAvatar');
var TextForEvent = require("../../../../src/TextForEvent");
module.exports = React.createClass({
@ -34,7 +35,7 @@ module.exports = React.createClass({
return (
<div className="mx_MessageTile mx_MessageTile_notice">
<div className="mx_MessageTile_avatar">
<img src={ this.props.mxEvent.sender ? MatrixClientPeg.get().getAvatarUrlForMember(this.props.mxEvent.sender, 40, 40, "crop") : null } width="40" height="40" alt=""/>
<MemberAvatar member={this.props.mxEvent.sender} />
</div>
{ timestamp }
<span className="mx_SenderProfile"></span>

View file

@ -24,6 +24,7 @@ var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var ComponentBroker = require('../../../../src/ComponentBroker');
var TextForEvent = require('../../../../src/TextForEvent');
var MessageTimestamp = ComponentBroker.get('atoms/MessageTimestamp');
var MemberAvatar = ComponentBroker.get('atoms/MemberAvatar');
module.exports = React.createClass({
displayName: 'MRoomMemberTile',
@ -41,7 +42,7 @@ module.exports = React.createClass({
return (
<div className="mx_MessageTile mx_MessageTile_notice">
<div className="mx_MessageTile_avatar">
<img src={ this.props.mxEvent.target ? MatrixClientPeg.get().getAvatarUrlForMember(this.props.mxEvent.target, 40, 40, "crop") : null } width="40" height="40" alt=""/>
<MemberAvatar member={this.props.mxEvent.sender} />
</div>
{ timestamp }
<span className="mx_SenderProfile"></span>

View file

@ -20,6 +20,8 @@ var React = require('react');
var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var MemberInfoController = require("../../../../src/controllers/molecules/MemberInfo");
var ComponentBroker = require('../../../../src/ComponentBroker');
var MemberAvatar = ComponentBroker.get('atoms/MemberAvatar');
module.exports = React.createClass({
displayName: 'MemberInfo',
@ -96,9 +98,7 @@ module.exports = React.createClass({
<img className="mx_MemberInfo_chevron" src="img/chevron-right.png" width="9" height="16" />
<div className="mx_MemberInfo_shim"></div>
<div className="mx_MemberInfo_avatar">
<img className="mx_MemberInfo_avatarImg"
src={ this.props.member ? MatrixClientPeg.get().getAvatarUrlForMember(this.props.member, 128, 128, "crop") : null }
width="128" height="128" alt=""/>
<MemberAvatar member={this.props.member} width={128} height={128} />
</div>
<div className="mx_MemberInfo_field">{this.props.member.userId}</div>
{opLabel}

View file

@ -24,6 +24,7 @@ var Modal = require("../../../../src/Modal");
var MemberTileController = require("../../../../src/controllers/molecules/MemberTile");
var MemberInfo = ComponentBroker.get('molecules/MemberInfo');
var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog");
var MemberAvatar = ComponentBroker.get('atoms/MemberAvatar');
// The Lato WOFF doesn't include sensible combining diacritics, so Chrome chokes on rendering them.
// Revert to Arial when this happens, which on OSX works at least.
@ -95,10 +96,8 @@ module.exports = React.createClass({
return (
<div className={mainClassName} onMouseEnter={ this.mouseEnter } onMouseLeave={ this.mouseLeave }>
<div className="mx_MemberTile_avatar">
<img className="mx_MemberTile_avatarImg"
src={ this.props.member ? MatrixClientPeg.get().getAvatarUrlForMember(this.props.member, 40, 40, "crop") : null }
width="40" height="40" alt=""/>
{ power }
<MemberAvatar member={this.props.member} />
{ power }
</div>
{ nameEl }
</div>

View file

@ -22,6 +22,9 @@ var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var MessageComposerController = require("../../../../src/controllers/molecules/MessageComposer");
var ContentMessages = require("../../../../src/ContentMessages");
var ComponentBroker = require('../../../../src/ComponentBroker');
var MemberAvatar = ComponentBroker.get('atoms/MemberAvatar');
module.exports = React.createClass({
displayName: 'MessageComposer',
mixins: [MessageComposerController],
@ -47,7 +50,7 @@ module.exports = React.createClass({
<div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row">
<div className="mx_MessageComposer_avatar">
<img src={ MatrixClientPeg.get().getAvatarUrlForMember(me, 40, 40, "crop") } width="40" height="40" alt=""/>
<MemberAvatar member={me} />
</div>
<div className="mx_MessageComposer_input">
<textarea ref="textarea" onKeyDown={this.onKeyDown} placeholder="Type a message" />

View file

@ -25,6 +25,7 @@ var ComponentBroker = require('../../../../src/ComponentBroker');
var MessageTimestamp = ComponentBroker.get('atoms/MessageTimestamp');
var SenderProfile = ComponentBroker.get('molecules/SenderProfile');
var MemberAvatar = ComponentBroker.get('atoms/MemberAvatar');
var UnknownMessageTile = ComponentBroker.get('molecules/UnknownMessageTile');
@ -64,7 +65,7 @@ module.exports = React.createClass({
if (!this.props.continuation) {
avatar = (
<div className="mx_MessageTile_avatar">
<img src={ this.props.mxEvent.sender ? MatrixClientPeg.get().getAvatarUrlForMember(this.props.mxEvent.sender, 40, 40, "crop") : null } width="40" height="40" alt=""/>
<MemberAvatar member={this.props.mxEvent.sender} />
</div>
);
sender = <SenderProfile mxEvent={this.props.mxEvent} />;

View file

@ -22,6 +22,7 @@ var ComponentBroker = require('../../../../src/ComponentBroker');
var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var RoomHeaderController = require("../../../../src/controllers/molecules/RoomHeader");
var EditableText = ComponentBroker.get("atoms/EditableText");
var RoomAvatar = ComponentBroker.get('atoms/RoomAvatar');
module.exports = React.createClass({
displayName: 'RoomHeader',
@ -93,11 +94,18 @@ module.exports = React.createClass({
);
}
var roomAvatar = null;
if (this.props.room) {
roomAvatar = (
<RoomAvatar room={this.props.room} />
);
}
header =
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_avatar">
<img src={ MatrixClientPeg.get().getAvatarUrlForRoom(this.props.room, 48, 48, "crop") } width="48" height="48" alt=""/>
{ roomAvatar }
</div>
<div className="mx_RoomHeader_info">
{ name }

View file

@ -23,6 +23,9 @@ var RoomTileController = require("../../../../src/controllers/molecules/RoomTile
var MatrixClientPeg = require("../../../../src/MatrixClientPeg");
var ComponentBroker = require('../../../../src/ComponentBroker');
var RoomAvatar = ComponentBroker.get('atoms/RoomAvatar');
module.exports = React.createClass({
displayName: 'RoomTile',
mixins: [RoomTileController],
@ -57,7 +60,10 @@ module.exports = React.createClass({
*/
return (
<div className={classes} onClick={this.onClick}>
<div className="mx_RoomTile_avatar"><img src={ MatrixClientPeg.get().getAvatarUrlForRoom(this.props.room, 40, 40, "crop") } width="40" height="40" alt=""/>{ badge }</div>
<div className="mx_RoomTile_avatar">
<RoomAvatar room={this.props.room} />
{ badge }
</div>
<div className="mx_RoomTile_name">{name}</div>
</div>
);

View file

@ -36,7 +36,7 @@ module.exports = React.createClass({
return (
<div className="mx_MessageTile mx_MessageTile_notice">
<div className="mx_MessageTile_avatar">
<img src={ this.props.mxEvent.sender ? MatrixClientPeg.get().getAvatarUrlForMember(this.props.mxEvent.sender, 40, 40, "crop") : null } width="40" height="40" alt=""/>
<MemberAvatar member={this.props.mxEvent.sender} />
</div>
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
<span className="mx_SenderProfile"></span>

View file

@ -36,7 +36,7 @@ module.exports = React.createClass({
return (
<div className="mx_MessageTile mx_MessageTile_notice">
<div className="mx_MessageTile_avatar">
<img src={ this.props.mxEvent.sender ? MatrixClientPeg.get().getAvatarUrlForMember(this.props.mxEvent.sender, 40, 40, "crop") : null } width="40" height="40" alt=""/>
<MemberAvatar member={this.props.mxEvent.sender} />
</div>
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
<span className="mx_SenderProfile"></span>

View file

@ -42,7 +42,7 @@ module.exports = React.createClass({
return (
<div className="mx_MessageTile mx_MessageTile_notice">
<div className="mx_MessageTile_avatar">
<img src={ this.props.mxEvent.sender ? MatrixClientPeg.get().getAvatarUrlForMember(this.props.mxEvent.sender, 40, 40, "crop") : null } width="40" height="40" alt=""/>
<MemberAvatar member={this.props.mxEvent.sender} />
</div>
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
<span className="mx_SenderProfile"></span>

View file

@ -23,6 +23,9 @@ var TextForEvent = require("../../../../src/TextForEvent");
var extend = require("../../../../src/extend");
var dis = require("../../../../src/dispatcher");
var ComponentBroker = require("../../../../src/ComponentBroker");
var MemberAvatar = ComponentBroker.get("atoms/MemberAvatar");
var NotifierView = {
notificationMessageForEvent: function(ev) {
@ -57,11 +60,13 @@ var NotifierView = {
if (ev.getContent().body) msg = ev.getContent().body;
}
var avatarUrlrl = MemberAvatar.avatarUrlForMember(ev.sender);
var notification = new global.Notification(
title,
{
"body": msg,
"icon": MatrixClientPeg.get().getAvatarUrlForMember(ev.sender)
"icon": avatarUrl
}
);

View file

@ -103,6 +103,8 @@ require('../skins/base/views/molecules/RoomDropTarget');
require('../skins/base/views/molecules/BottomLeftMenu');
require('../skins/base/views/molecules/DateSeparator');
require('../skins/base/views/atoms/voip/VideoFeed');
require('../skins/base/views/atoms/MemberAvatar');
require('../skins/base/views/atoms/RoomAvatar');
require('../skins/base/views/atoms/ImageView');
require('../skins/base/views/molecules/voip/VideoView');
require('../skins/base/views/molecules/voip/CallView');

38
src/DefaultAvatar.js Normal file
View file

@ -0,0 +1,38 @@
/*
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';
module.exports = {
defaultAvatarUrlForString: function(s) {
var total = 0;
for (var i = 0; i < s.length; ++i) {
total += s.charCodeAt(i);
}
switch (total % 3) {
case 0:
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9QjNbxSKP4eagAFnTseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAABSwCRWJw31gAAAAASUVORK5CYII=";
break;
case 1:
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9chOaxgCP4eagAFk9seHFErFYLBaLxWKxWCwWi8Vi8cX4CzAAtKMCks/JG8MAAAAASUVORK5CYII=";
break;
case 2:
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9YzNayQCP4eagADldseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAAyiACeHwPiu4AAAAASUVORK5CYII=";
break;
}
}
}

View file

@ -0,0 +1,76 @@
/*
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 MatrixClientPeg = require('../../MatrixClientPeg');
var DefaultAvatar = require('../../DefaultAvatar');
var React = require('react');
module.exports = {
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'
}
},
// takes member as an arg so it can be used if the
// avatars are required outsode of components
// (eg. in html5 desktop notifs)
avatarUrlForMember(member) {
var url = MatrixClientPeg.get().getAvatarUrlForMember(
member,
this.props.width, this.props.height, this.props.resizeMethod,
false
);
if (url === null) {
url = this.defaultAvatarUrl(member);
}
return url;
},
defaultAvatarUrl: function(member) {
return DefaultAvatar.defaultAvatarUrlForString(
member.userId
);
},
onError: function(ev) {
// don't tightloop if the browser can't load a data url
if (ev.target.src == this.defaultAvatarUrl()) {
return;
}
this.setState({
imageUrl: this.defaultAvatarUrl()
});
},
getInitialState: function() {
return {
imageUrl: this.avatarUrlForMember(this.props.member)
};
}
};

View file

@ -0,0 +1,67 @@
/*
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 MatrixClientPeg = require('../../MatrixClientPeg');
var DefaultAvatar = require('../../DefaultAvatar');
module.exports = {
getDefaultProps: function() {
return {
width: 40,
height: 40,
resizeMethod: 'crop'
}
},
// takes member as an arg so it can be used if the
// avatars are required outsode of components
// (eg. in html5 desktop notifs, although this is not)
avatarUrlForRoom(room) {
var url = MatrixClientPeg.get().getAvatarUrlForRoom(
room,
this.props.width, this.props.height, this.props.resizeMethod,
false
);
if (url === null) {
url = this.defaultAvatarUrl(room);
}
return url;
},
defaultAvatarUrl: function(room) {
return DefaultAvatar.defaultAvatarUrlForString(
this.props.room.roomId
);
},
onError: function(ev) {
// don't tightloop if the browser can't load a data url
if (ev.target.src == this.defaultAvatarUrl()) {
return;
}
this.setState({
imageUrl: this.defaultAvatarUrl()
});
},
getInitialState: function() {
return {
imageUrl: this.avatarUrlForRoom(this.props.room)
};
}
};