Add support for avatar status badge (presence)

This commit is contained in:
Bettenbuk Zoltan 2019-12-06 16:02:51 +01:00 committed by Zoltan Bettenbuk
parent 9645391180
commit e683d70a18
12 changed files with 184 additions and 62 deletions

View File

@ -1,12 +1,23 @@
.avatar { .avatar {
align-items: center;
background-color: #AAA; background-color: #AAA;
display: flex;
border-radius: 50%; border-radius: 50%;
color: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 0.6);
font-weight: 100; font-weight: 100;
justify-content: center;
object-fit: cover; object-fit: cover;
&.avatar-small {
height: 28px !important;
width: 28px !important;
}
&.avatar-xsmall {
height: 16px !important;
width: 16px !important;
}
.jitsi-icon {
transform: translateY(50%);
}
} }
.avatar-foreign { .avatar-foreign {
@ -28,4 +39,28 @@
.defaultAvatar { .defaultAvatar {
opacity: 0.6 opacity: 0.6
}
.avatar-badge {
position: relative;
&-available::after {
@include avatarBadge;
background-color: $presence-available;
}
&-away::after {
@include avatarBadge;
background-color: $presence-away;
}
&-busy::after {
@include avatarBadge;
background-color: $presence-busy;
}
&-idle::after {
@include avatarBadge;
background-color: $presence-idle;
}
} }

View File

@ -192,4 +192,17 @@
*/ */
@mixin transparentBg($color, $alpha) { @mixin transparentBg($color, $alpha) {
background-color: rgba(red($color), green($color), blue($color), $alpha); background-color: rgba(red($color), green($color), blue($color), $alpha);
} }
/**
* Avatar status badge mixin
*/
@mixin avatarBadge {
border-radius: 50%;
content: '';
display: block;
height: 35%;
position: absolute;
bottom: 0;
width: 35%;
}

View File

@ -29,6 +29,10 @@ $defaultSideBarFontColor: #44A5FF;
$defaultSemiDarkColor: #ACACAC; $defaultSemiDarkColor: #ACACAC;
$defaultDarkColor: #2b3d5c; $defaultDarkColor: #2b3d5c;
$defaultWarningColor: rgb(215, 121, 118); $defaultWarningColor: rgb(215, 121, 118);
$presence-available: rgb(110, 176, 5);
$presence-away: rgb(250, 201, 20);
$presence-busy: rgb(233, 0, 27);
$presence-idle: rgb(172, 172, 172);
/** /**
* Toolbar * Toolbar

32
package-lock.json generated
View File

@ -22,38 +22,6 @@
"@atlaskit/type-helpers": "^2.0.0" "@atlaskit/type-helpers": "^2.0.0"
} }
}, },
"@atlaskit/avatar": {
"version": "14.1.7",
"resolved": "https://registry.npmjs.org/@atlaskit/avatar/-/avatar-14.1.7.tgz",
"integrity": "sha512-KGtV0lRr3g+JX3XLZQKDGxGhtbVFRvM/Ku5C+CEJw2uDl1KFY0dJxfr2a/E32bEgUuvmqSL7D3ROrTrlHJ2fMA==",
"requires": {
"@atlaskit/analytics-next": "^3.1.2",
"@atlaskit/theme": "^7.0.1",
"@atlaskit/tooltip": "^12.1.13",
"@babel/runtime": "^7.0.0"
},
"dependencies": {
"@atlaskit/analytics-next": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@atlaskit/analytics-next/-/analytics-next-3.1.2.tgz",
"integrity": "sha512-bkYDvl3Ojsnim+bsc9BALfvOjiL7xdb2rTp/4yqUP9pfidtf5HudbOJ849+dKcRCmk/rFbfB/nhDBRU6rv1Ueg==",
"requires": {
"@babel/runtime": "^7.0.0",
"babel-runtime": "^6.26.0",
"prop-types": "^15.5.10"
}
},
"@atlaskit/theme": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-7.0.1.tgz",
"integrity": "sha512-wxXDnkUablJketNCrQuNUuazufYEA7kv0Y6Yzv6uvqfuyNpWUQt4H1psz/MW8DbZmCdku9dEYbNVK3nFP5TDGg==",
"requires": {
"@babel/runtime": "^7.0.0",
"prop-types": "^15.5.10"
}
}
}
},
"@atlaskit/blanket": { "@atlaskit/blanket": {
"version": "8.0.3", "version": "8.0.3",
"resolved": "https://registry.npmjs.org/@atlaskit/blanket/-/blanket-8.0.3.tgz", "resolved": "https://registry.npmjs.org/@atlaskit/blanket/-/blanket-8.0.3.tgz",

View File

@ -15,7 +15,6 @@
"author": "", "author": "",
"readmeFilename": "README.md", "readmeFilename": "README.md",
"dependencies": { "dependencies": {
"@atlaskit/avatar": "14.1.7",
"@atlaskit/button": "10.1.1", "@atlaskit/button": "10.1.1",
"@atlaskit/checkbox": "5.0.10", "@atlaskit/checkbox": "5.0.10",
"@atlaskit/dropdown-menu": "6.1.25", "@atlaskit/dropdown-menu": "6.1.25",

View File

@ -54,6 +54,11 @@ export type Props = {
*/ */
size: number, size: number,
/**
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
*/
status?: ?string,
/** /**
* URL of the avatar, if any. * URL of the avatar, if any.
*/ */
@ -117,6 +122,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
colorBase, colorBase,
id, id,
size, size,
status,
url url
} = this.props; } = this.props;
const { avatarFailed } = this.state; const { avatarFailed } = this.state;
@ -128,6 +134,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
initials: undefined, initials: undefined,
onAvatarLoadError: undefined, onAvatarLoadError: undefined,
size, size,
status,
url: undefined url: undefined
}; };

View File

@ -12,6 +12,11 @@ import styles from './styles';
type Props = AbstractProps & { type Props = AbstractProps & {
/**
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
*/
status?: ?string,
/** /**
* External style passed to the componant. * External style passed to the componant.
*/ */
@ -46,18 +51,40 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
} }
return ( return (
<View <View>
style = { [ <View
styles.avatarContainer(size), style = { [
style styles.avatarContainer(size),
] }> style
{ avatar } ] }>
{ avatar }
</View>
{ this._renderAvatarStatus() }
</View> </View>
); );
} }
_isIcon: (?string | ?Object) => boolean _isIcon: (?string | ?Object) => boolean
/**
* Renders a badge representing the avatar status.
*
* @returns {React$Elementaa}
*/
_renderAvatarStatus() {
const { size, status } = this.props;
if (!status) {
return null;
}
return (
<View style = { styles.badgeContainer }>
<View style = { styles.badge(size, status) } />
</View>
);
}
/** /**
* Renders the default avatar. * Renders the default avatar.
* *

View File

@ -1,5 +1,7 @@
// @flow // @flow
import { StyleSheet } from 'react-native';
import { ColorPalette } from '../../../styles'; import { ColorPalette } from '../../../styles';
const DEFAULT_SIZE = 65; const DEFAULT_SIZE = 65;
@ -27,6 +29,38 @@ export default {
}; };
}, },
badge: (size: number = DEFAULT_SIZE, status: string) => {
let color;
switch (status) {
case 'available':
color = 'rgb(110, 176, 5)';
break;
case 'away':
color = 'rgb(250, 201, 20)';
break;
case 'busy':
color = 'rgb(233, 0, 27)';
break;
case 'idle':
color = 'rgb(172, 172, 172)';
break;
}
return {
backgroundColor: color,
borderRadius: size / 2,
bottom: 0,
height: size * 0.3,
position: 'absolute',
width: size * 0.3
};
},
badgeContainer: {
...StyleSheet.absoluteFillObject
},
initialsContainer: { initialsContainer: {
alignItems: 'center', alignItems: 'center',
alignSelf: 'stretch', alignSelf: 'stretch',

View File

@ -21,7 +21,12 @@ type Props = AbstractProps & {
/** /**
* ID of the component to be rendered. * ID of the component to be rendered.
*/ */
id?: string id?: string,
/**
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
*/
status?: ?string
}; };
/** /**
@ -40,7 +45,7 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
if (this._isIcon(url)) { if (this._isIcon(url)) {
return ( return (
<div <div
className = { this._getAvatarClassName() } className = { `${this._getAvatarClassName()} ${this._getBadgeClassName()}` }
id = { this.props.id } id = { this.props.id }
style = { this._getAvatarStyle(this.props.color) }> style = { this._getAvatarStyle(this.props.color) }>
<Icon <Icon
@ -52,19 +57,21 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
if (url) { if (url) {
return ( return (
<img <div className = { this._getBadgeClassName() }>
className = { this._getAvatarClassName() } <img
id = { this.props.id } className = { this._getAvatarClassName() }
onError = { this.props.onAvatarLoadError } id = { this.props.id }
src = { url } onError = { this.props.onAvatarLoadError }
style = { this._getAvatarStyle() } /> src = { url }
style = { this._getAvatarStyle() } />
</div>
); );
} }
if (initials) { if (initials) {
return ( return (
<div <div
className = { this._getAvatarClassName() } className = { `${this._getAvatarClassName()} ${this._getBadgeClassName()}` }
id = { this.props.id } id = { this.props.id }
style = { this._getAvatarStyle(this.props.color) }> style = { this._getAvatarStyle(this.props.color) }>
<svg <svg
@ -87,11 +94,13 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
// default avatar // default avatar
return ( return (
<img <div className = { this._getBadgeClassName() }>
className = { this._getAvatarClassName('defaultAvatar') } <img
id = { this.props.id } className = { this._getAvatarClassName('defaultAvatar') }
src = { this.props.defaultAvatar || 'images/avatar.png' } id = { this.props.id }
style = { this._getAvatarStyle() } /> src = { this.props.defaultAvatar || 'images/avatar.png' }
style = { this._getAvatarStyle() } />
</div>
); );
} }
@ -122,5 +131,20 @@ export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
return `avatar ${additional || ''} ${this.props.className || ''}`; return `avatar ${additional || ''} ${this.props.className || ''}`;
} }
/**
* Generates a class name to render a badge on the avatar, if necessary.
*
* @returns {string}
*/
_getBadgeClassName() {
const { status } = this.props;
if (status) {
return `avatar-badge avatar-badge-${status}`;
}
return '';
}
_isIcon: (?string | ?Object) => boolean _isIcon: (?string | ?Object) => boolean
} }

View File

@ -23,6 +23,11 @@ type Props = {
*/ */
avatarSize?: number, avatarSize?: number,
/**
* One of the expected status strings (e.g. 'available') to render a badge on the avatar, if necessary.
*/
avatarStatus?: ?string,
/** /**
* External style to be applied to the avatar (icon). * External style to be applied to the avatar (icon).
*/ */
@ -83,6 +88,7 @@ export default class AvatarListItem extends Component<Props> {
const { const {
avatarOnly, avatarOnly,
avatarSize = AVATAR_SIZE, avatarSize = AVATAR_SIZE,
avatarStatus,
avatarStyle avatarStyle
} = this.props; } = this.props;
const { avatar, colorBase, lines, title } = this.props.item; const { avatar, colorBase, lines, title } = this.props.item;
@ -96,6 +102,7 @@ export default class AvatarListItem extends Component<Props> {
colorBase = { colorBase } colorBase = { colorBase }
displayName = { title } displayName = { title }
size = { avatarSize } size = { avatarSize }
status = { avatarStatus }
style = { avatarStyle } style = { avatarStyle }
url = { avatar } /> url = { avatar } />
{ avatarOnly || <Container style = { styles.listItemDetails }> { avatarOnly || <Container style = { styles.listItemDetails }>

View File

@ -443,6 +443,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
<AvatarListItem <AvatarListItem
avatarOnly = { true } avatarOnly = { true }
avatarSize = { AVATAR_SIZE } avatarSize = { AVATAR_SIZE }
avatarStatus = { item.status }
avatarStyle = { styles.avatar } avatarStyle = { styles.avatar }
avatarTextStyle = { styles.avatarText } avatarTextStyle = { styles.avatarText }
item = { renderableItem } item = { renderableItem }
@ -497,6 +498,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
style = { styles.itemWrapper }> style = { styles.itemWrapper }>
<AvatarListItem <AvatarListItem
avatarSize = { AVATAR_SIZE } avatarSize = { AVATAR_SIZE }
avatarStatus = { item.status }
avatarStyle = { styles.avatar } avatarStyle = { styles.avatar }
avatarTextStyle = { styles.avatarText } avatarTextStyle = { styles.avatarText }
item = { renderableItem } item = { renderableItem }

View File

@ -1,11 +1,11 @@
// @flow // @flow
import Avatar from '@atlaskit/avatar';
import InlineMessage from '@atlaskit/inline-message'; import InlineMessage from '@atlaskit/inline-message';
import React from 'react'; import React from 'react';
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';
import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics'; import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics';
import { Avatar } from '../../../../base/avatar';
import { Dialog, hideDialog } from '../../../../base/dialog'; import { Dialog, hideDialog } from '../../../../base/dialog';
import { translate, translateToHTML } from '../../../../base/i18n'; import { translate, translateToHTML } from '../../../../base/i18n';
import { Icon, IconPhone } from '../../../../base/icons'; import { Icon, IconPhone } from '../../../../base/icons';
@ -289,13 +289,15 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
return { return {
content: user.name, content: user.name,
elemBefore: <Avatar elemBefore: <Avatar
size = 'small' className = { 'avatar-small' }
src = { user.avatar } />, status = { user.status }
url = { user.avatar } />,
item: user, item: user,
tag: { tag: {
elemBefore: <Avatar elemBefore: <Avatar
size = 'xsmall' className = { 'avatar-xsmall' }
src = { user.avatar } /> status = { user.status }
url = { user.avatar } />
}, },
value: user.id || user.user_id value: user.id || user.user_id
}; };