Skip to content

Commit 84dbb2c

Browse files
DanC5Dan Carbonell
andauthored
Avatar customization (#610)
* add Avatar as MessageList prop * add avatar prop to virtualized message list * avatar prop for different message types * avatar prop for channel list, preview, and header * miscellaneous avatar components * add tests for custom avatars * update Avatar React component type * remove from virtualized list Co-authored-by: Dan Carbonell <[email protected]>
1 parent e173df1 commit 84dbb2c

24 files changed

+296
-34
lines changed

src/components/ChannelHeader/ChannelHeader.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
// @ts-check
22
import React, { useContext } from 'react';
33
import PropTypes from 'prop-types';
4-
import { Avatar } from '../Avatar';
4+
import { Avatar as DefaultAvatar } from '../Avatar';
55
import { ChannelContext, TranslationContext, ChatContext } from '../../context';
66

77
/**
88
* ChannelHeader - Render some basic information about this channel
99
* @example ../../docs/ChannelHeader.md
1010
* @type {React.FC<import('types').ChannelHeaderProps>}
1111
*/
12-
const ChannelHeader = ({ title, live }) => {
12+
const ChannelHeader = ({ Avatar = DefaultAvatar, title, live }) => {
1313
/** @type {import("types").TranslationContextValue} */
1414
const { t } = useContext(TranslationContext);
1515
/** @type {import("types").ChannelContextValue} */
@@ -62,6 +62,12 @@ const ChannelHeader = ({ title, live }) => {
6262
};
6363

6464
ChannelHeader.propTypes = {
65+
/**
66+
* Custom UI component to display user avatar
67+
*
68+
* Defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.js)
69+
* */
70+
Avatar: /** @type {PropTypes.Validator<React.ElementType<import('types').AvatarProps>>} */ (PropTypes.elementType),
6571
/** Set title manually */
6672
title: PropTypes.string,
6773
/** Show a little indicator that the channel is live right now */

src/components/ChannelList/ChannelList.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ChatContext } from '../../context';
77
import { smartRender } from '../../utils';
88

99
import ChannelListTeam from './ChannelListTeam';
10+
import { Avatar as DefaultAvatar } from '../Avatar';
1011
import { LoadMorePaginator } from '../LoadMore';
1112
import { LoadingChannels } from '../Loading';
1213
import { EmptyStateIndicator as DefaultEmptyStateIndicator } from '../EmptyStateIndicator';
@@ -163,8 +164,14 @@ const ChannelList = (props) => {
163164
const renderChannel = (item) => {
164165
if (!item) return null;
165166

166-
const { Preview = ChannelPreviewLastMessage, watchers = {} } = props;
167+
const {
168+
Avatar = DefaultAvatar,
169+
Preview = ChannelPreviewLastMessage,
170+
watchers = {},
171+
} = props;
172+
167173
const previewProps = {
174+
Avatar,
168175
channel: item,
169176
Preview,
170177
activeChannel: channel,
@@ -174,6 +181,7 @@ const ChannelList = (props) => {
174181
// To force the update of preview component upon channel update.
175182
channelUpdateCount,
176183
};
184+
177185
return smartRender(ChannelPreview, { ...previewProps });
178186
};
179187

@@ -187,6 +195,7 @@ const ChannelList = (props) => {
187195
// renders the list.
188196
const renderList = () => {
189197
const {
198+
Avatar = DefaultAvatar,
190199
List = ChannelListTeam,
191200
Paginator = LoadMorePaginator,
192201
showSidebar,
@@ -199,6 +208,7 @@ const ChannelList = (props) => {
199208
loading={status.loadingChannels}
200209
error={status.error}
201210
showSidebar={showSidebar}
211+
Avatar={Avatar}
202212
LoadingIndicator={LoadingIndicator}
203213
LoadingErrorIndicator={LoadingErrorIndicator}
204214
>
@@ -229,6 +239,12 @@ const ChannelList = (props) => {
229239
};
230240

231241
ChannelList.propTypes = {
242+
/**
243+
* Custom UI component to display user avatar
244+
*
245+
* Defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.js)
246+
* */
247+
Avatar: /** @type {PropTypes.Validator<React.ElementType<import('types').AvatarProps>>} */ (PropTypes.elementType),
232248
/** Indicator for Empty State */
233249
EmptyStateIndicator: /** @type {PropTypes.Validator<React.ElementType<import('types').EmptyStateIndicatorProps>>} */ (PropTypes.elementType),
234250
/**

src/components/ChannelList/ChannelListTeam.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import React, { useContext } from 'react';
44
import PropTypes from 'prop-types';
55

6-
import { Avatar } from '../Avatar';
6+
import { Avatar as DefaultAvatar } from '../Avatar';
77
import { ChatDown } from '../ChatDown';
88
import { LoadingChannels } from '../Loading';
99
import { ChatContext } from '../../context';
@@ -20,6 +20,7 @@ const ChannelListTeam = ({
2020
loading,
2121
sidebarImage,
2222
showSidebar,
23+
Avatar = DefaultAvatar,
2324
LoadingErrorIndicator = ChatDown,
2425
LoadingIndicator = LoadingChannels,
2526
children,
@@ -78,6 +79,12 @@ ChannelListTeam.propTypes = {
7879
showSidebar: PropTypes.bool,
7980
/** Url for sidebar logo image. */
8081
sidebarImage: PropTypes.string,
82+
/**
83+
* Custom UI component to display user avatar
84+
*
85+
* Defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.js)
86+
* */
87+
Avatar: /** @type {PropTypes.Validator<React.ElementType<import('types').AvatarProps>>} */ (PropTypes.elementType),
8188
/**
8289
* Loading indicator UI Component. It will be displayed if `loading` prop is true.
8390
*

src/components/ChannelList/__tests__/ChannelList.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ import { v4 as uuidv4 } from 'uuid';
3636
import { ChatContext } from '../../../context';
3737
import { Chat } from '../../Chat';
3838
import ChannelList from '../ChannelList';
39+
import {
40+
ChannelPreviewCompact,
41+
ChannelPreviewLastMessage,
42+
ChannelPreviewMessenger,
43+
} from '../../ChannelPreview';
3944

4045
/**
4146
* We are gonna use following custom UI components for preview and list.
@@ -192,6 +197,52 @@ describe('ChannelList', () => {
192197
});
193198
});
194199

200+
it('ChannelPreview UI components should render `Avatar` when the custom prop is provided', async () => {
201+
useMockedApis(chatClientUthred, [queryChannelsApi([testChannel1])]);
202+
203+
const { getByTestId, rerender } = render(
204+
<Chat client={chatClientUthred}>
205+
<ChannelList
206+
Avatar={() => <div data-testid="custom-avatar-compact">Avatar</div>}
207+
Preview={ChannelPreviewCompact}
208+
List={ChannelListComponent}
209+
/>
210+
</Chat>,
211+
);
212+
213+
await waitFor(() => {
214+
expect(getByTestId('custom-avatar-compact')).toBeInTheDocument();
215+
});
216+
217+
rerender(
218+
<Chat client={chatClientUthred}>
219+
<ChannelList
220+
Avatar={() => <div data-testid="custom-avatar-last">Avatar</div>}
221+
Preview={ChannelPreviewLastMessage}
222+
List={ChannelListComponent}
223+
/>
224+
</Chat>,
225+
);
226+
227+
await waitFor(() => {
228+
expect(getByTestId('custom-avatar-last')).toBeInTheDocument();
229+
});
230+
231+
rerender(
232+
<Chat client={chatClientUthred}>
233+
<ChannelList
234+
Avatar={() => <div data-testid="custom-avatar-messenger">Avatar</div>}
235+
Preview={ChannelPreviewMessenger}
236+
List={ChannelListComponent}
237+
/>
238+
</Chat>,
239+
);
240+
241+
await waitFor(() => {
242+
expect(getByTestId('custom-avatar-messenger')).toBeInTheDocument();
243+
});
244+
});
245+
195246
it('when queryChannels api returns no channels, `EmptyStateIndicator` should be rendered', async () => {
196247
useMockedApis(chatClientUthred, [queryChannelsApi([])]);
197248

src/components/ChannelList/__tests__/ChannelListTeam.test.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable sonarjs/no-unused-collection */
22
import React from 'react';
3-
import { cleanup } from '@testing-library/react';
3+
import { cleanup, render, waitFor } from '@testing-library/react';
44
import '@testing-library/jest-dom';
55
import renderer from 'react-test-renderer';
66
import { getTestClientWithUser } from 'mock-builders';
@@ -11,13 +11,14 @@ import { ChatContext } from '../../../context';
1111
// Maybe better to find a better solution for it.
1212
console.warn = () => null;
1313

14-
const Component = ({ client, loading = false, error = false }) => {
14+
const Component = ({ Avatar, client, loading = false, error = false }) => {
1515
return (
1616
<ChatContext.Provider value={{ client }}>
1717
<ChannelListTeam
1818
client={client}
1919
loading={loading}
2020
error={error}
21+
Avatar={Avatar}
2122
LoadingIndicator={() => <div>Loading Indicator</div>}
2223
LoadingErrorIndicator={() => <div>Loading Error Indicator</div>}
2324
>
@@ -36,22 +37,38 @@ describe('ChannelListTeam', () => {
3637
beforeEach(async () => {
3738
chatClientVishal = await getTestClientWithUser({ id: 'vishal' });
3839
});
40+
3941
it('by default, should render sidebar, userbar and children', () => {
4042
const tree = renderer
4143
.create(<Component client={chatClientVishal} />)
4244
.toJSON();
4345
expect(tree).toMatchSnapshot();
4446
});
47+
4548
it('when `error` prop is true, `LoadingErrorIndicator` should be rendered', () => {
4649
const tree = renderer
4750
.create(<Component client={chatClientVishal} error />)
4851
.toJSON();
4952
expect(tree).toMatchSnapshot();
5053
});
54+
5155
it('when `loading` prop is true, `LoadingIndicator` should be rendered', () => {
5256
const tree = renderer
5357
.create(<Component client={chatClientVishal} loading />)
5458
.toJSON();
5559
expect(tree).toMatchSnapshot();
5660
});
61+
62+
it('when `Avatar` prop is provided, custom avatar component should render', async () => {
63+
const { getByTestId } = render(
64+
<Component
65+
client={chatClientVishal}
66+
Avatar={() => <div data-testid="custom-avatar">Avatar</div>}
67+
/>,
68+
);
69+
70+
await waitFor(() => {
71+
expect(getByTestId('custom-avatar')).toBeInTheDocument();
72+
});
73+
});
5774
});

src/components/ChannelPreview/ChannelPreview.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ ChannelPreview.propTypes = {
8383
.object.isRequired),
8484
/** Current selected channel object */
8585
activeChannel: /** @type {PropTypes.Validator<import('stream-chat').Channel | null | undefined>} */ (PropTypes.object),
86+
/**
87+
* Custom UI component to display user avatar
88+
*
89+
* Defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.js)
90+
* */
91+
Avatar: /** @type {PropTypes.Validator<React.ElementType<import('types').AvatarProps>>} */ (PropTypes.elementType),
8692
/**
8793
* Available built-in options (also accepts the same props as):
8894
*

src/components/ChannelPreview/ChannelPreviewCompact.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import React, { useRef } from 'react';
44
import PropTypes from 'prop-types';
55
import { Channel } from 'stream-chat';
66

7-
import { Avatar } from '../Avatar';
7+
import { Avatar as DefaultAvatar } from '../Avatar';
88

99
/**
1010
*
1111
* @example ../../docs/ChannelPreviewCompact.md
1212
* @type {import('types').ChannelPreviewCompact}
1313
*/
1414
const ChannelPreviewCompact = (props) => {
15+
const { Avatar = DefaultAvatar } = props;
1516
/**
1617
* @type {React.MutableRefObject<HTMLButtonElement | null>} Typescript syntax
1718
*/
@@ -55,6 +56,12 @@ ChannelPreviewCompact.propTypes = {
5556
channel: PropTypes.instanceOf(Channel).isRequired,
5657
/** Current selected channel object */
5758
activeChannel: PropTypes.instanceOf(Channel),
59+
/**
60+
* Custom UI component to display user avatar
61+
*
62+
* Defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.js)
63+
* */
64+
Avatar: /** @type {PropTypes.Validator<React.ElementType<import('types').AvatarProps>>} */ (PropTypes.elementType),
5865
/** Setter for selected channel */
5966
setActiveChannel: PropTypes.func.isRequired,
6067
/**

src/components/ChannelPreview/ChannelPreviewLastMessage.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import React, { useRef } from 'react';
44
import PropTypes from 'prop-types';
55
import { truncate } from '../../utils';
66

7-
import { Avatar } from '../Avatar';
7+
import { Avatar as DefaultAvatar } from '../Avatar';
88

99
/**
1010
* Used as preview component for channel item in [ChannelList](#channellist) component.
@@ -13,6 +13,7 @@ import { Avatar } from '../Avatar';
1313
* @type {import('types').ChannelPreviewLastMessage}
1414
*/
1515
const ChannelPreviewLastMessage = (props) => {
16+
const { Avatar = DefaultAvatar } = props;
1617
/** @type {React.MutableRefObject<HTMLButtonElement | null>} Typescript syntax */
1718
const channelPreviewButton = useRef(null);
1819
const onSelectChannel = () => {
@@ -60,6 +61,12 @@ ChannelPreviewLastMessage.propTypes = {
6061
channel: PropTypes.object.isRequired,
6162
/** Current selected channel object */
6263
activeChannel: PropTypes.object,
64+
/**
65+
* Custom UI component to display user avatar
66+
*
67+
* Defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.js)
68+
* */
69+
Avatar: /** @type {PropTypes.Validator<React.ElementType<import('types').AvatarProps>>} */ (PropTypes.elementType),
6370
/** Setter for selected channel */
6471
setActiveChannel: PropTypes.func.isRequired,
6572
/**

src/components/ChannelPreview/ChannelPreviewMessenger.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import React, { useRef } from 'react';
44
import PropTypes from 'prop-types';
55
import { truncate } from '../../utils';
66

7-
import { Avatar } from '../Avatar';
7+
import { Avatar as DefaultAvatar } from '../Avatar';
88

99
/**
1010
* Used as preview component for channel item in [ChannelList](#channellist) component.
@@ -14,6 +14,7 @@ import { Avatar } from '../Avatar';
1414
* @type {import('types').ChannelPreviewMessenger}
1515
*/
1616
const ChannelPreviewMessenger = (props) => {
17+
const { Avatar = DefaultAvatar } = props;
1718
/** @type {React.MutableRefObject<HTMLButtonElement | null>} Typescript syntax */
1819
const channelPreviewButton = useRef(null);
1920
const unreadClass =
@@ -61,6 +62,12 @@ ChannelPreviewMessenger.propTypes = {
6162
channel: PropTypes.object.isRequired,
6263
/** Current selected channel object */
6364
activeChannel: PropTypes.object,
65+
/**
66+
* Custom UI component to display user avatar
67+
*
68+
* Defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.js)
69+
* */
70+
Avatar: /** @type {PropTypes.Validator<React.ElementType<import('types').AvatarProps>>} */ (PropTypes.elementType),
6471
/** Setter for selected channel */
6572
setActiveChannel: PropTypes.func.isRequired,
6673
/**

src/components/EventComponent/EventComponent.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
import React, { useContext } from 'react';
33
import PropTypes from 'prop-types';
44

5-
import { Avatar } from '../Avatar';
5+
import { Avatar as DefaultAvatar } from '../Avatar';
66
import { TranslationContext } from '../../context';
77

88
/**
99
* EventComponent - Custom render component for system and channel event messages
1010
* @type {React.FC<import('types').EventComponentProps>}
1111
*/
12-
const EventComponent = ({ message }) => {
12+
const EventComponent = ({ Avatar = DefaultAvatar, message }) => {
1313
const { tDateTimeParser } = useContext(TranslationContext);
1414
const { type, text, event, created_at = '' } = message;
1515

@@ -62,6 +62,12 @@ EventComponent.propTypes = {
6262
/** Message object */
6363
// @ts-ignore
6464
message: PropTypes.object.isRequired,
65+
/**
66+
* Custom UI component to display user avatar
67+
*
68+
* Defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.js)
69+
* */
70+
Avatar: /** @type {PropTypes.Validator<React.ElementType<import('types').AvatarProps>>} */ (PropTypes.elementType),
6571
};
6672

6773
export default React.memo(EventComponent);

0 commit comments

Comments
 (0)