Skip to content

Commit 5e4e3ec

Browse files
Merge pull request #257 from virdesai/crns-131/avatar-tests-and-hooks
CRNS-131: converting Avatar component from class to functional component and adding hooks and adding jsx lint configs
2 parents 4eec2fb + 2851690 commit 5e4e3ec

File tree

19 files changed

+162
-107
lines changed

19 files changed

+162
-107
lines changed

.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"react/prop-types": 0,
2626
"no-var": 2,
2727
"linebreak-style": [2, "unix"],
28-
"semi": [1, "always"]
28+
"semi": [1, "always"],
29+
"jsx-quotes": ["error", "prefer-single"]
2930
},
3031
"env": {
3132
"es6": true,

.prettierrc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
2-
"singleQuote": true,
3-
"trailingComma": "all",
42
"arrowParens": "always",
5-
"tabWidth": 2
3+
"jsxSingleQuote": true,
4+
"singleQuote": true,
5+
"tabWidth": 2,
6+
"trailingComma": "all"
67
}

src/components/Attachment/Card.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ class Card extends React.Component {
133133
>
134134
{Header && <Header {...this.props} />}
135135
{Cover && <Cover {...this.props} />}
136-
{uri && !Cover && <CardCover source={{ uri }} resizeMode="cover" />}
136+
{uri && !Cover && <CardCover source={{ uri }} resizeMode='cover' />}
137137
{Footer ? (
138138
<Footer {...this.props} />
139139
) : (

src/components/Attachment/FileAttachment.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const FileAttachment = ({
7979
mimeType={attachment.mime_type}
8080
/>
8181
<FileDetails>
82-
<FileTitle ellipsizeMode="tail" numberOfLines={2}>
82+
<FileTitle ellipsizeMode='tail' numberOfLines={2}>
8383
{attachment.title}
8484
</FileTitle>
8585
<FileSize>{attachment.file_size} KB</FileSize>
@@ -100,7 +100,7 @@ FileAttachment.propTypes = {
100100
attachment: PropTypes.object.isRequired,
101101
/**
102102
* Position of message. 'right' | 'left'
103-
* 'right' message belongs with current user while 'left' message belonds to other users.
103+
* 'right' message belongs with current user while 'left' message belongs to other users.
104104
* */
105105
alignment: PropTypes.string,
106106
/** Handler for actions. Actions in combination with attachments can be used to build [commands](https://getstream.io/chat/docs/#channel_commands). */

src/components/Attachment/Gallery.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class Gallery extends React.PureComponent {
115115
width: 100 + '%',
116116
height: 100 + '%',
117117
}}
118-
resizeMode="cover"
118+
resizeMode='cover'
119119
source={{ uri: images[0].url }}
120120
/>
121121
</Single>
@@ -183,7 +183,7 @@ class Gallery extends React.PureComponent {
183183
width: 100 + '%',
184184
height: 100 + '%',
185185
}}
186-
resizeMode="cover"
186+
resizeMode='cover'
187187
source={{ uri: images[3].url }}
188188
/>
189189
<View
@@ -218,7 +218,7 @@ class Gallery extends React.PureComponent {
218218
width: 100 + '%',
219219
height: 100 + '%',
220220
}}
221-
resizeMode="cover"
221+
resizeMode='cover'
222222
source={{ uri: image.url }}
223223
/>
224224
)}

src/components/Avatar/Avatar.js

Lines changed: 56 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useMemo, useState } from 'react';
22
import PropTypes from 'prop-types';
33

44
import styled from '@stream-io/styled-components';
@@ -8,100 +8,89 @@ const BASE_AVATAR_FALLBACK_TEXT_SIZE = 14;
88
const BASE_AVATAR_SIZE = 32;
99

1010
const AvatarContainer = styled.View`
11-
display: flex;
1211
align-items: center;
1312
${({ theme }) => theme.avatar.container.css}
1413
`;
1514

1615
const AvatarImage = styled.Image`
17-
border-radius: ${({ size }) => size / 2};
18-
width: ${({ size }) => size};
19-
height: ${({ size }) => size};
16+
border-radius: ${({ size }) => size / 2}px;
17+
height: ${({ size }) => size}px;
18+
width: ${({ size }) => size}px;
2019
${({ theme }) => theme.avatar.image.css}
2120
`;
2221

2322
const AvatarFallback = styled.View`
24-
border-radius: ${({ size }) => size / 2};
25-
width: ${({ size }) => size};
26-
height: ${({ size }) => size};
23+
align-items: center;
2724
background-color: ${({ theme }) => theme.colors.primary};
25+
border-radius: ${({ size }) => size / 2}px;
26+
height: ${({ size }) => size}px;
2827
justify-content: center;
29-
align-items: center;
28+
width: ${({ size }) => size}px;
3029
${({ theme }) => theme.avatar.fallback.css}
3130
`;
3231

3332
const AvatarText = styled.Text`
3433
color: ${({ theme }) => theme.colors.textLight};
35-
text-transform: uppercase;
36-
font-size: ${({ fontSize }) => fontSize};
34+
font-size: ${({ fontSize }) => fontSize}px;
3735
font-weight: bold;
36+
text-transform: uppercase;
3837
${({ theme }) => theme.avatar.text.css}
3938
`;
4039

40+
const getInitials = (fullName) =>
41+
fullName
42+
? fullName
43+
.split(' ')
44+
.slice(0, 2)
45+
.map((name) => name.charAt(0))
46+
: null;
47+
4148
/**
4249
* Avatar - A round avatar image with fallback to user's initials
4350
*
4451
* @example ../docs/Avatar.md
45-
* @extends PureComponent
4652
*/
47-
class Avatar extends React.PureComponent {
48-
static themePath = 'avatar';
49-
static propTypes = {
50-
/** image url */
51-
image: PropTypes.string,
52-
/** name of the picture, used for title tag fallback */
53-
name: PropTypes.string,
54-
/** size in pixels */
55-
size: PropTypes.number,
56-
/** Style overrides */
57-
style: PropTypes.object,
58-
};
59-
60-
static defaultProps = {
61-
size: 32,
62-
};
63-
64-
state = {
65-
imageError: false,
66-
};
53+
const Avatar = ({ image, name, size = BASE_AVATAR_SIZE }) => {
54+
const [imageError, setImageError] = useState(false);
6755

68-
setError = () => {
69-
this.setState({
70-
imageError: true,
71-
});
72-
};
56+
const fontSize = useMemo(
57+
() => BASE_AVATAR_FALLBACK_TEXT_SIZE * (size / BASE_AVATAR_SIZE),
58+
[size],
59+
);
60+
const initials = useMemo(() => getInitials(name), [name]);
7361

74-
getInitials = (name) =>
75-
name
76-
? name
77-
.split(' ')
78-
.slice(0, 2)
79-
.map((name) => name.charAt(0))
80-
: null;
62+
return (
63+
<AvatarContainer>
64+
{image && !imageError ? (
65+
<AvatarImage
66+
accessibilityLabel='initials'
67+
onError={() => setImageError(true)}
68+
resizeMethod='resize'
69+
size={size}
70+
source={{ uri: image }}
71+
testID='avatar-image'
72+
/>
73+
) : (
74+
<AvatarFallback size={size}>
75+
<AvatarText fontSize={fontSize} testID='avatar-text'>
76+
{initials}
77+
</AvatarText>
78+
</AvatarFallback>
79+
)}
80+
</AvatarContainer>
81+
);
82+
};
8183

82-
render() {
83-
const { size, name, image } = this.props;
84-
const initials = this.getInitials(name);
85-
const fontSize = BASE_AVATAR_FALLBACK_TEXT_SIZE * (size / BASE_AVATAR_SIZE);
84+
Avatar.propTypes = {
85+
/** image url */
86+
image: PropTypes.string,
87+
/** name of the picture, used for title tag fallback */
88+
name: PropTypes.string,
89+
/** size in pixels */
90+
size: PropTypes.number,
91+
};
8692

87-
return (
88-
<AvatarContainer>
89-
{image && !this.state.imageError ? (
90-
<AvatarImage
91-
size={size}
92-
source={{ uri: image }}
93-
accessibilityLabel="initials"
94-
resizeMethod="resize"
95-
onError={this.setError}
96-
/>
97-
) : (
98-
<AvatarFallback size={size}>
99-
<AvatarText fontSize={fontSize}>{initials}</AvatarText>
100-
</AvatarFallback>
101-
)}
102-
</AvatarContainer>
103-
);
104-
}
105-
}
93+
Avatar.themePath = 'avatar';
10694

95+
// TODO: remove HOC and use a theme context provider
10796
export default themed(Avatar);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
import { getNodeText, render, wait } from '@testing-library/react-native';
3+
4+
import Avatar from '../Avatar';
5+
6+
describe('Avatar', () => {
7+
it('should render an image with no name and default size', async () => {
8+
const { queryByTestId } = render(
9+
<Avatar image='https://pbs.twimg.com/profile_images/897621870069112832/dFGq6aiE_400x400.jpg' />,
10+
);
11+
12+
await wait(() => {
13+
expect(queryByTestId('avatar-image')).toBeTruthy();
14+
expect(queryByTestId('avatar-text')).toBeFalsy();
15+
});
16+
});
17+
18+
it('should render an image with name and default size', async () => {
19+
const { queryByTestId } = render(
20+
<Avatar
21+
image='https://pbs.twimg.com/profile_images/897621870069112832/dFGq6aiE_400x400.jpg'
22+
name='Test User'
23+
/>,
24+
);
25+
26+
await wait(() => {
27+
expect(queryByTestId('avatar-image')).toBeTruthy();
28+
expect(queryByTestId('avatar-text')).toBeFalsy();
29+
});
30+
});
31+
32+
it('should render an image with custom size', async () => {
33+
const { queryByTestId } = render(
34+
<Avatar
35+
image='https://pbs.twimg.com/profile_images/897621870069112832/dFGq6aiE_400x400.jpg'
36+
size={20}
37+
/>,
38+
);
39+
40+
await wait(() => {
41+
expect(queryByTestId('avatar-image')).toBeTruthy();
42+
expect(queryByTestId('avatar-text')).toBeFalsy();
43+
});
44+
});
45+
46+
it('should render an avatar with no image but a name and default size', async () => {
47+
const { getByTestId, queryByTestId } = render(<Avatar name='Test User' />);
48+
49+
await wait(() => {
50+
expect(queryByTestId('avatar-image')).toBeFalsy();
51+
expect(queryByTestId('avatar-text')).toBeTruthy();
52+
expect(getNodeText(getByTestId('avatar-text'))).toBe('TU');
53+
});
54+
});
55+
56+
it('should render an avatar with no image but a name and custom size', async () => {
57+
const { getByTestId, queryByTestId } = render(
58+
<Avatar name='Test User' size={20} />,
59+
);
60+
61+
await wait(() => {
62+
expect(queryByTestId('avatar-image')).toBeFalsy();
63+
expect(queryByTestId('avatar-text')).toBeTruthy();
64+
expect(getNodeText(getByTestId('avatar-text'))).toBe('TU');
65+
});
66+
});
67+
});

src/components/Channel/ChannelInner.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -655,12 +655,12 @@ class ChannelInner extends PureComponent {
655655

656656
renderLoading = () => {
657657
const Indicator = this.props.LoadingIndicator;
658-
return <Indicator listType="message" />;
658+
return <Indicator listType='message' />;
659659
};
660660

661661
renderLoadingError = () => {
662662
const Indicator = this.props.LoadingErrorIndicator;
663-
return <Indicator error={this.state.error} listType="message" />;
663+
return <Indicator error={this.state.error} listType='message' />;
664664
};
665665

666666
render() {

src/components/ChannelList/ChannelListMessenger.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class ChannelListMessenger extends PureComponent {
140140

141141
renderLoading = () => {
142142
const Indicator = this.props.LoadingIndicator;
143-
return <Indicator listType="channel" />;
143+
return <Indicator listType='channel' />;
144144
};
145145

146146
renderLoadingError = () => {
@@ -149,15 +149,15 @@ class ChannelListMessenger extends PureComponent {
149149
<Indicator
150150
error={this.props.error}
151151
retry={this.props.reloadList}
152-
listType="channel"
152+
listType='channel'
153153
loadNextPage={this.props.loadNextPage}
154154
/>
155155
);
156156
};
157157

158158
renderEmptyState = () => {
159159
const Indicator = this.props.EmptyStateIndicator;
160-
return <Indicator listType="channel" />;
160+
return <Indicator listType='channel' />;
161161
};
162162

163163
renderHeaderIndicator = () => {

0 commit comments

Comments
 (0)