Skip to content

Commit 29bb396

Browse files
authored
feat: unify channel and thread display titles (#2998)
BREAKING CHANGE: useChannelPreviewInfo now always returns a defined `groupChannelDisplayInfo` (object with `members` and `overflowCount`). For 1:1 or ≤2 members it returns `{ members: [], overflowCount: undefined }`. Code that treated `groupChannelDisplayInfo == null` as “not a group” should use e.g. `groupChannelDisplayInfo.members.length >= 2` instead.
1 parent 7308866 commit 29bb396

32 files changed

Lines changed: 565 additions & 177 deletions

examples/vite/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,9 @@ const App = () => {
341341
<ThreadStateSync />
342342
<ThreadList />
343343
<ChatView.ThreadAdapter>
344-
<Thread virtualized />
344+
<WithDragAndDropUpload className='str-chat__dropzone-root--thread'>
345+
<Thread virtualized />
346+
</WithDragAndDropUpload>
345347
</ChatView.ThreadAdapter>
346348
</ChatView.Threads>
347349
</ChatView>

src/components/Avatar/ChannelAvatar.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,28 @@ import React from 'react';
22

33
import { Avatar, GroupAvatar } from './';
44
import type { AvatarProps, GroupAvatarProps } from './';
5+
import type { GroupAvatarMember } from './GroupAvatar';
56

67
export type ChannelAvatarProps = Partial<Omit<GroupAvatarProps & AvatarProps, 'size'>> & {
78
size: GroupAvatarProps['size'];
9+
/** When set with length >= 2, GroupAvatar is used. */
10+
displayMembers?: GroupAvatarMember[];
11+
overflowCount?: number;
812
};
913

1014
export const ChannelAvatar = ({
11-
groupChannelDisplayInfo,
15+
displayMembers,
1216
imageUrl,
17+
overflowCount,
1318
size,
1419
userName,
1520
...sharedProps
1621
}: ChannelAvatarProps) => {
17-
if (groupChannelDisplayInfo) {
22+
if ((displayMembers?.length ?? 0) >= 2) {
1823
return (
1924
<GroupAvatar
20-
groupChannelDisplayInfo={groupChannelDisplayInfo}
25+
displayMembers={displayMembers}
26+
overflowCount={overflowCount}
2127
size={size}
2228
{...sharedProps}
2329
/>

src/components/Avatar/GroupAvatar.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
11
import clsx from 'clsx';
22
import React, { type ComponentPropsWithoutRef } from 'react';
33
import { Avatar, type AvatarProps } from './Avatar';
4-
import type { GroupChannelDisplayInfo } from '../ChannelPreview';
4+
5+
export type GroupAvatarMember = {
6+
imageUrl?: string;
7+
userName?: string;
8+
};
59

610
export type GroupAvatarProps = ComponentPropsWithoutRef<'div'> & {
7-
/** Mapping of image URLs to names which initials will be used as fallbacks in case image assets fail to load. */
8-
groupChannelDisplayInfo: GroupChannelDisplayInfo;
11+
/** List of members to show as avatars; at most 2 when overflowCount is set, otherwise 4. Defaults to [] when omitted. */
12+
displayMembers?: GroupAvatarMember[];
13+
/** Optional count for the "+N" badge when there are more members than shown. */
14+
overflowCount?: number;
915
size: '2xl' | 'xl' | 'lg' | null;
1016
isOnline?: boolean;
11-
overflowCount?: number;
1217
};
1318

1419
/**
15-
* Avatar component to display multiple users' avatars in a group channel, with a maximum of 4 avatars shown.
16-
* Renders a single Avatar if only one user is provided.
20+
* Avatar component to display multiple users' avatars in a group.
21+
* Renders a single Avatar if fewer than 2 members. Otherwise, renders up to 2 avatars (when overflowCount is set) or 4, plus an optional +N badge.
1722
*/
1823
// TODO: rename to AvatarGroup
1924
export const GroupAvatar = ({
2025
className,
21-
groupChannelDisplayInfo,
26+
displayMembers = [],
2227
isOnline,
2328
overflowCount,
2429
size,
2530
...rest
2631
}: GroupAvatarProps) => {
2732
const displayCountBadge = typeof overflowCount === 'number' && overflowCount > 0;
2833

29-
if (!groupChannelDisplayInfo || groupChannelDisplayInfo.length < 2) {
30-
const [firstUser] = groupChannelDisplayInfo || [];
34+
if (displayMembers.length < 2) {
35+
const firstUser = displayMembers[0];
3136

3237
return (
3338
<Avatar
@@ -64,7 +69,7 @@ export const GroupAvatar = ({
6469
role='button'
6570
{...rest}
6671
>
67-
{groupChannelDisplayInfo
72+
{displayMembers
6873
.slice(0, displayCountBadge ? 2 : 4)
6974
.map(({ imageUrl, userName }, index) => (
7075
<Avatar

src/components/ChannelHeader/ChannelHeader.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import React from 'react';
22

33
import { IconLayoutAlignLeft } from '../Icons/icons';
4-
import type { ChannelAvatarProps } from '../Avatar';
5-
import { Avatar as DefaultAvatar } from '../Avatar';
4+
import { type ChannelAvatarProps, ChannelAvatar as DefaultAvatar } from '../Avatar';
65
import { useChannelHeaderOnlineStatus } from './hooks/useChannelHeaderOnlineStatus';
76
import { useChannelPreviewInfo } from '../ChannelPreview/hooks/useChannelPreviewInfo';
87
import { useChannelStateContext } from '../../context/ChannelStateContext';
@@ -33,7 +32,7 @@ export const ChannelHeader = (props: ChannelHeaderProps) => {
3332
} = props;
3433

3534
const { channel } = useChannelStateContext();
36-
const { navOpen } = useChatContext('ChannelHeader');
35+
const { navOpen } = useChatContext();
3736
const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({
3837
channel,
3938
overrideImage,
@@ -60,8 +59,9 @@ export const ChannelHeader = (props: ChannelHeaderProps) => {
6059
</div>
6160
<Avatar
6261
className='str-chat__avatar--channel-header'
63-
groupChannelDisplayInfo={groupChannelDisplayInfo}
62+
displayMembers={groupChannelDisplayInfo?.members}
6463
imageUrl={displayImage}
64+
overflowCount={groupChannelDisplayInfo?.overflowCount}
6565
size='lg'
6666
userName={displayTitle}
6767
/>

src/components/ChannelPreview/ChannelPreviewMessenger.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ const UnMemoizedChannelPreviewMessenger = (props: ChannelPreviewUIComponentProps
6666
role='option'
6767
>
6868
<Avatar
69-
groupChannelDisplayInfo={groupChannelDisplayInfo}
69+
displayMembers={groupChannelDisplayInfo?.members}
7070
imageUrl={displayImage}
71+
overflowCount={groupChannelDisplayInfo?.overflowCount}
7172
size='xl'
7273
userName={avatarName}
7374
/>

src/components/ChannelPreview/__tests__/utils.test.js

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import {
1414
useMockedApis,
1515
} from 'mock-builders';
1616

17-
import { getDisplayImage, getDisplayTitle, getLatestMessagePreview } from '../utils';
17+
import {
18+
getChannelDisplayImage,
19+
getGroupChannelDisplayInfo,
20+
getLatestMessagePreview,
21+
} from '../utils';
1822
import { generateStaticLocationResponse } from '../../../mock-builders';
1923
import { render } from '@testing-library/react';
2024

@@ -107,7 +111,7 @@ describe('ChannelPreview utils', () => {
107111
generateChannel({ channel: { name } }),
108112
);
109113

110-
expect(getDisplayTitle(channel, chatClient.user)).toBe(name);
114+
expect(channel.getDisplayName()).toBe(name);
111115
});
112116

113117
it('should return name of other member of conversation if only 2 members and channel name doesnot exist', async () => {
@@ -120,7 +124,7 @@ describe('ChannelPreview utils', () => {
120124
],
121125
}),
122126
);
123-
expect(getDisplayTitle(channel, chatClient.user)).toBe(otherUser.name);
127+
expect(channel.getDisplayName()).toBe(otherUser.name);
124128
});
125129
});
126130

@@ -131,10 +135,10 @@ describe('ChannelPreview utils', () => {
131135
generateChannel({ channel: { image } }),
132136
);
133137

134-
expect(getDisplayImage(channel, chatClient.user)).toBe(image);
138+
expect(channel.getDisplayImage()).toBe(image);
135139
});
136140

137-
it('should return picture of other member of conversation if only 2 members and channel name doesnot exist', async () => {
141+
it('should return null when no image is available (image fallback removed)', async () => {
138142
const otherUser = generateUser();
139143
const channel = await getQueriedChannelInstance(
140144
generateChannel({
@@ -144,7 +148,74 @@ describe('ChannelPreview utils', () => {
144148
],
145149
}),
146150
);
147-
expect(getDisplayImage(channel, chatClient.user)).toBe(otherUser.image);
151+
// getDisplayImage no longer falls back to member image, only channel.data.image
152+
expect(channel.getDisplayImage()).toBeNull();
153+
});
154+
});
155+
156+
describe('getChannelDisplayImage (utils)', () => {
157+
it('returns channel.data.image when set', async () => {
158+
const image = nanoid();
159+
const channel = await getQueriedChannelInstance(
160+
generateChannel({ channel: { image } }),
161+
);
162+
expect(getChannelDisplayImage(channel)).toBe(image);
163+
});
164+
165+
it('returns other member user.image for DM (2 members) when channel has no image', async () => {
166+
const otherUser = generateUser({ image: 'https://other-avatar.jpg' });
167+
const channel = await getQueriedChannelInstance(
168+
generateChannel({
169+
members: [
170+
generateMember({ user: otherUser }),
171+
generateMember({ user: clientUser }),
172+
],
173+
}),
174+
);
175+
expect(getChannelDisplayImage(channel)).toBe('https://other-avatar.jpg');
176+
});
177+
178+
it('returns undefined for DM when other member has no image', async () => {
179+
const otherUser = generateUser({ image: undefined });
180+
const channel = await getQueriedChannelInstance(
181+
generateChannel({
182+
members: [
183+
generateMember({ user: otherUser }),
184+
generateMember({ user: clientUser }),
185+
],
186+
}),
187+
);
188+
expect(getChannelDisplayImage(channel)).toBeUndefined();
189+
});
190+
});
191+
192+
describe('getGroupChannelDisplayInfo (utils)', () => {
193+
it('returns undefined for 2 or fewer members', async () => {
194+
const channel = await getQueriedChannelInstance(
195+
generateChannel({
196+
members: [
197+
generateMember({ user: generateUser() }),
198+
generateMember({ user: clientUser }),
199+
],
200+
}),
201+
);
202+
expect(getGroupChannelDisplayInfo(channel)).toBeUndefined();
203+
});
204+
205+
it('returns members and overflowCount for 3+ members', async () => {
206+
const channel = await getQueriedChannelInstance(
207+
generateChannel({
208+
members: [
209+
generateMember({ user: generateUser({ image: 'a.jpg', name: 'A' }) }),
210+
generateMember({ user: generateUser({ image: 'b.jpg', name: 'B' }) }),
211+
generateMember({ user: clientUser }),
212+
],
213+
}),
214+
);
215+
const info = getGroupChannelDisplayInfo(channel);
216+
expect(info).toBeDefined();
217+
expect(info.members.length).toBeGreaterThanOrEqual(2);
218+
expect(info.members.every((m) => 'imageUrl' in m && 'userName' in m)).toBe(true);
148219
});
149220
});
150221
});

0 commit comments

Comments
 (0)