Skip to content

Commit

Permalink
feat: add group avatar (#2556)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinCupela authored Nov 14, 2024
1 parent 41b23db commit 414745d
Show file tree
Hide file tree
Showing 18 changed files with 716 additions and 151 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -187,7 +187,7 @@ 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 `Avatar`.

| Type | Default |
| --------- | ---------------------------------------------------------- |
Expand Down
70 changes: 51 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,64 @@ id: avatar
title: Avatar
---

The `Avatar` component displays an image, with fallback to the first letter of the optional name prop.
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:

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

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

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:

```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
const CustomAvatar = (props) => {
return <Avatar image={props.image} />;
import { ChannelList } 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} />} />;
<ChannelList Avatar={CustomChannelAvatar} />;
```

## Props
To override the channel avatar in `ChannelHeader` we need to provide it prop `Avatar`:

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

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

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

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

Expand Down Expand Up @@ -89,3 +109,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 }[]` |
12 changes: 10 additions & 2 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat';
import {
Channel,
ChannelAvatar,
ChannelHeader,
ChannelList,
Chat,
Expand Down Expand Up @@ -75,10 +76,17 @@ const App = () => {
<ChatView>
<ChatView.Selector />
<ChatView.Channels>
<ChannelList filters={filters} options={options} sort={sort} />
<ChannelList
Avatar={ChannelAvatar}
filters={filters}
options={options}
sort={sort}
showChannelSearch
additionalChannelSearchProps={{ searchForChannels: true }}
/>
<Channel>
<Window>
<ChannelHeader />
<ChannelHeader Avatar={ChannelAvatar} />
<MessageList returnAllReadData />
<MessageInput focus />
</Window>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
"@semantic-release/changelog": "^6.0.2",
"@semantic-release/git": "^10.0.1",
"@stream-io/rollup-plugin-node-builtins": "^2.1.5",
"@stream-io/stream-chat-css": "^5.2.0",
"@stream-io/stream-chat-css": "^5.4.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^13.1.1",
"@testing-library/react-hooks": "^8.0.0",
Expand Down
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';
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 { ChannelAvatarProps, Avatar as DefaultAvatar } from '../Avatar';
import { useChannelPreviewInfo } from '../ChannelPreview/hooks/useChannelPreviewInfo';

import { useChannelStateContext } from '../../context/ChannelStateContext';
Expand All @@ -12,8 +12,8 @@ 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) */
Avatar?: React.ComponentType<AvatarProps>;
/** 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<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 Down Expand Up @@ -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 414745d

Please sign in to comment.