From dc8d82edcbc2ba607320dd7abb84de0aad623a33 Mon Sep 17 00:00:00 2001 From: martincupela Date: Fri, 8 Nov 2024 11:58:36 +0100 Subject: [PATCH] refactor: keep using Avatar component as a default avatar everywhere --- .../contexts/channel-list-context.mdx | 2 +- .../components/contexts/component-context.mdx | 8 ----- .../core-components/channel-list.mdx | 8 ++--- .../components/core-components/channel.mdx | 8 ----- .../components/utility-components/avatar.mdx | 36 ++++++++----------- src/components/Channel/Channel.tsx | 3 -- .../ChannelHeader/ChannelHeader.tsx | 6 ++-- .../__tests__/ChannelHeader.test.js | 17 +++++---- src/components/ChannelList/ChannelList.tsx | 6 ++-- .../ChannelPreview/ChannelPreview.tsx | 2 +- .../ChannelPreviewMessenger.tsx | 4 +-- .../__tests__/ChannelPreview.test.js | 16 +++++---- src/context/ComponentContext.tsx | 3 -- 13 files changed, 49 insertions(+), 70 deletions(-) diff --git a/docusaurus/docs/React/components/contexts/channel-list-context.mdx b/docusaurus/docs/React/components/contexts/channel-list-context.mdx index 23995e1fd..13e3dc491 100644 --- a/docusaurus/docs/React/components/contexts/channel-list-context.mdx +++ b/docusaurus/docs/React/components/contexts/channel-list-context.mdx @@ -5,7 +5,7 @@ title: ChannelListContext The context value is provided by `ChannelListContextProvider` which wraps the contents rendered by [`ChannelList`](../core-components/channel-list.mdx). It exposes API that the default and custom components rendered by `ChannelList` can take advantage of. The components that can consume the context are customizable via `ChannelListProps`: -- `ChannelAvatar` - component used to display channel image +- `Avatar` - component used to display channel image - `ChannelSearch` - renders channel search input and results - `EmptyStateIndicator` - rendered when the channels query returns and empty array - `LoadingErrorIndicator` - rendered when the channels query fails diff --git a/docusaurus/docs/React/components/contexts/component-context.mdx b/docusaurus/docs/React/components/contexts/component-context.mdx index 9c3f77ecb..7239e3a57 100644 --- a/docusaurus/docs/React/components/contexts/component-context.mdx +++ b/docusaurus/docs/React/components/contexts/component-context.mdx @@ -131,14 +131,6 @@ The [default `BaseImage` component](../../utility-components/base-image) tries t | --------- | ----------------------------------------------------------------- | | component | | -### ChannelAvatar - -Custom UI component to display avatar for a channel in ChannelHeader. - -| Type | Default | -| --------- | ------------------------------------------------------------------------ | -| component | | - ### CooldownTimer Custom UI component to display the slow mode cooldown timer. diff --git a/docusaurus/docs/React/components/core-components/channel-list.mdx b/docusaurus/docs/React/components/core-components/channel-list.mdx index bf8a94432..5821365ad 100644 --- a/docusaurus/docs/React/components/core-components/channel-list.mdx +++ b/docusaurus/docs/React/components/core-components/channel-list.mdx @@ -187,11 +187,11 @@ list from incrementing the list. ### Avatar -Custom UI component to display the channel avatar. The default avatar component for `ChannelList` is `ChannelAvatar`. +Custom UI component to display the channel avatar. The default avatar component for `ChannelList` is `Avatar`. -| Type | Default | -| --------- | ----------------------------------------------------------------- | -| component | | +| Type | Default | +| --------- | ---------------------------------------------------------- | +| component | | ### channelRenderFilterFn diff --git a/docusaurus/docs/React/components/core-components/channel.mdx b/docusaurus/docs/React/components/core-components/channel.mdx index b53b0d2e2..a8984f272 100644 --- a/docusaurus/docs/React/components/core-components/channel.mdx +++ b/docusaurus/docs/React/components/core-components/channel.mdx @@ -190,14 +190,6 @@ Custom UI component to display a user's avatar. | --------- | ---------------------------------------------------------- | | component | | -### ChannelAvatar - -Custom UI component to display avatar for a channel in ChannelHeader. - -| Type | Default | -| --------- | ------------------------------------------------------------------------ | -| component | | - ### channelQueryOptions Optional configuration parameters used for the initial channel query. Applied only if the value of `channel.initialized` is false. If the channel instance has already been initialized (channel has been queried), then the channel query will be skipped and channelQueryOptions will not be applied. diff --git a/docusaurus/docs/React/components/utility-components/avatar.mdx b/docusaurus/docs/React/components/utility-components/avatar.mdx index 3c832aa01..d9d67ba61 100644 --- a/docusaurus/docs/React/components/utility-components/avatar.mdx +++ b/docusaurus/docs/React/components/utility-components/avatar.mdx @@ -3,28 +3,17 @@ id: avatar title: Avatar --- -The SDK supports variety of avatar component types. Different approach is taken to display a channel avatar and an avatar of a message author. +Semantically we can speak about two types of avatars in the SDK. One type is the avatar that represents the channel and the other representing another user. The SDK exports the follwing avatar components: -The channel avatar accounts for the fact that the channel may contain more than two members and thus become a group channel. Therefore, it renders a `GroupAvatar` component in case of more than two channel members and `Avatar` in case of two or less channel members. -On the other hand, messages use the avatars for a specific single user and thus using the `Avatar` component exclusively. +- `Avatar` - displays single image or name initials in case image is not available +- `GroupAvatar` - displays images or name initials as a fallback in a 2x2 grid +- `ChannelAvatar` - renders `GroupAvatar` in case a channel has more than two members and `Avatar` otherwise -The `Avatar` component displays an image, with fallback to the first letter of the optional name prop. The `GroupAvatar` displays up to four `Avatar` components in a 2x2 grid. +By default, all the SDK components use `Avatar` to display channel resp. user avatar. However, it makes sense to override the default in `ChannelList` resp. `ChannelPreview` and `ChannelHeader` as those avatars may represent a group of users . -## Customizing Avatar component +## Customizing avatar component -The SDK's default `Avatar` component is used by the following components: - -- `ChannelSearch` results for users -- `Message` -- `QuotesMessage` -- `MesageStatus` -- `QuotedMessagePreview` in message composer -- Suggestion items for user mentions -- `Poll` -- Message `Reactions` -- `ThreadList` - -Passing your custom avatar component to `Channel` prop `Avatar` overrides the avatar for all the above components. +Passing your custom avatar component to `Channel` prop `Avatar` overrides the avatar for all the `Channel` component's children. Here's an example of using the `Avatar` component within a custom preview component: @@ -39,13 +28,14 @@ const Avatar = (props: AvatarProps) => { ; ``` -## Customizing Channel Avatar +## Customizing channel avatar You can also take advantage of the `Avatar` prop on the `ChannelHeader` and `ChannelList` components to override just that aspect of these components specifically, see the example below. An example of overriding just the `Avatar` component in the default `ChannelPreview` component. ```tsx +import { ChannelList } from 'stream-chat-react'; import type { ChannelAvatarProps } from 'stream-chat-react'; const CustomChannelAvatar = (props: ChannelAvatarProps) => { @@ -55,19 +45,21 @@ const CustomChannelAvatar = (props: ChannelAvatarProps) => { ; ``` -To override channel avatar in `ChannelHeader` we need to provide our component to `Channel` prop `ChannelAvatar`: +To override the channel avatar in `ChannelHeader` we need to provide it prop `Avatar`: ```tsx -import { Channel } from 'stream-chat-react'; +import { ChannelHeader } from 'stream-chat-react'; import type { ChannelAvatarProps } from 'stream-chat-react'; const CustomChannelAvatar = (props: ChannelAvatarProps) => { return
Custom Channel Avatar
; }; -; +; ``` +Also, we can take the advantage of existing SDK's `ChannelAvatar` and pass it to both `ChannelHeader` and `ChannelList` as described above. + ## Avatar Props ### className diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index 2cdd673a4..419663eed 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -112,7 +112,6 @@ type ChannelPropsForwardedToComponentContext< | 'AutocompleteSuggestionList' | 'Avatar' | 'BaseImage' - | 'ChannelAvatar' | 'CooldownTimer' | 'CustomMessageActionsList' | 'DateSeparator' @@ -1235,7 +1234,6 @@ const ChannelInner = < AutocompleteSuggestionList: props.AutocompleteSuggestionList, Avatar: props.Avatar, BaseImage: props.BaseImage, - ChannelAvatar: props.ChannelAvatar, CooldownTimer: props.CooldownTimer, CustomMessageActionsList: props.CustomMessageActionsList, DateSeparator: props.DateSeparator, @@ -1295,7 +1293,6 @@ const ChannelInner = < props.AutocompleteSuggestionList, props.Avatar, props.BaseImage, - props.ChannelAvatar, props.CooldownTimer, props.CustomMessageActionsList, props.DateSeparator, diff --git a/src/components/ChannelHeader/ChannelHeader.tsx b/src/components/ChannelHeader/ChannelHeader.tsx index d00516540..05e56d9d9 100644 --- a/src/components/ChannelHeader/ChannelHeader.tsx +++ b/src/components/ChannelHeader/ChannelHeader.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { MenuIcon as DefaultMenuIcon } from './icons'; -import { ChannelAvatar, ChannelAvatarProps } from '../Avatar'; +import { ChannelAvatarProps, Avatar as DefaultAvatar } from '../Avatar'; import { useChannelPreviewInfo } from '../ChannelPreview/hooks/useChannelPreviewInfo'; import { useChannelStateContext } from '../../context/ChannelStateContext'; @@ -12,7 +12,7 @@ import { useTranslationContext } from '../../context/TranslationContext'; import type { DefaultStreamChatGenerics } from '../../types/types'; export type ChannelHeaderProps = { - /** UI component to display a user's avatar, defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.tsx) */ + /** UI component to display an avatar, defaults to [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.tsx) component and accepts the same props as: [ChannelAvatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/ChannelAvatar.tsx) */ Avatar?: React.ComponentType; /** Manually set the image to render, defaults to the Channel image */ image?: string; @@ -33,7 +33,7 @@ export const ChannelHeader = < props: ChannelHeaderProps, ) => { const { - Avatar = ChannelAvatar, + Avatar = DefaultAvatar, MenuIcon = DefaultMenuIcon, image: overrideImage, live, diff --git a/src/components/ChannelHeader/__tests__/ChannelHeader.test.js b/src/components/ChannelHeader/__tests__/ChannelHeader.test.js index dbdb85364..b6d10b749 100644 --- a/src/components/ChannelHeader/__tests__/ChannelHeader.test.js +++ b/src/components/ChannelHeader/__tests__/ChannelHeader.test.js @@ -20,6 +20,7 @@ import { } from '../../../mock-builders'; import { toHaveNoViolations } from 'jest-axe'; import { axe } from '../../../../axe-helper'; +import { ChannelAvatar } from '../../Avatar'; expect.extend(toHaveNoViolations); const AVATAR_IMG_TEST_ID = 'avatar-img'; @@ -194,6 +195,10 @@ describe('ChannelHeader', () => { }); describe('group channel', () => { + const props = { + Avatar: ChannelAvatar, + }; + const getChannelState = (memberCount, channelData) => { const users = Array.from({ length: memberCount }, generateUser); const members = users.map((user) => generateMember({ user })); @@ -216,7 +221,7 @@ describe('ChannelHeader', () => { channelsData: [channelState], customUser: ownUser, }); - await renderComponentBase({ channel, client }); + await renderComponentBase({ channel, client, props }); await waitFor(() => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages).toHaveLength(4); @@ -237,7 +242,7 @@ describe('ChannelHeader', () => { client, } = await initClientWithChannels({ channelsData: [channelState] }); const updatedAttribute = { name: 'new-name' }; - await renderComponentBase({ channel, client }); + await renderComponentBase({ channel, client, props }); await waitFor(() => { expect(screen.queryByText(updatedAttribute.name)).not.toBeInTheDocument(); @@ -263,7 +268,7 @@ describe('ChannelHeader', () => { customUser: ownUser, }); const updatedAttribute = { image: 'new-image' }; - await renderComponentBase({ channel, client }); + await renderComponentBase({ channel, client, props }); await waitFor(() => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages).toHaveLength(3); @@ -295,7 +300,7 @@ describe('ChannelHeader', () => { customUser: ownUser, }); const updatedAttribute = { image: 'new-image' }; - await renderComponentBase({ channel, client }); + await renderComponentBase({ channel, client, props }); await waitFor(() => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages).toHaveLength(3); @@ -327,7 +332,7 @@ describe('ChannelHeader', () => { customUser: ownUser, }); const updatedAttribute = { custom: 'new-custom' }; - await renderComponentBase({ channel, client }); + await renderComponentBase({ channel, client, props }); await waitFor(() => { expect(screen.queryByText(updatedAttribute.custom)).not.toBeInTheDocument(); @@ -362,7 +367,7 @@ describe('ChannelHeader', () => { customUser: ownUser, }); const updatedAttribute = { custom: 'new-custom' }; - await renderComponentBase({ channel, client }); + await renderComponentBase({ channel, client, props }); await waitFor(() => { expect(screen.queryByText(updatedAttribute.custom)).not.toBeInTheDocument(); diff --git a/src/components/ChannelList/ChannelList.tsx b/src/components/ChannelList/ChannelList.tsx index 478cc9e66..6b7ba41eb 100644 --- a/src/components/ChannelList/ChannelList.tsx +++ b/src/components/ChannelList/ChannelList.tsx @@ -16,7 +16,7 @@ import { useNotificationRemovedFromChannelListener } from './hooks/useNotificati import { CustomQueryChannelsFn, usePaginatedChannels } from './hooks/usePaginatedChannels'; import { useUserPresenceChangedListener } from './hooks/useUserPresenceChangedListener'; import { MAX_QUERY_CHANNELS_LIMIT, moveChannelUp } from './utils'; -import { ChannelAvatar } from '../Avatar'; +import { Avatar as DefaultAvatar } from '../Avatar'; import { ChannelPreview, ChannelPreviewUIComponentProps } from '../ChannelPreview/ChannelPreview'; import { ChannelSearchProps, @@ -54,7 +54,7 @@ export type ChannelListProps< * to false, which will prevent channels not in the list from incrementing the list. The default is true. */ allowNewMessagesFromUnfilteredChannels?: boolean; - /** Custom UI component to display channel avatar, defaults to and accepts same props as: [ChannelAvatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/ChannelAvatar.tsx) */ + /** UI component to display an avatar, defaults to [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.tsx) component and accepts the same props as: [ChannelAvatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/ChannelAvatar.tsx) */ Avatar?: React.ComponentType; /** Optional function to filter channels prior to loading in the DOM. Do not use any complex or async logic that would delay the loading of the ChannelList. We recommend using a pure function with array methods like filter/sort/reduce. */ channelRenderFilterFn?: ( @@ -166,7 +166,7 @@ const UnMemoizedChannelList = < ) => { const { additionalChannelSearchProps, - Avatar = ChannelAvatar, + Avatar = DefaultAvatar, allowNewMessagesFromUnfilteredChannels, channelRenderFilterFn, ChannelSearch = DefaultChannelSearch, diff --git a/src/components/ChannelPreview/ChannelPreview.tsx b/src/components/ChannelPreview/ChannelPreview.tsx index 39e88f5ae..836805935 100644 --- a/src/components/ChannelPreview/ChannelPreview.tsx +++ b/src/components/ChannelPreview/ChannelPreview.tsx @@ -48,7 +48,7 @@ export type ChannelPreviewProps< channel: Channel; /** Current selected channel object */ activeChannel?: Channel; - /** Custom UI component to display user avatar, defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.tsx) */ + /** UI component to display an avatar, defaults to [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.tsx) component and accepts the same props as: [ChannelAvatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/ChannelAvatar.tsx) */ Avatar?: React.ComponentType>; /** Forces the update of preview component on channel update */ channelUpdateCount?: number; diff --git a/src/components/ChannelPreview/ChannelPreviewMessenger.tsx b/src/components/ChannelPreview/ChannelPreviewMessenger.tsx index 0792fbec7..4fb9c5435 100644 --- a/src/components/ChannelPreview/ChannelPreviewMessenger.tsx +++ b/src/components/ChannelPreview/ChannelPreviewMessenger.tsx @@ -1,6 +1,6 @@ import React, { useRef } from 'react'; import clsx from 'clsx'; -import { ChannelAvatar } from '../Avatar/ChannelAvatar'; +import { Avatar as DefaultAvatar } from '../Avatar'; import type { ChannelPreviewUIComponentProps } from './ChannelPreview'; import type { DefaultStreamChatGenerics } from '../../types/types'; @@ -12,7 +12,7 @@ const UnMemoizedChannelPreviewMessenger = < ) => { const { active, - Avatar = ChannelAvatar, + Avatar = DefaultAvatar, channel, className: customClassName = '', displayImage, diff --git a/src/components/ChannelPreview/__tests__/ChannelPreview.test.js b/src/components/ChannelPreview/__tests__/ChannelPreview.test.js index 1069e2e5f..035ac4bb5 100644 --- a/src/components/ChannelPreview/__tests__/ChannelPreview.test.js +++ b/src/components/ChannelPreview/__tests__/ChannelPreview.test.js @@ -2,6 +2,7 @@ import React from 'react'; import { act, render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; +import { ChannelAvatar } from '../../Avatar'; import { ChannelPreview } from '../ChannelPreview'; import { Chat } from '../../Chat'; @@ -601,6 +602,9 @@ describe('ChannelPreview', () => { }); describe('group channel', () => { + const channelPreviewProps = { + Avatar: ChannelAvatar, + }; const channelName = 'channel-name'; const channelState = getChannelState(3, { channel: { name: channelName } }); @@ -614,7 +618,7 @@ describe('ChannelPreview', () => { channelsData: [channelState], customUser: ownUser, }); - await renderComponent({ channel, client }); + await renderComponent({ channel, channelPreviewProps, client }); await waitFor(() => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages).toHaveLength(4); @@ -635,7 +639,7 @@ describe('ChannelPreview', () => { client, } = await initClientWithChannels({ channelsData: [channelState] }); const updatedAttribute = { name: 'new-name' }; - await renderComponent({ channel, client }); + await renderComponent({ channel, channelPreviewProps, client }); await waitFor(() => { expect(screen.queryByText(updatedAttribute.name)).not.toBeInTheDocument(); @@ -661,7 +665,7 @@ describe('ChannelPreview', () => { customUser: ownUser, }); const updatedAttribute = { image: 'new-image' }; - await renderComponent({ channel, client }); + await renderComponent({ channel, channelPreviewProps, client }); await waitFor(() => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages).toHaveLength(3); @@ -693,7 +697,7 @@ describe('ChannelPreview', () => { customUser: ownUser, }); const updatedAttribute = { image: 'new-image' }; - await renderComponent({ channel, client }); + await renderComponent({ channel, channelPreviewProps, client }); await waitFor(() => { const avatarImages = screen.getAllByTestId(AVATAR_IMG_TEST_ID); expect(avatarImages).toHaveLength(3); @@ -725,7 +729,7 @@ describe('ChannelPreview', () => { customUser: ownUser, }); const updatedAttribute = { custom: 'new-custom' }; - await renderComponent({ channel, client }); + await renderComponent({ channel, channelPreviewProps, client }); await waitFor(() => { expect(screen.queryByText(updatedAttribute.custom)).not.toBeInTheDocument(); @@ -760,7 +764,7 @@ describe('ChannelPreview', () => { customUser: ownUser, }); const updatedAttribute = { custom: 'new-custom' }; - await renderComponent({ channel, client }); + await renderComponent({ channel, channelPreviewProps, client }); await waitFor(() => { expect(screen.queryByText(updatedAttribute.custom)).not.toBeInTheDocument(); diff --git a/src/context/ComponentContext.tsx b/src/context/ComponentContext.tsx index b0d6eb6ff..ed28c7124 100644 --- a/src/context/ComponentContext.tsx +++ b/src/context/ComponentContext.tsx @@ -5,7 +5,6 @@ import { AttachmentProps, AvatarProps, BaseImageProps, - ChannelAvatarProps, CooldownTimerProps, CustomMessageActionsListProps, DateSeparatorProps, @@ -78,8 +77,6 @@ export type ComponentContextValue< Avatar?: React.ComponentType>; /** Custom UI component to display elements resp. a fallback in case of load error, defaults to and accepts same props as: [BaseImage](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Gallery/BaseImage.tsx) */ BaseImage?: React.ComponentType; - /** Custom UI component to display avatar for a channel in ChannelHeader: [ChannelAvatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/ChannelAvatar.tsx) */ - ChannelAvatar?: React.ComponentType; /** Custom UI component to display the slow mode cooldown timer, defaults to and accepts same props as: [CooldownTimer](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageInput/CooldownTimer.tsx) */ CooldownTimer?: React.ComponentType; /** Custom UI component to render set of buttons to be displayed in the MessageActionsBox, defaults to and accepts same props as: [CustomMessageActionsList](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageActions/CustomMessageActionsList.tsx) */