Repository navigation
fix!: remove dead v15 surface and restore v14 behaviors found by the API audit #3328
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
86de0ba
e8dde4b
7f9239d
8cc2de9
8034671
ccffeba
d4edc38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import clsx from 'clsx'; | ||
| import React from 'react'; | ||
| import type { SearchControllerState } from 'stream-chat'; | ||
|
|
||
|
|
@@ -6,6 +7,12 @@ import { ChannelLists } from './ChannelLists'; | |
| import { NotificationList as DefaultNotificationList } from '../Notifications'; | ||
| import { Search as DefaultSearch } from '../Search'; | ||
| import { useChatContext, useComponentContext } from '../../context'; | ||
| import { | ||
| ThemeScopeProvider, | ||
| useIsInThemeScope, | ||
| useThemeScopeClassName, | ||
| } from '../../context/ThemeScopeContext'; | ||
| import { useImageFlagEmojisOnWindowsClass } from '../Channel/hooks/useChannelContainerClasses'; | ||
| import { useStateStore } from '../../store'; | ||
|
|
||
| const searchControllerStateSelector = (state: SearchControllerState) => ({ | ||
|
|
@@ -18,25 +25,41 @@ const searchControllerStateSelector = (state: SearchControllerState) => ({ | |
| * always; results while active), then the paginator-driven `ChannelLists` only while | ||
| * search is inactive. Mapped to the channel-navigation slot; `Search` is customizable | ||
| * via the `ComponentContext` `Search` slot. | ||
| * | ||
| * Outside `ChatView` its root carries the `str-chat <theme>` scope itself, so the design tokens | ||
| * apply; inside one it inherits `ChatView`'s instead of resetting them with a nested scope. | ||
| * `customClasses.chat` / `customClasses.channelList` replace the `str-chat` / | ||
| * `str-chat__channel-list` classes. | ||
| */ | ||
| export const ChannelNavigation = () => { | ||
| const { | ||
| ChannelListHeader = DefaultChannelListHeader, | ||
| NotificationList = DefaultNotificationList, | ||
| Search = DefaultSearch, | ||
| } = useComponentContext(); | ||
| const { searchController } = useChatContext(); | ||
| const { customClasses, searchController } = useChatContext(); | ||
| const isInThemeScope = useIsInThemeScope(); | ||
| const themeScopeClassName = useThemeScopeClassName(); | ||
| const windowsEmojiClass = useImageFlagEmojisOnWindowsClass(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# Inspect whether Chat renders its children during SSR and where the flag is used.
ast-grep outline src/components/Chat/Chat.tsx --match Chat --view expanded
sed -n '110,210p' src/components/Chat/Chat.tsx
sed -n '1,35p' src/components/Channel/hooks/useChannelContainerClasses.ts
rg -n -C 3 'useImageFlagEmojisOnWindowsClass\(' srcRepository: GetStream/stream-chat-react Length of output: 5677 🏁 Script executed: rg -n -F --glob '*.{ts,tsx,md,mdx}' -e 'useImageFlagEmojisOnWindows' -e 'SSR' -e 'server-side' -e 'server rendering' -e 'renderTo' src docs README.md 2>/dev/null || test "$?" -eq 1
rg -n -F --glob '*.ts' -e 'useCreateChatContext' -e 'useImageFlagEmojisOnWindows' src/context src 2>/dev/null || test "$?" -eq 1
sed -n '1,180p' src/context/ChatContext.tsx
sed -n '1,180p' src/components/Chat/hooks/useCreateChatContext.tsRepository: GetStream/stream-chat-react Length of output: 8985 Guard the Windows emoji check during server rendering. When 🐛 Suggested fix- return useImageFlagEmojisOnWindows && navigator.userAgent.match(/Win/)
+ return (
+ useImageFlagEmojisOnWindows &&
+ typeof navigator !== 'undefined' &&
+ navigator.userAgent.match(/Win/)
+ )🤖 Prompt for AI Agents |
||
| const { isActive } = useStateStore( | ||
| searchController.state, | ||
| searchControllerStateSelector, | ||
| ); | ||
|
|
||
| return ( | ||
| <div className='str-chat__channel-list'> | ||
| <ChannelListHeader /> | ||
| <Search /> | ||
| {!isActive && <ChannelLists />} | ||
| <NotificationList panel='channel-list' /> | ||
| <div | ||
| className={clsx( | ||
| !isInThemeScope && themeScopeClassName, | ||
| customClasses?.channelList ?? 'str-chat__channel-list', | ||
| windowsEmojiClass, | ||
| )} | ||
| > | ||
| <ThemeScopeProvider> | ||
| <ChannelListHeader /> | ||
| <Search /> | ||
| {!isActive && <ChannelLists />} | ||
| <NotificationList panel='channel-list' /> | ||
| </ThemeScopeProvider> | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import React from 'react'; | ||
| import { cleanup, render } from '@testing-library/react'; | ||
| import { afterEach, describe, expect, it, vi } from 'vitest'; | ||
| import type { StreamChat } from 'stream-chat'; | ||
|
|
||
| import { Chat } from '../../Chat'; | ||
| import type { ChatProps } from '../../Chat'; | ||
| import { ChannelNavigation } from '../ChannelNavigation'; | ||
| import { ThemeScopeProvider } from '../../../context/ThemeScopeContext'; | ||
| import { getTestClientWithUser } from '../../../mock-builders'; | ||
| import { axe } from '../../../../axe-helper'; | ||
|
|
||
| const setupClient = async () => { | ||
| const client = await getTestClientWithUser({ id: 'user_x' }); | ||
| // no paginator is registered, so nothing should query | ||
| vi.spyOn(client, 'queryChannelsAndHydrate').mockRejectedValue( | ||
| new Error('unexpected channel query'), | ||
| ); | ||
| return client; | ||
| }; | ||
|
|
||
| const renderNavigation = ( | ||
| client: StreamChat, | ||
| chatProps: Partial<Omit<ChatProps, 'client'>> = {}, | ||
| ) => | ||
| render( | ||
| <Chat client={client} {...chatProps}> | ||
| <ChannelNavigation /> | ||
| </Chat>, | ||
| ); | ||
|
|
||
| describe('ChannelNavigation', () => { | ||
| afterEach(cleanup); | ||
|
|
||
| it('carries the str-chat theme scope on its root outside ChatView', async () => { | ||
| const client = await setupClient(); | ||
|
|
||
| const { container } = renderNavigation(client, { theme: 'str-chat__theme-dark' }); | ||
|
|
||
| const root = container.firstElementChild; | ||
| expect(root).toHaveClass( | ||
| 'str-chat', | ||
| 'str-chat__theme-dark', | ||
| 'str-chat__channel-list', | ||
| ); | ||
| }); | ||
|
|
||
| it('applies customClasses.chat and customClasses.channelList to its root', async () => { | ||
| const client = await setupClient(); | ||
|
|
||
| const { container } = renderNavigation(client, { | ||
| customClasses: { channelList: 'custom-channel-list', chat: 'custom-chat' }, | ||
| }); | ||
|
|
||
| const root = container.firstElementChild; | ||
| expect(root).toHaveClass('custom-chat', 'custom-channel-list'); | ||
| expect(root).not.toHaveClass('str-chat'); | ||
| expect(root).not.toHaveClass('str-chat__channel-list'); | ||
| }); | ||
|
|
||
| it('inherits an enclosing theme scope (e.g. ChatView) instead of nesting one', async () => { | ||
| // `.str-chat` declares the whole token set: a nested one would reset tokens an app overrides | ||
| // on the ChatView root, and pick up app CSS written for `.str-chat` roots. | ||
| const client = await setupClient(); | ||
|
|
||
| const { container } = render( | ||
| <Chat client={client} customClasses={{ channelList: 'custom-channel-list' }}> | ||
| <ThemeScopeProvider> | ||
| <ChannelNavigation /> | ||
| </ThemeScopeProvider> | ||
| </Chat>, | ||
| ); | ||
|
|
||
| const root = container.firstElementChild; | ||
| expect(root).toHaveClass('custom-channel-list'); | ||
| expect(root).not.toHaveClass('str-chat'); | ||
| expect(container.querySelector('.str-chat')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('has no axe violations', async () => { | ||
| const client = await setupClient(); | ||
|
|
||
| const { container } = renderNavigation(client); | ||
|
|
||
| expect(await axe(container)).toHaveNoViolations(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use the selected item message for metadata fallbacks.
If
item.messagebelongs to a different message thanMessageContext.message, an item withoutcreatedAtoruserreceives the surrounding message's timestamp and sender. The timestamp override then receivesitem.message, so the header can show conflicting metadata. Derive both fallbacks fromitem.message ?? message, while preserving explicititem.createdAtanditem.uservalues.🤖 Prompt for AI Agents