Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 24 additions & 19 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
ReactionOptions,
mapEmojiMartData,
useStateStore,
TypingIndicator,
} from 'stream-chat-react';
import { createTextComposerEmojiMiddleware, EmojiPicker } from 'stream-chat-react/emojis';
import { init, SearchIndex } from 'emoji-mart';
Expand Down Expand Up @@ -318,31 +319,35 @@ const App = () => {
sort={sort}
showChannelSearch
/>
<Channel>
<WithDragAndDropUpload>
<Window>
<ChannelHeader Avatar={ChannelAvatar} />
<MessageList returnAllReadData />
<AIStateIndicator />
<MessageInput
focus
audioRecordingEnabled
maxRows={10}
asyncMessagesMultiSendEnabled
/>
</Window>
</WithDragAndDropUpload>
<WithDragAndDropUpload className='str-chat__dropzone-root--thread'>
<Thread virtualized />
</WithDragAndDropUpload>
</Channel>
<WithComponents overrides={{ TypingIndicator }}>
<Channel>
<WithDragAndDropUpload>
<Window>
<ChannelHeader Avatar={ChannelAvatar} />
<MessageList returnAllReadData />
<AIStateIndicator />
<MessageInput
focus
audioRecordingEnabled
maxRows={10}
asyncMessagesMultiSendEnabled
/>
</Window>
</WithDragAndDropUpload>
<WithDragAndDropUpload className='str-chat__dropzone-root--thread'>
<Thread virtualized />
</WithDragAndDropUpload>
</Channel>
</WithComponents>
</ChatView.Channels>
<ChatView.Threads>
<ThreadStateSync />
<ThreadList />
<ChatView.ThreadAdapter>
<WithDragAndDropUpload className='str-chat__dropzone-root--thread'>
<Thread virtualized />
<WithComponents overrides={{ TypingIndicator }}>
<Thread virtualized />
</WithComponents>
</WithDragAndDropUpload>
</ChatView.ThreadAdapter>
</ChatView.Threads>
Expand Down
2 changes: 1 addition & 1 deletion examples/vite/src/stream-imports-layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
//@use 'stream-chat-react/dist/scss/v2/Thread/Thread-layout';
//@use 'stream-chat-react/dist/scss/v2/Search/Search-layout';
@use 'stream-chat-react/dist/scss/v2/Tooltip/Tooltip-layout';
@use 'stream-chat-react/dist/scss/v2/TypingIndicator/TypingIndicator-layout';
//@use 'stream-chat-react/dist/scss/v2/TypingIndicator/TypingIndicator-layout';
// @use 'stream-chat-react/dist/scss/v2/ThreadList/ThreadList-layout';
//@use 'stream-chat-react/dist/scss/v2/ChatView/ChatView-layout';
//@use 'stream-chat-react/dist/scss/v2/UnreadCountBadge/UnreadCountBadge-layout';
Expand Down
2 changes: 1 addition & 1 deletion examples/vite/src/stream-imports-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
//@use 'stream-chat-react/dist/scss/v2/Thread/Thread-theme';
//@use 'stream-chat-react/dist/scss/v2/Search/Search-theme';
@use 'stream-chat-react/dist/scss/v2/Tooltip/Tooltip-theme';
@use 'stream-chat-react/dist/scss/v2/TypingIndicator/TypingIndicator-theme';
//@use 'stream-chat-react/dist/scss/v2/TypingIndicator/TypingIndicator-theme';
// @use 'stream-chat-react/dist/scss/v2/ThreadList/ThreadList-theme';
//@use 'stream-chat-react/dist/scss/v2/ChatView/ChatView-theme';
//@use 'stream-chat-react/dist/scss/v2/UnreadCountBadge/UnreadCountBadge-theme';
Expand Down
13 changes: 11 additions & 2 deletions src/components/Avatar/AvatarStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { type ComponentProps, type ElementType } from 'react';
import { useComponentContext } from '../../context';
import { type AvatarProps, Avatar as DefaultAvatar } from './Avatar';
import clsx from 'clsx';
import { Badge, type BadgeSize } from '../Badge';

export function AvatarStack({
badgeSize,
component: Component = 'div',
displayInfo = [],
overflowCount,
Expand All @@ -12,7 +14,8 @@ export function AvatarStack({
component?: ElementType;
displayInfo?: (Pick<AvatarProps, 'imageUrl' | 'userName'> & { id?: string })[];
overflowCount?: number;
size: 'sm' | 'xs' | null;
size: 'md' | 'sm' | 'xs' | null;
badgeSize?: BadgeSize;
}) {
const { Avatar = DefaultAvatar } = useComponentContext(AvatarStack.name);

Expand All @@ -35,7 +38,13 @@ export function AvatarStack({
/>
))}
{typeof overflowCount === 'number' && overflowCount > 0 && (
<div className='str-chat__avatar-stack__count-badge'>{overflowCount}</div>
<Badge
className='str-chat__avatar-stack__count-badge'
size={badgeSize ?? size}
variant='counter'
>
+{overflowCount}
</Badge>
)}
</Component>
);
Expand Down
41 changes: 12 additions & 29 deletions src/components/Avatar/styling/AvatarStack.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,27 @@
display: flex;
align-items: center;

& > .str-chat__avatar:not(:first-child),
.str-chat__avatar-stack__count-badge {
margin-left: calc(var(--spacing-xs) * -1);
position: relative;
}

&.str-chat__avatar-stack--size-sm {
--avatar-stack-count-badge-size: 24px;
// FIXME?: should be sm but it looks way too big
--avatar-stack-count-badge-font-size: var(--typography-font-size-xs);

&.str-chat__avatar-stack--size-xs {
& > .str-chat__avatar:not(:first-child),
.str-chat__avatar-stack__count-badge {
padding-inline: var(--spacing-xs);
margin-left: calc(var(--spacing-xs) * -1);
}
}

&.str-chat__avatar-stack--size-xs {
--avatar-stack-count-badge-size: 20px;
--avatar-stack-count-badge-font-size: var(--typography-font-size-xxs);

&.str-chat__avatar-stack--size-sm {
& > .str-chat__avatar:not(:first-child),
.str-chat__avatar-stack__count-badge {
padding-inline: var(--spacing-xxs);
margin-left: calc(var(--spacing-sm) * -1);
}
}

.str-chat__avatar-stack__count-badge {
font-size: var(--avatar-stack-count-badge-font-size);
font-weight: var(--typography-font-weight-bold);
display: flex;
justify-content: center;
align-items: center;
height: var(--avatar-stack-count-badge-size);
min-width: var(--avatar-stack-count-badge-size);
min-height: var(--avatar-stack-count-badge-size);
border-radius: var(--radius-max);
border: 1px solid var(--border-core-subtle);
background: var(--badge-bg-default);
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14);
line-height: 1;
position: relative;
&.str-chat__avatar-stack--size-md {
& > .str-chat__avatar:not(:first-child),
.str-chat__avatar-stack__count-badge {
margin-left: calc(var(--spacing-sm) * -1);
}
}
}
18 changes: 12 additions & 6 deletions src/components/Badge/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import clsx from 'clsx';
import React, { type ComponentProps } from 'react';

export type BadgeVariant = 'default' | 'primary' | 'error' | 'neutral' | 'inverse';
export type BadgeVariant =
| 'default'
| 'primary'
| 'error'
| 'neutral'
| 'counter'
| 'inverse';

export type BadgeSize = 'sm' | 'md' | 'lg';
export type BadgeSize = 'xs' | 'sm' | 'md' | 'lg' | null;

export type BadgeProps = ComponentProps<'span'> & {
export type BadgeProps = ComponentProps<'div'> & {
/** Visual variant mapping to design tokens */
variant?: BadgeVariant;
/** Size preset (typography and padding) */
Expand All @@ -23,15 +29,15 @@ export const Badge = ({
variant = 'default',
...spanProps
}: BadgeProps) => (
<span
<div
{...spanProps}
className={clsx(
'str-chat__badge',
`str-chat__badge--variant-${variant}`,
`str-chat__badge--size-${size}`,
{ [`str-chat__badge--size-${size}`]: size },
className,
)}
>
{children}
</span>
</div>
);
36 changes: 35 additions & 1 deletion src/components/Badge/styling/Badge.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Figma: Badge Notification (scroll-to-bottom unread count)

.str-chat__badge {
display: inline-flex;
display: flex;
align-items: center;
justify-content: center;
font-weight: var(--typography-font-weight-bold);
Expand Down Expand Up @@ -69,3 +69,37 @@
border-width: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14);
}

.str-chat__badge--variant-counter {
border-radius: var(--radius-max);
border: 1px solid var(--border-core-subtle);
background: var(--badge-bg-default);
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14);
font: var(--str-chat__numeric-xl-text);

&.str-chat__badge--size-xs {
min-width: 20px;
min-height: 20px;
padding-inline: var(--spacing-xxs);
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.14);
font: var(--str-chat__numeric-md-text);
}

&.str-chat__badge--size-sm {
min-width: 24px;
min-height: 24px;
padding-inline: var(--spacing-xs);
}

&.str-chat__badge--size-md {
min-width: 32px;
min-height: 32px;
padding-inline: var(--spacing-xs);
}

&.str-chat__badge--size-lg {
min-width: 40px;
min-height: 40px;
padding-inline: var(--spacing-sm);
}
}
33 changes: 27 additions & 6 deletions src/components/ChannelHeader/ChannelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,39 @@ import React from 'react';

import { IconLayoutAlignLeft } from '../Icons/icons';
import { type ChannelAvatarProps, ChannelAvatar as DefaultAvatar } from '../Avatar';
import { TypingIndicatorHeader } from '../TypingIndicator/TypingIndicatorHeader';
import { useChannelHeaderOnlineStatus } from './hooks/useChannelHeaderOnlineStatus';
import { useChannelPreviewInfo } from '../ChannelPreview/hooks/useChannelPreviewInfo';
import { useChannelStateContext } from '../../context/ChannelStateContext';
import { useChatContext } from '../../context/ChatContext';
import { useTypingContext } from '../../context/TypingContext';
import clsx from 'clsx';
import { ToggleSidebarButton } from '../Button/ToggleSidebarButton';

const ChannelHeaderSubtitle = () => {
const { channelConfig } = useChannelStateContext('ChannelHeaderSubtitle');
const { client } = useChatContext('ChannelHeaderSubtitle');
const { typing = {} } = useTypingContext('ChannelHeaderSubtitle');
const onlineStatusText = useChannelHeaderOnlineStatus();
const typingInChannel = Object.values(typing).filter(
({ parent_id, user }) => user?.id !== client.user?.id && !parent_id,
);
const hasTyping = channelConfig?.typing_events !== false && typingInChannel.length > 0;

if (!hasTyping && !onlineStatusText) return null;

return (
<div className='str-chat__channel-header__data__subtitle'>
<span
className='str-chat__subtitle-content-transition'
key={hasTyping ? 'typing' : 'default'}
>
{hasTyping ? <TypingIndicatorHeader /> : onlineStatusText}
</span>
</div>
);
};

export type ChannelHeaderProps = {
/** 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>;
Expand Down Expand Up @@ -38,7 +64,6 @@ export const ChannelHeader = (props: ChannelHeaderProps) => {
overrideImage,
overrideTitle,
});
const onlineStatusText = useChannelHeaderOnlineStatus();

return (
<div
Expand All @@ -51,11 +76,7 @@ export const ChannelHeader = (props: ChannelHeaderProps) => {
</ToggleSidebarButton>
<div className='str-chat__channel-header__data'>
<div className='str-chat__channel-header__data__title'>{displayTitle}</div>
{onlineStatusText != null && (
<div className='str-chat__channel-header__data__subtitle'>
{onlineStatusText}
</div>
)}
<ChannelHeaderSubtitle />
</div>
<Avatar
className='str-chat__avatar--channel-header'
Expand Down
6 changes: 5 additions & 1 deletion src/components/MessageList/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,11 @@ const MessageListWithContext = (props: MessageListWithContextProps) => {
<MessageListWrapper className='str-chat__ul'>
{elements}
</MessageListWrapper>
<TypingIndicator threadList={threadList} />
<TypingIndicator
isMessageListScrolledToBottom={isMessageListScrolledToBottom}
scrollToBottom={scrollToBottom}
threadList={threadList}
/>

<div key='bottom' />
</InfiniteScroll>
Expand Down
11 changes: 10 additions & 1 deletion src/components/MessageList/VirtualizedMessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,16 @@ const VirtualizedMessageListWithContext = (
EmptyPlaceholder,
Header,
Item,
...(TypingIndicator && {
Footer: () =>
isMessageListScrolledToBottom ? (
<TypingIndicator
isMessageListScrolledToBottom={isMessageListScrolledToBottom}
scrollToBottom={scrollToBottom}
threadList={threadList}
/>
) : null,
}),
...virtuosoComponentsFromProps,
}}
computeItemKey={computeItemKey}
Expand Down Expand Up @@ -584,7 +594,6 @@ const VirtualizedMessageListWithContext = (
/>
</div>
</DialogManagerProvider>
{TypingIndicator && <TypingIndicator />}
</MessageListMainPanel>
<MessageListNotifications notifications={notifications} />
{giphyPreviewMessage && <GiphyPreviewMessage message={giphyPreviewMessage} />}
Expand Down
13 changes: 13 additions & 0 deletions src/components/MessageList/styling/MessageList.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@
}
}

.str-chat__thread {
.str-chat__message-list .str-chat__message-list-scroll {
padding-inline: 0;
/* Max container 800px, 16px padding → 768px readable content; matches composer width + padding */
max-width: var(--str-chat__message-list-scroll-max-width);
.str-chat__ul {
list-style: none;
padding: 0;
margin: 0;
}
}
}

.str-chat__main-panel {
.str-chat__ul {
.str-chat__li:first-of-type {
Expand Down
Loading
Loading