Skip to content

Commit d6c08ca

Browse files
authored
feat: redesign typing indicator (#3005)
1 parent 08dc4b6 commit d6c08ca

39 files changed

Lines changed: 690 additions & 137 deletions

examples/vite/src/App.tsx

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
ReactionOptions,
4141
mapEmojiMartData,
4242
useStateStore,
43+
TypingIndicator,
4344
} from 'stream-chat-react';
4445
import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis';
4546
import { init, SearchIndex } from 'emoji-mart';
@@ -329,31 +330,35 @@ const App = () => {
329330
sort={sort}
330331
showChannelSearch
331332
/>
332-
<Channel>
333-
<WithDragAndDropUpload>
334-
<Window>
335-
<ChannelHeader Avatar={ChannelAvatar} />
336-
<MessageList returnAllReadData />
337-
<AIStateIndicator />
338-
<MessageInput
339-
focus
340-
audioRecordingEnabled
341-
maxRows={10}
342-
asyncMessagesMultiSendEnabled
343-
/>
344-
</Window>
345-
</WithDragAndDropUpload>
346-
<WithDragAndDropUpload className='str-chat__dropzone-root--thread'>
347-
<Thread virtualized />
348-
</WithDragAndDropUpload>
349-
</Channel>
333+
<WithComponents overrides={{ TypingIndicator }}>
334+
<Channel>
335+
<WithDragAndDropUpload>
336+
<Window>
337+
<ChannelHeader Avatar={ChannelAvatar} />
338+
<MessageList returnAllReadData />
339+
<AIStateIndicator />
340+
<MessageInput
341+
focus
342+
audioRecordingEnabled
343+
maxRows={10}
344+
asyncMessagesMultiSendEnabled
345+
/>
346+
</Window>
347+
</WithDragAndDropUpload>
348+
<WithDragAndDropUpload className='str-chat__dropzone-root--thread'>
349+
<Thread virtualized />
350+
</WithDragAndDropUpload>
351+
</Channel>
352+
</WithComponents>
350353
</ChatView.Channels>
351354
<ChatView.Threads>
352355
<ThreadStateSync />
353356
<ThreadList />
354357
<ChatView.ThreadAdapter>
355358
<WithDragAndDropUpload className='str-chat__dropzone-root--thread'>
356-
<Thread virtualized />
359+
<WithComponents overrides={{ TypingIndicator }}>
360+
<Thread virtualized />
361+
</WithComponents>
357362
</WithDragAndDropUpload>
358363
</ChatView.ThreadAdapter>
359364
</ChatView.Threads>

examples/vite/src/stream-imports-layout.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
//@use 'stream-chat-react/dist/scss/v2/Thread/Thread-layout';
4343
//@use 'stream-chat-react/dist/scss/v2/Search/Search-layout';
4444
@use 'stream-chat-react/dist/scss/v2/Tooltip/Tooltip-layout';
45-
@use 'stream-chat-react/dist/scss/v2/TypingIndicator/TypingIndicator-layout';
45+
//@use 'stream-chat-react/dist/scss/v2/TypingIndicator/TypingIndicator-layout';
4646
// @use 'stream-chat-react/dist/scss/v2/ThreadList/ThreadList-layout';
4747
//@use 'stream-chat-react/dist/scss/v2/ChatView/ChatView-layout';
4848
//@use 'stream-chat-react/dist/scss/v2/UnreadCountBadge/UnreadCountBadge-layout';

examples/vite/src/stream-imports-theme.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
//@use 'stream-chat-react/dist/scss/v2/Thread/Thread-theme';
3737
//@use 'stream-chat-react/dist/scss/v2/Search/Search-theme';
3838
@use 'stream-chat-react/dist/scss/v2/Tooltip/Tooltip-theme';
39-
@use 'stream-chat-react/dist/scss/v2/TypingIndicator/TypingIndicator-theme';
39+
//@use 'stream-chat-react/dist/scss/v2/TypingIndicator/TypingIndicator-theme';
4040
// @use 'stream-chat-react/dist/scss/v2/ThreadList/ThreadList-theme';
4141
//@use 'stream-chat-react/dist/scss/v2/ChatView/ChatView-theme';
4242
//@use 'stream-chat-react/dist/scss/v2/UnreadCountBadge/UnreadCountBadge-theme';

src/components/Avatar/AvatarStack.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { type ComponentProps, type ElementType } from 'react';
22
import { useComponentContext } from '../../context';
33
import { type AvatarProps, Avatar as DefaultAvatar } from './Avatar';
44
import clsx from 'clsx';
5+
import { Badge, type BadgeSize } from '../Badge';
56

67
export function AvatarStack({
8+
badgeSize,
79
component: Component = 'div',
810
displayInfo = [],
911
overflowCount,
@@ -12,7 +14,8 @@ export function AvatarStack({
1214
component?: ElementType;
1315
displayInfo?: (Pick<AvatarProps, 'imageUrl' | 'userName'> & { id?: string })[];
1416
overflowCount?: number;
15-
size: 'sm' | 'xs' | null;
17+
size: 'md' | 'sm' | 'xs' | null;
18+
badgeSize?: BadgeSize;
1619
}) {
1720
const { Avatar = DefaultAvatar } = useComponentContext(AvatarStack.name);
1821

@@ -35,7 +38,13 @@ export function AvatarStack({
3538
/>
3639
))}
3740
{typeof overflowCount === 'number' && overflowCount > 0 && (
38-
<div className='str-chat__avatar-stack__count-badge'>{overflowCount}</div>
41+
<Badge
42+
className='str-chat__avatar-stack__count-badge'
43+
size={badgeSize ?? size}
44+
variant='counter'
45+
>
46+
+{overflowCount}
47+
</Badge>
3948
)}
4049
</Component>
4150
);

src/components/Avatar/styling/AvatarStack.scss

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,27 @@
22
display: flex;
33
align-items: center;
44

5-
& > .str-chat__avatar:not(:first-child),
65
.str-chat__avatar-stack__count-badge {
7-
margin-left: calc(var(--spacing-xs) * -1);
6+
position: relative;
87
}
9-
10-
&.str-chat__avatar-stack--size-sm {
11-
--avatar-stack-count-badge-size: 24px;
12-
// FIXME?: should be sm but it looks way too big
13-
--avatar-stack-count-badge-font-size: var(--typography-font-size-xs);
14-
8+
&.str-chat__avatar-stack--size-xs {
9+
& > .str-chat__avatar:not(:first-child),
1510
.str-chat__avatar-stack__count-badge {
16-
padding-inline: var(--spacing-xs);
11+
margin-left: calc(var(--spacing-xs) * -1);
1712
}
1813
}
1914

20-
&.str-chat__avatar-stack--size-xs {
21-
--avatar-stack-count-badge-size: 20px;
22-
--avatar-stack-count-badge-font-size: var(--typography-font-size-xxs);
23-
15+
&.str-chat__avatar-stack--size-sm {
16+
& > .str-chat__avatar:not(:first-child),
2417
.str-chat__avatar-stack__count-badge {
25-
padding-inline: var(--spacing-xxs);
18+
margin-left: calc(var(--spacing-sm) * -1);
2619
}
2720
}
2821

29-
.str-chat__avatar-stack__count-badge {
30-
font-size: var(--avatar-stack-count-badge-font-size);
31-
font-weight: var(--typography-font-weight-bold);
32-
display: flex;
33-
justify-content: center;
34-
align-items: center;
35-
height: var(--avatar-stack-count-badge-size);
36-
min-width: var(--avatar-stack-count-badge-size);
37-
min-height: var(--avatar-stack-count-badge-size);
38-
border-radius: var(--radius-max);
39-
border: 1px solid var(--border-core-subtle);
40-
background: var(--badge-bg-default);
41-
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14);
42-
line-height: 1;
43-
position: relative;
22+
&.str-chat__avatar-stack--size-md {
23+
& > .str-chat__avatar:not(:first-child),
24+
.str-chat__avatar-stack__count-badge {
25+
margin-left: calc(var(--spacing-sm) * -1);
26+
}
4427
}
4528
}

src/components/Badge/Badge.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import clsx from 'clsx';
22
import React, { type ComponentProps } from 'react';
33

4-
export type BadgeVariant = 'default' | 'primary' | 'error' | 'neutral' | 'inverse';
4+
export type BadgeVariant =
5+
| 'default'
6+
| 'primary'
7+
| 'error'
8+
| 'neutral'
9+
| 'counter'
10+
| 'inverse';
511

6-
export type BadgeSize = 'sm' | 'md' | 'lg';
12+
export type BadgeSize = 'xs' | 'sm' | 'md' | 'lg' | null;
713

8-
export type BadgeProps = ComponentProps<'span'> & {
14+
export type BadgeProps = ComponentProps<'div'> & {
915
/** Visual variant mapping to design tokens */
1016
variant?: BadgeVariant;
1117
/** Size preset (typography and padding) */
@@ -23,15 +29,15 @@ export const Badge = ({
2329
variant = 'default',
2430
...spanProps
2531
}: BadgeProps) => (
26-
<span
32+
<div
2733
{...spanProps}
2834
className={clsx(
2935
'str-chat__badge',
3036
`str-chat__badge--variant-${variant}`,
31-
`str-chat__badge--size-${size}`,
37+
{ [`str-chat__badge--size-${size}`]: size },
3238
className,
3339
)}
3440
>
3541
{children}
36-
</span>
42+
</div>
3743
);

src/components/Badge/styling/Badge.scss

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Figma: Badge Notification (scroll-to-bottom unread count)
33

44
.str-chat__badge {
5-
display: inline-flex;
5+
display: flex;
66
align-items: center;
77
justify-content: center;
88
font-weight: var(--typography-font-weight-bold);
@@ -69,3 +69,37 @@
6969
border-width: 2px;
7070
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14);
7171
}
72+
73+
.str-chat__badge--variant-counter {
74+
border-radius: var(--radius-max);
75+
border: 1px solid var(--border-core-subtle);
76+
background: var(--badge-bg-default);
77+
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14);
78+
font: var(--str-chat__numeric-xl-text);
79+
80+
&.str-chat__badge--size-xs {
81+
min-width: 20px;
82+
min-height: 20px;
83+
padding-inline: var(--spacing-xxs);
84+
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14);
85+
font: var(--str-chat__numeric-md-text);
86+
}
87+
88+
&.str-chat__badge--size-sm {
89+
min-width: 24px;
90+
min-height: 24px;
91+
padding-inline: var(--spacing-xs);
92+
}
93+
94+
&.str-chat__badge--size-md {
95+
min-width: 32px;
96+
min-height: 32px;
97+
padding-inline: var(--spacing-xs);
98+
}
99+
100+
&.str-chat__badge--size-lg {
101+
min-width: 40px;
102+
min-height: 40px;
103+
padding-inline: var(--spacing-sm);
104+
}
105+
}

src/components/ChannelHeader/ChannelHeader.tsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,39 @@ import React from 'react';
22

33
import { IconLayoutAlignLeft } from '../Icons/icons';
44
import { type ChannelAvatarProps, ChannelAvatar as DefaultAvatar } from '../Avatar';
5+
import { TypingIndicatorHeader } from '../TypingIndicator/TypingIndicatorHeader';
56
import { useChannelHeaderOnlineStatus } from './hooks/useChannelHeaderOnlineStatus';
67
import { useChannelPreviewInfo } from '../ChannelPreview/hooks/useChannelPreviewInfo';
78
import { useChannelStateContext } from '../../context/ChannelStateContext';
89
import { useChatContext } from '../../context/ChatContext';
10+
import { useTypingContext } from '../../context/TypingContext';
911
import clsx from 'clsx';
1012
import { ToggleSidebarButton } from '../Button/ToggleSidebarButton';
1113

14+
const ChannelHeaderSubtitle = () => {
15+
const { channelConfig } = useChannelStateContext('ChannelHeaderSubtitle');
16+
const { client } = useChatContext('ChannelHeaderSubtitle');
17+
const { typing = {} } = useTypingContext('ChannelHeaderSubtitle');
18+
const onlineStatusText = useChannelHeaderOnlineStatus();
19+
const typingInChannel = Object.values(typing).filter(
20+
({ parent_id, user }) => user?.id !== client.user?.id && !parent_id,
21+
);
22+
const hasTyping = channelConfig?.typing_events !== false && typingInChannel.length > 0;
23+
24+
if (!hasTyping && !onlineStatusText) return null;
25+
26+
return (
27+
<div className='str-chat__channel-header__data__subtitle'>
28+
<span
29+
className='str-chat__subtitle-content-transition'
30+
key={hasTyping ? 'typing' : 'default'}
31+
>
32+
{hasTyping ? <TypingIndicatorHeader /> : onlineStatusText}
33+
</span>
34+
</div>
35+
);
36+
};
37+
1238
export type ChannelHeaderProps = {
1339
/** 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) */
1440
Avatar?: React.ComponentType<ChannelAvatarProps>;
@@ -38,7 +64,6 @@ export const ChannelHeader = (props: ChannelHeaderProps) => {
3864
overrideImage,
3965
overrideTitle,
4066
});
41-
const onlineStatusText = useChannelHeaderOnlineStatus();
4267

4368
return (
4469
<div
@@ -51,11 +76,7 @@ export const ChannelHeader = (props: ChannelHeaderProps) => {
5176
</ToggleSidebarButton>
5277
<div className='str-chat__channel-header__data'>
5378
<div className='str-chat__channel-header__data__title'>{displayTitle}</div>
54-
{onlineStatusText != null && (
55-
<div className='str-chat__channel-header__data__subtitle'>
56-
{onlineStatusText}
57-
</div>
58-
)}
79+
<ChannelHeaderSubtitle />
5980
</div>
6081
<Avatar
6182
className='str-chat__avatar--channel-header'

src/components/MessageList/MessageList.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,11 @@ const MessageListWithContext = (props: MessageListWithContextProps) => {
283283
<MessageListWrapper className='str-chat__ul'>
284284
{elements}
285285
</MessageListWrapper>
286-
<TypingIndicator threadList={threadList} />
286+
<TypingIndicator
287+
isMessageListScrolledToBottom={isMessageListScrolledToBottom}
288+
scrollToBottom={scrollToBottom}
289+
threadList={threadList}
290+
/>
287291

288292
<div key='bottom' />
289293
</InfiniteScroll>

src/components/MessageList/VirtualizedMessageList.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,16 @@ const VirtualizedMessageListWithContext = (
515515
EmptyPlaceholder,
516516
Header,
517517
Item,
518+
...(TypingIndicator && {
519+
Footer: () =>
520+
isMessageListScrolledToBottom ? (
521+
<TypingIndicator
522+
isMessageListScrolledToBottom={isMessageListScrolledToBottom}
523+
scrollToBottom={scrollToBottom}
524+
threadList={threadList}
525+
/>
526+
) : null,
527+
}),
518528
...virtuosoComponentsFromProps,
519529
}}
520530
computeItemKey={computeItemKey}
@@ -584,7 +594,6 @@ const VirtualizedMessageListWithContext = (
584594
/>
585595
</div>
586596
</DialogManagerProvider>
587-
{TypingIndicator && <TypingIndicator />}
588597
</MessageListMainPanel>
589598
<MessageListNotifications notifications={notifications} />
590599
{giphyPreviewMessage && <GiphyPreviewMessage message={giphyPreviewMessage} />}

0 commit comments

Comments
 (0)