Skip to content

Commit

Permalink
feat: add group avatar
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinCupela committed Nov 7, 2024
1 parent 949446b commit 398cc6d
Show file tree
Hide file tree
Showing 19 changed files with 727 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

- `Avatar` - component used to display channel image
- `ChannelAvatar` - 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
Expand All @@ -24,10 +24,10 @@ import { useChannelListContext } from 'stream-chat-react';
export const CustomComponent = () => {
const { channels, setChannels } = useChannelListContext();
// component logic ...
return(
{/* rendered elements */}
);
}
return {
/* rendered elements */
};
};
```

## Value
Expand All @@ -37,7 +37,7 @@ export const CustomComponent = () => {
State representing the array of loaded channels. Channels query is executed by default only within the [`ChannelList` component](../core-components/channel-list.mdx) in the SDK.

| Type |
|-------------|
| ----------- |
| `Channel[]` |

### setChannels
Expand Down Expand Up @@ -109,5 +109,5 @@ const Sidebar = () => {
```
| Type |
|---------------------------------------|
| ------------------------------------- |
| `Dispatch<SetStateAction<Channel[]>>` |
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ The [default `BaseImage` component](../../utility-components/base-image) tries t
| --------- | ----------------------------------------------------------------- |
| component | <GHComponentLink text='BaseImage' path='/Gallery/BaseImage.tsx'/> |

### ChannelAvatar

Custom UI component to display avatar for a channel in ChannelHeader.

| Type | Default |
| --------- | ------------------------------------------------------------------------ |
| component | <GHComponentLink text='ChannelAvatar' path='/Avatar/ChannelAvatar.tsx'/> |

### CooldownTimer

Custom UI component to display the slow mode cooldown timer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,11 @@ list from incrementing the list.

### Avatar

Custom UI component to display the user's avatar.
Custom UI component to display the channel avatar. The default avatar component for `ChannelList` is `ChannelAvatar`.

| Type | Default |
| --------- | ---------------------------------------------------------- |
| component | <GHComponentLink text='Avatar' path='/Avatar/Avatar.tsx'/> |
| Type | Default |
| --------- | ----------------------------------------------------------------- |
| component | <GHComponentLink text='Avatar' path='/Avatar/ChannelAvatar.tsx'/> |

### channelRenderFilterFn

Expand Down
8 changes: 8 additions & 0 deletions docusaurus/docs/React/components/core-components/channel.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ Custom UI component to display a user's avatar.
| --------- | ---------------------------------------------------------- |
| component | <GHComponentLink text='Avatar' path='/Avatar/Avatar.tsx'/> |

### ChannelAvatar

Custom UI component to display avatar for a channel in ChannelHeader.

| Type | Default |
| --------- | ------------------------------------------------------------------------ |
| component | <GHComponentLink text='ChannelAvatar' path='/Avatar/ChannelAvatar.tsx'/> |

### 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.
Expand Down
78 changes: 59 additions & 19 deletions docusaurus/docs/React/components/utility-components/avatar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,72 @@ id: avatar
title: Avatar
---

The `Avatar` component displays an image, with fallback to the first letter of the optional name prop.
The SDK supports variety of avatar component types. Different approach is taken to display a channel avatar and an avatar of a message author.

## Basic Usage
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.

A typical use case for the `Avatar` component would be to import and use in your custom components that will completely override a header component, preview component, or similar.
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.

## 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.

Here's an example of using the `Avatar` component within a custom preview component:

```tsx
import { Avatar } from 'stream-chat-react';

const YourCustomChannelPreview = (props) => {
return (
<div>
<Avatar name={props.displayTitle} image={props.displayImage} />
<div> Other channel info needed in the preview </div>
</div>
);
import { Channel } from 'stream-chat-react';
import type { AvatarProps } from 'stream-chat-react';

const Avatar = (props: AvatarProps) => {
return <div>Custom avatar UI</div>;
};

<ChannelList Preview={YourCustomChannelPreview} />;
<Channel Avatar={Avatar} />;
```

## UI Customization
## 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 `ChannelPreviewMessenger` component.
An example of overriding just the `Avatar` component in the default `ChannelPreview` component.

```tsx
import type { ChannelAvatarProps } from 'stream-chat-react';

const CustomChannelAvatar = (props: ChannelAvatarProps) => {
return <div>Custom Channel Avatar</div>;
};

<ChannelList Avatar={CustomChannelAvatar} />;
```

To override channel avatar in `ChannelHeader` we need to provide our component to `Channel` prop `ChannelAvatar`:

```tsx
const CustomAvatar = (props) => {
return <Avatar image={props.image} />;
import { Channel } from 'stream-chat-react';
import type { ChannelAvatarProps } from 'stream-chat-react';

const CustomChannelAvatar = (props: ChannelAvatarProps) => {
return <div>Custom Channel Avatar</div>;
};

<ChannelList Preview={(props) => <ChannelPreviewMessenger {...props} Avatar={CustomAvatar} />} />;
<Channel ChannelAvatar={CustomChannelAvatar} />;
```

## Props
## Avatar Props

### className

Expand Down Expand Up @@ -89,3 +117,15 @@ The entire user object for the chat user represented by the Avatar component. Th
| Type |
| ------ |
| Object |

## ChannelAvatar Props

Besides the `Avatar` props listed above, the `ChannelAvatar` component accepts the following props.

### groupChannelDisplayInfo

Mapping of image URLs to names which initials will be used as fallbacks in case image assets fail to load.

| Type |
| ------------------------------------- |
| `{ image?: string; name?: string }[]` |
6 changes: 3 additions & 3 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import type { DefaultStreamChatGenerics } from '../../types/types';
export type AvatarProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = {
/** Custom class that will be merged with the default class */
/** Custom root element class that will be merged with the default class */
className?: string;
/** Image URL or default is an image of the first initial of the name if there is one */
image?: string | null;
/** Name of the image, used for title tag fallback */
name?: string;
/** click event handler */
/** click event handler attached to the component root element */
onClick?: (event: React.BaseSyntheticEvent) => void;
/** mouseOver event handler */
/** mouseOver event handler attached to the component root element */
onMouseOver?: (event: React.BaseSyntheticEvent) => void;
/** The entire user object for the chat user displayed in the component */
user?: UserResponse<StreamChatGenerics>;
Expand Down
22 changes: 22 additions & 0 deletions src/components/Avatar/ChannelAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { Avatar, AvatarProps, GroupAvatar, GroupAvatarProps } from './index';
import type { DefaultStreamChatGenerics } from '../../types';

export type ChannelAvatarProps<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
> = Partial<GroupAvatarProps> & AvatarProps<StreamChatGenerics>;

export const ChannelAvatar = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({
groupChannelDisplayInfo,
image,
name,
user,
...sharedProps
}: ChannelAvatarProps<StreamChatGenerics>) => {
if (groupChannelDisplayInfo) {
return <GroupAvatar groupChannelDisplayInfo={groupChannelDisplayInfo} {...sharedProps} />;
}
return <Avatar image={image} name={name} user={user} {...sharedProps} />;
};
39 changes: 39 additions & 0 deletions src/components/Avatar/GroupAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import clsx from 'clsx';
import React from 'react';
import { Avatar, AvatarProps } from './Avatar';
import { GroupChannelDisplayInfo } from '../ChannelPreview';

export type GroupAvatarProps = Pick<AvatarProps, 'className' | 'onClick' | 'onMouseOver'> & {
/** Mapping of image URLs to names which initials will be used as fallbacks in case image assets fail to load. */
groupChannelDisplayInfo: GroupChannelDisplayInfo;
};

export const GroupAvatar = ({
className,
groupChannelDisplayInfo,
onClick,
onMouseOver,
}: GroupAvatarProps) => (
<div
className={clsx(
`str-chat__avatar-group`,
{ 'str-chat__avatar-group--three-part': groupChannelDisplayInfo.length === 3 },
className,
)}
data-testid='group-avatar'
onClick={onClick}
onMouseOver={onMouseOver}
role='button'
>
{groupChannelDisplayInfo.slice(0, 4).map(({ image, name }, i) => (
<Avatar
className={clsx({
'str-chat__avatar--single': groupChannelDisplayInfo.length === 3 && i === 0,
})}
image={image}
key={`${name}-${image}-${i}`}
name={name}
/>
))}
</div>
);
2 changes: 2 additions & 0 deletions src/components/Avatar/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './Avatar';
export * from './ChannelAvatar';
export * from './GroupAvatar';
3 changes: 3 additions & 0 deletions src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ type ChannelPropsForwardedToComponentContext<
| 'AutocompleteSuggestionList'
| 'Avatar'
| 'BaseImage'
| 'ChannelAvatar'
| 'CooldownTimer'
| 'CustomMessageActionsList'
| 'DateSeparator'
Expand Down Expand Up @@ -1234,6 +1235,7 @@ const ChannelInner = <
AutocompleteSuggestionList: props.AutocompleteSuggestionList,
Avatar: props.Avatar,
BaseImage: props.BaseImage,
ChannelAvatar: props.ChannelAvatar,
CooldownTimer: props.CooldownTimer,
CustomMessageActionsList: props.CustomMessageActionsList,
DateSeparator: props.DateSeparator,
Expand Down Expand Up @@ -1293,6 +1295,7 @@ const ChannelInner = <
props.AutocompleteSuggestionList,
props.Avatar,
props.BaseImage,
props.ChannelAvatar,
props.CooldownTimer,
props.CustomMessageActionsList,
props.DateSeparator,
Expand Down
9 changes: 5 additions & 4 deletions src/components/ChannelHeader/ChannelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';

import { MenuIcon as DefaultMenuIcon } from './icons';

import { AvatarProps, Avatar as DefaultAvatar } from '../Avatar';
import { ChannelAvatar, ChannelAvatarProps } from '../Avatar';
import { useChannelPreviewInfo } from '../ChannelPreview/hooks/useChannelPreviewInfo';

import { useChannelStateContext } from '../../context/ChannelStateContext';
Expand All @@ -13,7 +13,7 @@ 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) */
Avatar?: React.ComponentType<AvatarProps>;
Avatar?: React.ComponentType<ChannelAvatarProps>;
/** Manually set the image to render, defaults to the Channel image */
image?: string;
/** Show a little indicator that the Channel is live right now */
Expand All @@ -33,7 +33,7 @@ export const ChannelHeader = <
props: ChannelHeaderProps,
) => {
const {
Avatar = DefaultAvatar,
Avatar = ChannelAvatar,
MenuIcon = DefaultMenuIcon,
image: overrideImage,
live,
Expand All @@ -43,7 +43,7 @@ export const ChannelHeader = <
const { channel, watcher_count } = useChannelStateContext<StreamChatGenerics>('ChannelHeader');
const { openMobileNav } = useChatContext<StreamChatGenerics>('ChannelHeader');
const { t } = useTranslationContext('ChannelHeader');
const { displayImage, displayTitle } = useChannelPreviewInfo({
const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({
channel,
overrideImage,
overrideTitle,
Expand All @@ -62,6 +62,7 @@ export const ChannelHeader = <
</button>
<Avatar
className='str-chat__avatar--channel-header'
groupChannelDisplayInfo={groupChannelDisplayInfo}
image={displayImage}
name={displayTitle}
/>
Expand Down
Loading

0 comments on commit 398cc6d

Please sign in to comment.