feat: add ChannelDetail component#3221
Conversation
# Conflicts: # src/components/Modal/GlobalModal.tsx
…ing destructive action
# Conflicts: # src/components/Modal/GlobalModal.tsx
📝 WalkthroughWalkthroughThe PR adds a new ChannelDetail plugin with section-based navigation, member/files/media/pinned-message subviews, and example-app settings integration. It also updates shared dialog, modal, notification, icon, form, and styling primitives, plus locale strings and build exports. ChangesShared UI primitives and hooks
Sequence Diagram(s)sequenceDiagram
participant AvatarWithChannelDetail
participant GlobalModal
participant ChannelDetail
participant SectionNavigator
AvatarWithChannelDetail->>GlobalModal: open modal
GlobalModal->>ChannelDetail: render channel detail content
ChannelDetail->>SectionNavigator: provide sections and layout
SectionNavigator->>ChannelDetail: onLayoutChange
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/Dialog/service/DialogPortal.tsx (1)
25-69:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
captureOutsideClicksis cosmetic right now.Line 26 adds a behavioral prop, but Lines 36-69 still register the global outside-click handler unconditionally. That means dialogs close on clicks outside the overlay even when
captureOutsideClicksis falsy, soDialogManagerProvider.portalDestinationPropscannot actually disable that behavior.Suggested direction
useEffect(() => { - if (!destinationRoot || !dialogManager) return; + if (!destinationRoot || !dialogManager || !captureOutsideClicks) return; const handleDocumentClick = (event: MouseEvent) => { const target = event.target as Node; const clickedOverlay = (target as HTMLElement).closest?.( '[data-str-chat__portal-id]', @@ - }, [destinationRoot, dialogManager]); + }, [captureOutsideClicks, destinationRoot, dialogManager]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Dialog/service/DialogPortal.tsx` around lines 25 - 69, The click-to-close behavior is registered unconditionally in DialogPortalDestination; make it respect the captureOutsideClicks prop by only attaching the document 'click' listener when captureOutsideClicks is truthy (and dialogManager/destinationRoot exist and openedDialogCount > 0), and remove it otherwise; update the useEffect to include captureOutsideClicks in its dependency array and gate the listener attachment around captureOutsideClicks, keeping the existing handler (handleDocumentClick) and removal logic unchanged.
🟡 Minor comments (26)
src/i18n/ja.json-68-68 (1)
68-68:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUntranslated strings in Japanese translation file.
Several translation keys contain English text instead of Japanese translations:
- Line 68: "Are you sure you want to leave this channel?"
- Line 185: "Chat deleted"
- Line 237: "Error adding members"
- Line 240: "Error deleting chat"
- Line 421: "Open members actions"
- Line 528: "This permanently deletes your message history with {{ user }}. This can't be undone."
- Line 530: "This user won't be able to message you anymore. You can unblock them anytime."
As per coding guidelines, all translation values must be non-empty strings in the target language, with zero-tolerance validation via
yarn validate-translations.Also applies to: 185-185, 237-237, 240-240, 421-421, 528-528, 530-530
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/ja.json` at line 68, The Japanese translation file has several untranslated values (keys like "Are you sure you want to leave this channel?", "Chat deleted", "Error adding members", "Error deleting chat", "Open members actions", "This permanently deletes your message history with {{ user }}. This can't be undone.", and "This user won't be able to message you anymore. You can unblock them anytime.")—replace each English value with the correct Japanese translation (ensuring placeholders like {{ user }} remain intact), save the updated src/i18n/ja.json, and run the zero-tolerance validator (yarn validate-translations) to confirm there are no empty or English strings remaining before committing.Source: Coding guidelines
src/i18n/pt.json-83-83 (1)
83-83:⚠️ Potential issue | 🟡 MinorFix untranslated English strings in
src/i18n/pt.json
src/i18n/pt.jsonstill contains English values (the value matches the English key) for:
- Line 83:
"Are you sure you want to leave this channel?"- Line 200:
"Chat deleted"- Line 252:
"Error adding members"- Line 255:
"Error deleting chat"- Line 442:
"Open members actions"- Line 554:
"This permanently deletes your message history with {{ user }}. This can't be undone."- Line 556:
"This user won't be able to message you anymore. You can unblock them anytime.""Are you sure you want to leave this channel?": "Are you sure you want to leave this channel?",Replace these values with proper Portuguese translations and re-run
yarn validate-translationsto catch any remaining issues.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/pt.json` at line 83, Update the untranslated English values in src/i18n/pt.json by replacing the listed keys' values with proper Portuguese translations: "Are you sure you want to leave this channel?", "Chat deleted", "Error adding members", "Error deleting chat", "Open members actions", "This permanently deletes your message history with {{ user }}. This can't be undone.", and "This user won't be able to message you anymore. You can unblock them anytime."; edit the corresponding entries so the keys remain identical but the values are translated to Portuguese, save the file and re-run yarn validate-translations to verify there are no remaining translation errors.Source: Coding guidelines
src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts-43-43 (1)
43-43:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse pluralized i18n keys for the member count label.
'{{ memberCount }} members'will render ungrammatical text for singular counts and won't pluralize correctly across locales. Switch this to the repo's_one/_othertranslation pattern instead of a fixed plural string. As per coding guidelines, "Use Plurals in translations with_one/_othersuffixes".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts` at line 43, The current return in useChannelHeaderOnlineStatus uses a fixed plural string ("{{ memberCount }} members") which won't pluralize for singular or other locales; replace it with the repo's pluralized i18n keys using the _one/_other suffix and pass the count (memberCount) so the i18n system chooses the correct form (e.g., use the members_one/members_other key pattern or channel.members_one/channel.members_other per our conventions) while keeping the watcherCount part unchanged; update the t(...) call in useChannelHeaderOnlineStatus to use the count-aware plural key and the memberCount variable so singular/plural translations work correctly.Source: Coding guidelines
src/components/ChannelDetail/Views/ChannelFilesView/__tests__/ChannelFilesView.test.tsx-170-183 (1)
170-183:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate test expectation after fixing
allowEmptySearchString.This test expects
allowEmptySearchString: true(line 174), which corresponds to the invalid property flagged inuseChannelFilesSearch.ts. Once the hook is corrected to use the valid stream-chat API, update this assertion to match the corrected configuration.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/Views/ChannelFilesView/__tests__/ChannelFilesView.test.tsx` around lines 170 - 183, The test currently asserts an invalid option name (allowEmptySearchString) — update the assertion in ChannelFilesView.test.tsx to match the corrected option used in the hook (useChannelFilesSearch) / MessageSearchSource configuration; replace the allowEmptySearchString: true expectation with the valid property name used in your fix (e.g., allowEmptySearchQuery: true) or otherwise assert the actual option that useChannelFilesSearch sets on mocks.searchSourceOptions[0], and remove the incorrect allowEmptySearchString check so the test reflects the hook's corrected configuration.src/components/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx-216-231 (1)
216-231:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winClear
searchSourceActivatebefore asserting the whitespace-reset path.This assertion is already satisfied by the mount-time
activate()call, so the test would still pass if the empty-query branch stopped reactivating the source. Reset the relevant spies after the initial render/search step and then assert the second activation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx` around lines 216 - 231, The test currently asserts that the whitespace reset branch calls searchSourceActivate, but that activation also happens at mount, so clear the activation spy after the initial render/search step to ensure the second activation is the one being asserted; specifically after renderWithChannel(<PinnedMessagesView ...>) and after mocks.searchSourceSearch.mockClear(), call mocks.searchSourceActivate.mockClear() (or mockReset) before firing the whitespace change, then assert mocks.searchSourceActivate was called once along with searchSourceCancelScheduledQuery, searchSourceResetState, and that searchSourceSearch was not called.src/components/SectionNavigator/styling/SectionNavigator.scss-19-22 (1)
19-22:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix the stylelint failure in this block.
Stylelint is already flagging Line 21 with
declaration-empty-line-before, so this new stylesheet currently introduces a merge-blocking lint error.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/SectionNavigator/styling/SectionNavigator.scss` around lines 19 - 22, The stylelint error comes from missing an empty line before the declaration in the .str-chat__section-navigator__navigation block; update that block (involving `@include` utils.hide-scrollbar(y), overscroll-behavior, display) to satisfy declaration-empty-line-before by inserting a blank line before the overscroll-behavior declaration (or alternatively add a targeted stylelint disable comment before the overscroll-behavior line if an exception is needed).Source: Linters/SAST tools
src/i18n/nl.json-72-72 (1)
72-72:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSeveral locale additions were copied over in English.
src/i18n/nl.jsonandsrc/i18n/ko.jsonboth still contain English values for new Channel Detail strings, including destructive confirmations, error messages,"Open members actions", and the new pinned-message timestamplastDaytext. The shared root cause is incomplete localization for the new feature, and it will ship mixed-language UI in both Dutch and Korean.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/nl.json` at line 72, The Dutch and Korean locale files contain untranslated English strings for new Channel Detail keys (e.g., "Are you sure you want to leave this channel?", "Open members actions", and the pinned-message timestamp key "lastDay") and associated destructive confirmations and error messages; update the corresponding JSON values for those keys in both locale files with proper Dutch and Korean translations, ensure no English fallbacks remain for any new Channel Detail strings, and run the i18n validation/test to confirm translations load correctly.src/components/ChannelDetail/styling/ChannelManagementView.scss-103-113 (1)
103-113:⚠️ Potential issue | 🟡 MinorReplace deprecated
clipin the visually-hidden file input
The visually-hidden input still relies on the deprecatedclipproperty (.str-chat__channel-detail__channel-management-view__file-input); switch to aclip-path-based recipe.Suggested update
.str-chat__channel-detail__channel-management-view__file-input { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; - clip: rect(0, 0, 0, 0); + clip-path: inset(50%); white-space: nowrap; border: 0; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/styling/ChannelManagementView.scss` around lines 103 - 113, The .str-chat__channel-detail__channel-management-view__file-input rule uses the deprecated clip property; replace it with a clip-path-based visually-hidden recipe by removing the clip declaration and adding a clip-path (e.g., clip-path: inset(50%)); keep position:absolute, width:1px, height:1px, padding:0, margin:-1px, overflow:hidden, white-space:nowrap, border:0, and add -webkit-clip-path: inset(50%) for broader browser support so the input remains visually hidden but accessible (update the selector .str-chat__channel-detail__channel-management-view__file-input).Source: Linters/SAST tools
src/i18n/es.json-55-55 (1)
55-55:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFinish translating the new Spanish strings before merging.
Several new
esvalues are still English ("Actions","Are you sure you want to leave this channel?","Chat deleted","Error adding members","Open members actions", the permanent-delete copy, and"[Yesterday]"insidetimestamp/ChannelDetailPinnedMessageTimestamp). This will surface mixed-language UI throughout the new Channel Detail flows for Spanish users.Also applies to: 83-83, 200-200, 252-255, 442-442, 554-556, 566-566
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/es.json` at line 55, Several Spanish translations in es.json are still English; locate the keys with values "Actions", "Are you sure you want to leave this channel?", "Chat deleted", "Error adding members", "Open members actions", the permanent-delete confirmation copy, and the "[Yesterday]" value inside timestamp/ChannelDetailPinnedMessageTimestamp and replace them with proper Spanish translations (ensure plural/singular and formal/informal tone matches the app). Update the corresponding JSON entries for those keys and any other English strings in the same file ranges mentioned so the Channel Detail flows are fully localized, then run a quick JSON lint and a smoke-check in the UI to confirm no missing quotes or broken keys.src/components/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx-95-100 (1)
95-100:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove the leading
.from the destructive icon classes.
classNameshould contain raw class tokens, not CSS selectors. The current values create literal class names like.str-chat__icon--destructive, so any existing.str-chat__icon--destructivestyling will never match these icons.💡 Suggested fix
const BlockUserActionIcon = () => ( - <IconNoSign className='.str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--block-user' /> + <IconNoSign className='str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--block-user' /> ); const RemoveUserActionIcon = () => ( - <IconUserRemove className='.str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--remove-user' /> + <IconUserRemove className='str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--remove-user' /> );Based on learnings, icon selectors must use
str-chat__icon--<name>class tokens without selector syntax.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx` around lines 95 - 100, In BlockUserActionIcon and RemoveUserActionIcon update the IconNoSign and IconUserRemove className props to remove the leading '.' from the destructive class tokens so they use raw class names (e.g., "str-chat__icon--destructive" not ".str-chat__icon--destructive"); ensure both className strings for IconNoSign and IconUserRemove use space-separated class tokens without selector syntax.Source: Learnings
src/i18n/it.json-55-55 (1)
55-55:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winTranslate the remaining English fallbacks in the Italian locale.
Several new entries are still English here (
"Actions","Are you sure you want to leave this channel?","Chat deleted","Error adding members","Open members actions", the delete-history prompt, and"Yesterday"insidetimestamp/ChannelDetailPinnedMessageTimestamp). Those strings will leak into the Italian UI as-is.Also applies to: 83-83, 200-200, 252-255, 442-442, 554-566
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/it.json` at line 55, The Italian locale file (src/i18n/it.json) contains untranslated English fallbacks (e.g., the keys "Actions", "Are you sure you want to leave this channel?", "Chat deleted", "Error adding members", "Open members actions", the delete-history prompt, and "Yesterday" under timestamp/ChannelDetailPinnedMessageTimestamp); replace those English values with their correct Italian translations, ensuring you update the exact JSON keys mentioned and any other entries at the ranges called out (lines ~83, ~200, ~252-255, ~442, ~554-566) so no English strings remain in the Italian locale.src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx-42-56 (1)
42-56:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove the leading
.from these icon class names.The destructive modifier is never applied here because the rendered class becomes the literal token
.str-chat__icon--destructiveinstead ofstr-chat__icon--destructive.💡 Suggested fix
const BlockUserActionIcon = () => ( - <IconNoSign className='.str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--block-user' /> + <IconNoSign className='str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--block-user' /> ); const DeleteChatActionIcon = () => ( - <IconDelete className='.str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--delete-chat' /> + <IconDelete className='str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--delete-chat' /> ); const LeaveChannelActionIcon = () => ( - <IconLeave className='.str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--leave-channel' /> + <IconLeave className='str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--leave-channel' /> );Based on learnings:
createIcon-generated selectors in this repo use thestr-chat__icon--<name>class form, so the leading dot breaks the expected class token.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx` around lines 42 - 56, The icon classNames in BlockUserActionIcon, DeleteChatActionIcon, LeaveChannelActionIcon (rendering IconNoSign, IconDelete, IconLeave) incorrectly include a leading '.' which causes the literal token to be used; remove the leading '.' from those className strings so they become e.g. 'str-chat__icon--destructive str-chat__channel-detail__action-icon ...' (also verify MuteActionIcon and MutedActionIcon className forms remain unchanged) to restore proper CSS selector matching.Source: Learnings
src/components/Form/Checkbox.tsx-6-12 (1)
6-12:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPreserve caller-provided
className.
classNamefromCheckboxPropsis currently overwritten by the hard-coded classes, so consumers cannot attach styling or selectors to this new component.♻️ Proposed fix
-export const Checkbox = ({ checked, ...props }: CheckboxProps) => ( +export const Checkbox = ({ checked, className, ...props }: CheckboxProps) => ( <div {...props} - className={clsx('str-chat__checkmark str-chat__checkbox', { + className={clsx('str-chat__checkmark str-chat__checkbox', className, { 'str-chat__checkbox--checked': checked, 'str-chat__checkmark--checked': checked, })} /> );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Form/Checkbox.tsx` around lines 6 - 12, The Checkbox component currently overwrites any caller-provided className; update the props handling in Checkbox (and the clsx call) to preserve and merge the incoming className from CheckboxProps with the hard-coded classes (e.g., pass className through or extract it from props and include it in clsx) so consumers can add custom styling/selectors; ensure you still apply the conditional 'str-chat__checkbox--checked' and 'str-chat__checkmark--checked' classes based on the checked prop while merging the user className.src/components/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsx-198-245 (1)
198-245:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winTranslate the menu's accessibility label.
aria-label='Members actions'is the one user-facing string in this component that bypassest(), so screen readers will always announce it in English and it will never be extracted into the locale JSON files. As per coding guidelines, useuseTranslationContext()to access thet()function for translations and add all translatable strings tosrc/i18n/files.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsx` around lines 198 - 245, The aria-label on ContextMenuComponent is hardcoded as 'Members actions' and bypasses translation; import and call useTranslationContext() to get t() (e.g., const { t } = useTranslationContext()) and replace the literal aria-label on ContextMenuComponent with a translated string (e.g., t('Members actions') or the agreed i18n key), and add that key/translation to the i18n JSON; update ChannelMembersHeaderActions.defaults.tsx where ContextMenuComponent and dialog logic are defined to use t() for accessibility labels.Source: Coding guidelines
src/components/ChannelDetail/styling/ChannelDetail.scss-56-59 (1)
56-59:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAvoid capitalizing translated labels in CSS.
text-transform: capitalizerewrites whatever localized label gets rendered here, which breaks locale-specific casing rules and can mangle scripts that do not use title case. If these nav labels need capitalization, do it in the source strings instead of forcing it at paint time.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/styling/ChannelDetail.scss` around lines 56 - 59, Remove the forced text-transform from the CSS rule for .str-chat__channel-detail__nav-button so translated labels are not modified at render time; either delete the text-transform: capitalize declaration or set it to none, and if capitalization is required, perform that in the source/localized strings that feed the nav button rather than in ChannelDetail.scss.src/i18n/ru.json-589-589 (1)
589-589:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winLocalize the pinned-message “Yesterday” calendar label.
This formatter still hard-codes
[Yesterday]inside the Russian locale, so pinned-message timestamps will render an English day label even when the rest of the UI is Russian.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/ru.json` at line 589, The calendarFormats in the "timestamp/ChannelDetailPinnedMessageTimestamp" entry is hard-coded with "[Yesterday]" in English; update that lastDay value to a localized Russian label (e.g. "[Вчера]") or reference the appropriate i18n key so the pinned-message timestamp uses the locale string rather than the English literal; edit the "timestamp/ChannelDetailPinnedMessageTimestamp" value and replace the lastDay entry inside calendarFormats accordingly.src/i18n/ru.json-66-66 (1)
66-66:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winTranslate the new Russian locale entries before shipping.
Several newly added
ru.jsonvalues are still English ("Actions","Are you sure you want to leave this channel?","Chat deleted","Error adding members","Open members actions", and the new unblock/delete-history copy). That will surface mixed-language UI in the Russian locale.Also applies to: 95-95, 212-212, 264-267, 460-460, 576-578
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/ru.json` at line 66, Several new keys in ru.json are still in English and should be translated into Russian to avoid mixed-language UI; locate the entries for "Actions", "Are you sure you want to leave this channel?", "Chat deleted", "Error adding members", "Open members actions", and the new unblock/delete-history strings (also other occurrences flagged around the file) and replace the English values with accurate Russian translations that match tone and terminology used elsewhere in ru.json, keeping the JSON key names unchanged and preserving punctuation and capitalization.src/components/ChannelDetail/styling/ChannelFilesView.scss-62-63 (1)
62-63:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd empty line before declaration to satisfy stylelint.
Stylelint requires an empty line before the
displaydeclaration.🎨 Proposed fix
.str-chat__channel-detail__files-view__list { `@include` utils.hide-scrollbar; + display: flex;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/styling/ChannelFilesView.scss` around lines 62 - 63, Stylelint flags missing empty line before the display declaration in ChannelFilesView.scss; open the ChannelFilesView.scss file, locate the ruleset containing the `@include utils.hide-scrollbar;` followed immediately by `display: flex;` and insert a single blank line between the include and the `display` declaration so the file has an empty line before `display` (keep existing indentation and formatting).src/components/ChannelDetail/styling/ChannelMediaView.scss-62-63 (1)
62-63:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd empty line before declaration to satisfy stylelint.
Stylelint requires an empty line before the
displaydeclaration.🎨 Proposed fix
.str-chat__channel-detail__media-view__grid { `@include` utils.hide-scrollbar; + display: flex;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/styling/ChannelMediaView.scss` around lines 62 - 63, Add a blank line before the display declaration to satisfy stylelint: locate the block containing the `@include` utils.hide-scrollbar; line and insert one empty line before the subsequent display: flex; declaration so the file has an empty line between `@include` utils.hide-scrollbar; and display: flex;.src/components/Form/styling/Checkbox.scss-3-4 (1)
3-4:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd empty line before declaration to satisfy stylelint.
Stylelint requires an empty line before the
grid-columndeclaration.🎨 Proposed fix
.str-chat__checkbox, .str-chat__checkmark { $poll-checkmark-size: var(--str-chat__size-24); + grid-column: 1;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Form/styling/Checkbox.scss` around lines 3 - 4, Add a blank line before the grid-column declaration to satisfy stylelint: open the block containing $poll-checkmark-size and grid-column (look for $poll-checkmark-size: var(--str-chat__size-24); and the grid-column: 1; declaration) and insert an empty line between the two lines so grid-column is separated from the preceding $poll-checkmark-size declaration.src/components/Dialog/styling/Prompt.scss-3-9 (1)
3-9:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix the Stylelint violation in
.str-chat__prompt.This block is currently failing
declaration-empty-line-beforeon Line 5, which will break the repo's zero-warnings/lint-clean expectation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/Dialog/styling/Prompt.scss` around lines 3 - 9, The .str-chat__prompt rule violates declaration-empty-line-before; fix it by adding a single blank line immediately after the opening brace of the .str-chat__prompt block so there is an empty line before the first declaration (`@include` utils.modal), keeping the rest of the declarations intact; target the .str-chat__prompt selector in Prompt.scss to make this change.Sources: Coding guidelines, Linters/SAST tools
src/i18n/de.json-72-72 (1)
72-72:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSome new locale entries are still untranslated in
src/i18n/de.json,src/i18n/hi.json,src/i18n/fr.json, andsrc/i18n/tr.json.The same root cause shows up across all four locale files: several new Channel Detail strings were added with English values (for example the leave-channel confirmation,
"Chat deleted","Error adding members","Open members actions", parts of the destructive-history/unblock copy, and[Yesterday]inside the pinned-message timestamp formatter). That will ship a mixed-language UI in German, Hindi, French, and Turkish for the new feature surface.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/i18n/de.json` at line 72, Several new i18n keys were left untranslated (English values) and must be localized: update the JSON entries for keys like "Are you sure you want to leave this channel?", "Chat deleted", "Error adding members", "Open members actions", the destructive-history/unblock strings, and the pinned-message timestamp fragment "[Yesterday]" in the de/hi/fr/tr locale files; replace the English values with the correct translated strings for German, Hindi, French, and Turkish respectively, ensuring proper JSON quoting/escaping and preserving the existing key names (e.g., the string keys shown above) so the app uses the localized messages.src/components/ChannelDetail/styling/PinnedMessagesView.scss-17-19 (1)
17-19:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd empty line before
displaydeclaration to satisfy stylelint.Stylelint expects an empty line between the
@includedirective and the following property declaration.🎨 Proposed fix
.str-chat__channel-detail__pinned-messages-view__list { `@include` utils.hide-scrollbar; + display: flex;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/styling/PinnedMessagesView.scss` around lines 17 - 19, In .str-chat__channel-detail__pinned-messages-view__list, add a blank line between the `@include` utils.hide-scrollbar directive and the subsequent display: flex declaration to satisfy stylelint; locate the rule for .str-chat__channel-detail__pinned-messages-view__list, ensure there is exactly one empty line after the `@include` utils.hide-scrollbar line and before the display property so the linter passes.src/components/ChannelDetail/Views/ChannelMembersView/useChannelMembersSearch.ts-30-40 (1)
30-40:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse
channel.cidin dependency array, notchannel.The coding guideline requires using the stable
channel.cididentifier in dependency arrays instead of thechannelobject to prevent re-creating the search source when only the channel reference changes.♻️ Proposed fix
const membersSearchSource = useMemo(() => { const source = new ChannelMemberSearchSource(channel, { allowEmptySearchString: true, debounceMs: MEMBERS_SEARCH_DEBOUNCE_MS, pageSize: CHANNEL_MEMBERS_QUERY_LIMIT, resetOnNewSearchQuery: false, }); if (hasMembers) source.activate(); return source; - }, [channel, hasMembers]); + }, [channel.cid, hasMembers]);As per coding guidelines: "Do not include
channelin dependency arrays; usechannel.cidonly (stable identifier), notchannel.state".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/Views/ChannelMembersView/useChannelMembersSearch.ts` around lines 30 - 40, The useMemo that creates membersSearchSource currently depends on the unstable channel object; change the dependency to use the stable channel.cid instead (keep hasMembers), so update the useMemo dependency array to [channel.cid, hasMembers] to avoid recreating ChannelMemberSearchSource when only the channel reference changes; ensure you still reference channel when constructing new ChannelMemberSearchSource but only use channel.cid in the dependency list.Source: Coding guidelines
src/components/ChannelDetail/Views/ChannelMembersView/useChannelMembersSearch.ts-22-25 (1)
22-25:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse
channel.cidin dependency array, notchannel.The coding guideline requires using the stable
channel.cididentifier in dependency arrays instead of thechannelobject to avoid unnecessary re-creation when the channel reference changes.♻️ Proposed fix
const fallbackMembers = useMemo( () => Object.values(channel.state?.members ?? {}), - [channel], + [channel.cid], );As per coding guidelines: "Do not include
channelin dependency arrays; usechannel.cidonly (stable identifier), notchannel.state".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/Views/ChannelMembersView/useChannelMembersSearch.ts` around lines 22 - 25, The useMemo creating fallbackMembers currently depends on the entire channel object which can change reference; change its dependency array to use the stable channel.cid instead: keep the factory as () => Object.values(channel.state?.members ?? {}) but replace [channel] with [channel.cid] so fallbackMembers only recomputes when the channel identifier changes; update the dependency array for useMemo referencing fallbackMembers accordingly.Source: Coding guidelines
src/components/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.utils.ts-68-72 (1)
68-72:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard against undefined
message.idto prevent key collisions.While
MessageResponseandLocalMessageshould typically have anid, optimistic or draft messages might haveundefinedor missing IDs, leading to colliding keys like"undefined-0"and"undefined-1". Consider adding a fallback or filter:🛡️ Proposed fix
message.attachments?.forEach((attachment, index) => { if (!isChannelFileAttachment(attachment)) return; + if (!message.id) return; // Skip messages without stable IDs const item: ChannelFileItem = { attachment, createdAt, id: `${message.id}-${index}`, };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.utils.ts` around lines 68 - 72, When building ChannelFileItem in ChannelFilesView.utils.ts you must guard against a missing or undefined message.id to avoid colliding keys like "undefined-0"; update the code that creates the id (`id: `${message.id}-${index}``) to either skip messages lacking a stable id or generate a safe fallback (for example combine a deterministic fallback field such as message.clientId/message.timestamp/createdAt or attachment filename with the index) so the resulting id is always a unique non-"undefined" string; ensure any filtering or fallback logic is applied where ChannelFileItem objects are created so ChannelFileItem and its id field are never created with `"undefined-..."`.
🧹 Nitpick comments (7)
src/context/DialogManagerContext.tsx (1)
55-64: ⚡ Quick winDocument the new
portalDestinationPropspublic prop.
DialogManagerProvidernow exposes a new SDK prop, but the props block only documentscloseOnClickOutside. Please add inline docs here and update any affected public docs/examples so consumers know whatportalDestinationPropsdoes and which fields are supported.As per coding guidelines, "Ensure public API changes include documentation updates" and "When altering public API, update inline docs and any affected guide pages in the docs site".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/context/DialogManagerContext.tsx` around lines 55 - 64, Add inline documentation for the new public prop portalDestinationProps on the DialogManagerProviderProps type: describe that portalDestinationProps accepts a DialogPortalDestinationProps object used to control where manager dialogs are rendered (e.g., portal container element, mount id, or append behavior) and enumerate supported fields from DialogPortalDestinationProps with short descriptions and default behavior; update any public examples/docs that show DialogManagerProvider usage to include an example using portalDestinationProps and mention compatibility/defaults. Target symbols: DialogManagerProviderProps, portalDestinationProps, and DialogPortalDestinationProps.Source: Coding guidelines
src/components/ChannelDetail/ChannelDetailEmptyList.tsx (1)
5-5: ⚡ Quick winOverly-specific class name for a reusable component.
The component is named
ChannelDetailEmptyList(generic), but the class namestr-chat__channel-detail__channel-members-view__empty-stateis specific to the members view. If this component is reused across other channel detail views (files, media, pinned messages), the class name will be misleading and harder to maintain.Consider either:
- Renaming the class to
str-chat__channel-detail__empty-state(generic)- Or creating view-specific empty list components if styling differs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/ChannelDetailEmptyList.tsx` at line 5, The component ChannelDetailEmptyList uses an overly-specific CSS class 'str-chat__channel-detail__channel-members-view__empty-state' that ties it to the members view; update the component to use a generic class name like 'str-chat__channel-detail__empty-state' (or split into view-specific components if styles differ), and update any related CSS/SCSS selectors and other usages referencing the old class to the new generic name so styles continue to apply; ensure the class string in ChannelDetailEmptyList and any tests or consumers are changed consistently.src/components/ChannelDetail/__tests__/ChannelManagementView.test.tsx (1)
31-155: 🏗️ Heavy liftUse the repo's Chat/Channel test harness instead of ad-hoc context mocks.
This suite stubs most of the runtime contexts and renders only
ChannelDetailProvider, so it bypasses theChat/Channelwiring the SDK relies on in production. That makes the coverage less representative for mute/presence/update flows than the repository's standard test setup. As per coding guidelines, "Use mock-builders pattern for tests: getTestClientWithUser(), useMockedApis(), getOrCreateChannelApi(), set connectionId and wsPromise" and "Wrap component tests with<Chat>,<Channel>, and relevant sub-components; use channel.watch() after setup".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/__tests__/ChannelManagementView.test.tsx` around lines 31 - 155, The test currently bypasses SDK wiring by stubbing contexts and rendering ChannelManagementView inside ChannelDetailProvider (see ChannelManagementView and ChannelDetailProvider); replace the ad-hoc mocks with the repo test harness: create a test client using getTestClientWithUser(), call useMockedApis() and getOrCreateChannelApi() to set up the channel, set connectionId and wsPromise as needed, wrap the rendered component with <Chat> and <Channel> (instead of directly using ChannelDetailProvider), and call channel.watch() after setup so mute/presence/update flows exercise the real wiring used in production.Source: Coding guidelines
src/components/ChannelDetail/ChannelDetailListLoadingIndicator.tsx (2)
10-12: ⚡ Quick winRename type to reflect general channel detail usage.
The type
ChannelMembersViewListFooterPropssuggests it's specific to the members view, but this component is reused across files, media, and pinned messages views. Consider renaming toChannelDetailListLoadingIndicatorPropsto match the component name and reflect its broader usage.♻️ Suggested rename
-export type ChannelMembersViewListFooterProps<T> = { +export type ChannelDetailListLoadingIndicatorProps<T> = { searchSource: SearchSource<T>; }; export const ChannelDetailListLoadingIndicator = <T,>({ searchSource, -}: ChannelMembersViewListFooterProps<T>) => { +}: ChannelDetailListLoadingIndicatorProps<T>) => {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/ChannelDetailListLoadingIndicator.tsx` around lines 10 - 12, Rename the exported type ChannelMembersViewListFooterProps<T> to ChannelDetailListLoadingIndicatorProps<T> to reflect the component's broader usage (component: ChannelDetailListLoadingIndicator); update the type declaration name and all references/usages (imports, props annotations, generics) across files that consume this component so the exported generic SearchSource<T> prop remains named searchSource and the API stays identical.
24-28: ⚡ Quick winRemove redundant
isLoadingcheck.Line 26 checks
{isLoading && <LoadingIndicator />}, but this is redundant because line 22 already returnsnullwhen!isLoading. The conditional on line 26 will always evaluate to true at this point.♻️ Simplify the render
return ( <div className='str-chat__loading-indicator-placeholder'> - {isLoading && <LoadingIndicator />} + <LoadingIndicator /> </div> );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/ChannelDetailListLoadingIndicator.tsx` around lines 24 - 28, The render contains a redundant conditional: ChannelDetailListLoadingIndicator already returns null when !isLoading, so remove the inner {isLoading && <LoadingIndicator />} and render <LoadingIndicator /> directly inside the div; update the JSX in ChannelDetailListLoadingIndicator to replace the conditional expression with a plain <LoadingIndicator /> (keeping the surrounding div and className intact).src/components/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsx (1)
54-63: ⚡ Quick winUse one shared Channel-detail component-test harness across
src/components/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsxandsrc/components/ChannelDetail/Views/ChannelMemberDetailView/__tests__/ChannelMemberDetail.test.tsx.The shared root cause is the same in both files: each suite bypasses the repo’s required mock-builder +
<Chat>/<Channel>setup and mounts only a lighter provider tree. Reusing the standard Channel-detail test harness in both files will keep these tests aligned with the real provider contract and reduce false confidence from provider-light setups.As per coding guidelines, "Use mock-builders pattern for tests: getTestClientWithUser(), useMockedApis(), getOrCreateChannelApi(), set connectionId and wsPromise" and "Wrap component tests with , , and relevant sub-components; use channel.watch() after setup".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsx` around lines 54 - 63, The tests currently use createChannel and renderWithChannel with a lightweight ChannelDetailProvider; replace these with the repo's standard channel test harness: use getTestClientWithUser(), useMockedApis(), getOrCreateChannelApi() to build a mocked client and channel, set connectionId and wsPromise, call channel.watch() after setup, and render the component wrapped in <Chat> and <Channel> (instead of only ChannelDetailProvider) so the provider contract matches real usage; update both ChannelMembersHeaderActions.defaults.test.tsx and ChannelMemberDetail.test.tsx to call the shared harness rather than the local createChannel/renderWithChannel helpers.Source: Coding guidelines
src/components/ChannelDetail/styling/ChannelMembersView.scss (1)
23-31: 💤 Low valueAdd empty line before declaration (stylelint).
Stylelint flagged line 25 for missing an empty line before the
displaydeclaration.✨ Suggested fix
.str-chat__channel-detail__channel-members-view__list { `@include` utils.hide-scrollbar; + display: flex; flex-direction: column;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/ChannelDetail/styling/ChannelMembersView.scss` around lines 23 - 31, Stylelint reports a missing empty line before the display declaration inside the .str-chat__channel-detail__channel-members-view__list rule; open the SCSS rule for .str-chat__channel-detail__channel-members-view__list and insert a blank line before the "display: flex;" declaration so there is an empty line separating the preceding mixin/statement (`@include` utils.hide-scrollbar;) and the display property.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2ab854d9-cb41-4fcd-840f-0e75c9329bf3
📒 Files selected for processing (143)
examples/vite/src/AppSettings/ActionsMenu/NotificationPromptDialog.tsxexamples/vite/src/AppSettings/AppSettings.scssexamples/vite/src/AppSettings/AppSettings.tsxexamples/vite/src/AppSettings/state.tsexamples/vite/src/AppSettings/tabs/ChannelDetail/ChannelDetailTab.tsxexamples/vite/src/AppSettings/tabs/ChannelDetail/channelDetailSettings.tsexamples/vite/src/AppSettings/tabs/ChannelDetail/index.tsexamples/vite/src/AppSettings/tabs/General/GeneralTab.tsxexamples/vite/src/AppSettings/tabs/MessageActions/MessageActionsTab.tsxexamples/vite/src/AppSettings/tabs/Notifications/NotificationsTab.tsxexamples/vite/src/AppSettings/tabs/Reactions/ReactionsTab.tsxexamples/vite/src/AppSettings/tabs/SettingsTabLayoutComponents.tsxexamples/vite/src/AppSettings/tabs/Sidebar/SidebarTab.tsxexamples/vite/src/ChatLayout/ConfiguredChannelDetail.tsxexamples/vite/src/ChatLayout/Panels.tsxexamples/vite/src/SystemNotification/SystemNotification.tsxexamples/vite/src/icons.tsxsrc/components/AudioPlayback/components/index.tssrc/components/Avatar/AvatarWithChannelDetail.tsxsrc/components/Avatar/ChannelAvatar.tsxsrc/components/Avatar/__tests__/GroupAvatar.test.tsxsrc/components/Avatar/index.tssrc/components/Avatar/styling/AvatarWithChannelDetail.scsssrc/components/Avatar/styling/index.scsssrc/components/ChannelDetail/ChannelDetail.tsxsrc/components/ChannelDetail/ChannelDetailContext.tsxsrc/components/ChannelDetail/ChannelDetailEmptyList.tsxsrc/components/ChannelDetail/ChannelDetailListLoadingIndicator.tsxsrc/components/ChannelDetail/ChannelDetailNavButton.tsxsrc/components/ChannelDetail/ChannelDetailSearchInput.tsxsrc/components/ChannelDetail/Views/ChannelFilesView/ChannelFilesEmptyList.tsxsrc/components/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.tsxsrc/components/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.utils.tssrc/components/ChannelDetail/Views/ChannelFilesView/__tests__/ChannelFilesView.test.tsxsrc/components/ChannelDetail/Views/ChannelFilesView/index.tssrc/components/ChannelDetail/Views/ChannelFilesView/useChannelFilesSearch.tssrc/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsxsrc/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsxsrc/components/ChannelDetail/Views/ChannelManagementView/index.tssrc/components/ChannelDetail/Views/ChannelMediaView/ChannelMediaEmptyList.tsxsrc/components/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsxsrc/components/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.utils.tssrc/components/ChannelDetail/Views/ChannelMediaView/__tests__/ChannelMediaView.test.tsxsrc/components/ChannelDetail/Views/ChannelMediaView/index.tssrc/components/ChannelDetail/Views/ChannelMediaView/useChannelMediaSearch.tssrc/components/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsxsrc/components/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberDetail.tsxsrc/components/ChannelDetail/Views/ChannelMemberDetailView/__tests__/ChannelMemberDetail.test.tsxsrc/components/ChannelDetail/Views/ChannelMemberDetailView/index.tssrc/components/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsxsrc/components/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsxsrc/components/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsxsrc/components/ChannelDetail/Views/ChannelMembersView/ChannelMembersRemoveView.tsxsrc/components/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.tsxsrc/components/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.utils.tssrc/components/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersAddView.test.tsxsrc/components/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersBrowseView.test.tsxsrc/components/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsxsrc/components/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersRemoveView.test.tsxsrc/components/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersView.test.tsxsrc/components/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersView.utils.test.tssrc/components/ChannelDetail/Views/ChannelMembersView/__tests__/testUtils.tsxsrc/components/ChannelDetail/Views/ChannelMembersView/index.tssrc/components/ChannelDetail/Views/ChannelMembersView/useChannelMembersSearch.tssrc/components/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesEmptyList.tsxsrc/components/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesView.tsxsrc/components/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsxsrc/components/ChannelDetail/Views/PinnedMessagesView/index.tssrc/components/ChannelDetail/Views/PinnedMessagesView/usePinnedMessagesSearch.tssrc/components/ChannelDetail/__tests__/ChannelDetail.test.tsxsrc/components/ChannelDetail/__tests__/ChannelManagementActions.defaults.test.tsxsrc/components/ChannelDetail/__tests__/ChannelManagementView.test.tsxsrc/components/ChannelDetail/index.tssrc/components/ChannelDetail/styling/ChannelDetail.scsssrc/components/ChannelDetail/styling/ChannelFilesView.scsssrc/components/ChannelDetail/styling/ChannelManagementView.scsssrc/components/ChannelDetail/styling/ChannelMediaView.scsssrc/components/ChannelDetail/styling/ChannelMemberDetailView.scsssrc/components/ChannelDetail/styling/ChannelMembersView.scsssrc/components/ChannelDetail/styling/ChannelMembersViewListFooter.scsssrc/components/ChannelDetail/styling/PinnedMessagesView.scsssrc/components/ChannelDetail/styling/index.scsssrc/components/ChannelHeader/hooks/useChannelHasMembersOnline.tssrc/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.tssrc/components/ChannelListItem/__tests__/ChannelListItemActionButtons.defaults.test.tsxsrc/components/ChannelListItem/hooks/index.tssrc/components/ChannelListItem/hooks/useChannelPreviewInfo.tssrc/components/ChannelListItem/hooks/useIsUserMuted.tssrc/components/Chat/Chat.tsxsrc/components/Chat/__tests__/Chat.test.tsxsrc/components/Dialog/components/Prompt.tsxsrc/components/Dialog/service/DialogPortal.tsxsrc/components/Dialog/styling/Dialog.scsssrc/components/Dialog/styling/Prompt.scsssrc/components/FileIcon/FileIcon.tsxsrc/components/FileIcon/iconMap.tssrc/components/Form/Checkbox.tsxsrc/components/Form/SwitchField.tsxsrc/components/Form/index.tssrc/components/Form/styling/Checkbox.scsssrc/components/Form/styling/index.scsssrc/components/Icons/icons.tsxsrc/components/ListItemLayout/ListItemLayout.tsxsrc/components/ListItemLayout/index.tssrc/components/ListItemLayout/styling/ListItemLayout.scsssrc/components/ListItemLayout/styling/index.scsssrc/components/MessageActions/MessageActions.defaults.tsxsrc/components/MessageComposer/__tests__/MessageInput.test.tsxsrc/components/Modal/GlobalModal.tsxsrc/components/Modal/__tests__/GlobalModal.test.tsxsrc/components/Modal/styling/Modal.scsssrc/components/Notifications/hooks/__tests__/useNotificationApi.test.tsxsrc/components/Notifications/hooks/useNotificationApi.tssrc/components/Poll/PollCreationDialog/MultipleAnswersField.tsxsrc/components/Poll/PollOptionSelector.tsxsrc/components/Poll/styling/PollCreationDialog.scsssrc/components/Poll/styling/PollOptionList.scsssrc/components/SectionNavigator/SectionNavigator.tsxsrc/components/SectionNavigator/SectionNavigatorHeader.tsxsrc/components/SectionNavigator/__tests__/SectionNavigator.test.tsxsrc/components/SectionNavigator/__tests__/SectionNavigatorHeader.test.tsxsrc/components/SectionNavigator/index.tssrc/components/SectionNavigator/styling/SectionNavigator.scsssrc/components/SectionNavigator/styling/index.scsssrc/components/index.tssrc/context/DialogManagerContext.tsxsrc/i18n/de.jsonsrc/i18n/en.jsonsrc/i18n/es.jsonsrc/i18n/fr.jsonsrc/i18n/hi.jsonsrc/i18n/it.jsonsrc/i18n/ja.jsonsrc/i18n/ko.jsonsrc/i18n/nl.jsonsrc/i18n/pt.jsonsrc/i18n/ru.jsonsrc/i18n/tr.jsonsrc/styling/_utils.scsssrc/styling/index.scsssrc/utils/__tests__/isDmChannel.test.tssrc/utils/index.tssrc/utils/isDmChannel.ts
💤 Files with no reviewable changes (2)
- src/components/Poll/styling/PollOptionList.scss
- examples/vite/src/icons.tsx
| useEffect(() => { | ||
| setSearchInput(''); | ||
| }, [resetKey]); |
There was a problem hiding this comment.
Reset the upstream query when resetKey changes.
This effect only clears the local input state, so consumers can be left searching with the previous query while the field looks empty. Call onSearchChange('') here as well to keep the visible input and the active search source in sync.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/ChannelDetail/ChannelDetailSearchInput.tsx` around lines 18 -
20, The useEffect that currently only calls setSearchInput('') when resetKey
changes must also reset the upstream query by calling onSearchChange('') so the
visible input and the active search source stay in sync; update the effect in
ChannelDetailSearchInput (the useEffect referencing resetKey and setSearchInput)
to invoke onSearchChange('') as well (guarding if onSearchChange is provided)
whenever resetKey changes.
| return useMemo(() => { | ||
| const stateMembers = Object.values(channel.state?.members ?? {}); | ||
| const members = stateMembers.length ? stateMembers : (channel.data?.members ?? []); | ||
|
|
||
| return members.find( | ||
| (member) => member.user?.id && member.user.id !== client.user?.id, | ||
| ); | ||
| }, [channel, client.user?.id]); |
There was a problem hiding this comment.
Stop using the mutable channel object in hook dependency arrays—use channel.cid
src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx memoizes useOtherMember with useMemo(..., [channel, client.user?.id]). Replace the channel dependency with stable channel.cid (keep using the live channel inside the callback body, but don’t include it in the deps) to avoid handler churn and align with the repo rule. Also audit the other useMemo/useCallback dependencies in this file for channel.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx`
around lines 122 - 129, The useMemo for computing the other member currently
depends on the mutable channel object which causes unnecessary churn; change the
dependency array to use channel.cid and client.user?.id instead of channel,
keeping the live channel reference inside the callback (i.e., modify the useMemo
call that returns members.find(...) to use [channel.cid, client.user?.id]). Also
scan this file for other useMemo/useCallback hooks (e.g., any other hooks
referencing channel in their deps) and replace mutable channel dependencies with
channel.cid to follow the repo rule while retaining access to channel inside
callbacks.
| return { | ||
| canBlockUser: | ||
| isDmChannelWithOtherUser && ownCapabilities?.includes('ban-channel-members'), | ||
| canLeaveChannel: isGroupChannel && ownCapabilities?.includes('leave-channel'), | ||
| canMuteChannel: ownCapabilities?.includes('mute-channel'), | ||
| canMuteUser: isDmChannelWithOtherUser, | ||
| }; | ||
| }; | ||
|
|
||
| export const useBaseChannelManagementActionSetFilter = ( | ||
| channelManagementActionSet: ChannelManagementActionItem[], | ||
| ) => { | ||
| const { canBlockUser, canLeaveChannel, canMuteChannel, canMuteUser } = | ||
| useChannelManagementActionFilterState(); | ||
|
|
||
| return useMemo( | ||
| () => | ||
| channelManagementActionSet.filter((action) => { | ||
| switch (action.type) { | ||
| case 'blockUser': | ||
| return canBlockUser; | ||
| case 'muteChannel': | ||
| return canMuteChannel; | ||
| case 'muteUser': | ||
| return canMuteUser; | ||
| case 'leaveChannel': | ||
| return canLeaveChannel; | ||
| default: | ||
| return true; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx"
echo "=== File exists? ==="
ls -l "$FILE"
echo
echo "=== useChannelManagementActionFilterState (capability computations) ==="
# Print around the hook/function definition; fallback to searching for its name
rg -n "useChannelManagementActionFilterState" "$FILE"
rg -n "ban-channel-members|leave-channel|mute-channel|mute-user|block|deleteChat|delete.*channel|channel.delete" "$FILE" || true
# Show around the capability hook
line="$(rg -n "useChannelManagementActionFilterState" "$FILE" | head -n1 | cut -d: -f1)"
start=$((line-60))
end=$((line+80))
sed -n "${start},${end}p" "$FILE" | cat -n
echo
echo "=== useBaseChannelManagementActionSetFilter switch cases ==="
# Show around the filter function provided in the snippet
line2="$(rg -n "useBaseChannelManagementActionSetFilter" "$FILE" | head -n1 | cut -d: -f1)"
start2=$((line2-40))
end2=$((line2+120))
sed -n "${start2},${end2}p" "$FILE" | cat -n
echo
echo "=== Find deleteChat action item definition and its conditions ==="
rg -n "deleteChat" "$FILE"
# Show a wider window around each occurrence, but cap output
for l in $(rg -n "deleteChat" "$FILE" | cut -d: -f1 | sort -n | head -n 10); do
start=$((l-70))
end=$((l+120))
echo "--- Context around deleteChat at line $l (showing $start-$end) ---"
sed -n "${start},${end}p" "$FILE" | cat -n
done
echo
echo "=== Find blockUser/unBlockUser handler gating or capability checks ==="
rg -n "blockUser|unBlockUser" "$FILE"
for l in $(rg -n "blockUser|unBlockUser" "$FILE" | cut -d: -f1 | sort -n | head -n 10); do
start=$((l-60))
end=$((l+120))
echo "--- Context around block/unblock at line $l (showing $start-$end) ---"
sed -n "${start},${end}p" "$FILE" | cat -n
done
echo
echo "=== Search for tests asserting UI visibility gating ==="
# If tests exist, look for the test file name and relevant strings
TESTFILE="$(fd -t f "ChannelManagementActions.*test.*" src | head -n 20 | head -n 1 || true)"
echo "Possible test file: $TESTFILE"
if [ -n "${TESTFILE:-}" ]; then
rg -n "deleteChat|blockUser|ban-channel-members|leave-channel|mute-channel" "$TESTFILE" || true
# show some surrounding test blocks
rg -n "deleteChat|blockUser" "$TESTFILE" || true
fi
echo
echo "=== Done ==="Repository: GetStream/stream-chat-react
Length of output: 16417
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx"
echo "=== Key line numbers ==="
rg -n "const useChannelManagementActionFilterState|own_capabilities|ban-channel-members|deleteChat|type: 'deleteChat'|blockUser|unBlockUser" "$FILE" | head -n 80
echo
echo "=== Capability gating state (around useChannelManagementActionFilterState) ==="
start=120; end=190
sed -n "${start},${end}p" "$FILE" | cat -n
echo
echo "=== Filter switch (useBaseChannelManagementActionSetFilter) ==="
start=140; end=220
sed -n "${start},${end}p" "$FILE" | cat -n
echo
echo "=== deleteChat handler (around const deleteChat) ==="
# Based on prior output: deleteChat starts ~644
start=620; end=710
sed -n "${start},${end}p" "$FILE" | cat -n
echo
echo "=== deleteChat action item (around type: 'deleteChat') ==="
# Based on prior output: type: 'deleteChat' around ~736
start=690; end=780
sed -n "${start},${end}p" "$FILE" | cat -n
echo
echo "=== blockUser handler/action item (around blockUser) ==="
# Based on prior output: blockUser around ~474 and action item around ~700? We'll print broader window.
start=430; end=610
sed -n "${start},${end}p" "$FILE" | cat -n
echo
echo "=== Find where default action set items are built and how deleteChat/blockUser are included ==="
# Look for functions/constants that build the action set; limit to nearby names
rg -n "default action|action set|ChannelManagementActionItem|channelManagementActionSet|useBaseChannelManagementActionSetFilter" "$FILE" | head -n 80
echo
echo "=== Search for tests ==="
# Find any test files relevant to ChannelManagementActions.defaults
fd -t f "*ChannelManagementActions*defaults*test*" src | head -n 20
fd -t f "*ChannelManagementActions*test*" src | head -n 50Repository: GetStream/stream-chat-react
Length of output: 19741
🏁 Script executed:
#!/bin/bash
set -euo pipefail
RG_OPTS="--no-heading --line-number"
FILE="src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx"
echo "=== Find usages of defaultChannelManagementActionSet and useBaseChannelManagementActionSetFilter ==="
rg ${RG_OPTS} "defaultChannelManagementActionSet|useBaseChannelManagementActionSetFilter" src/components/ChannelDetail/Views/ChannelManagementView -S
rg ${RG_OPTS} "defaultChannelManagementActionSet|useBaseChannelManagementActionSetFilter" src -S
echo
echo "=== Search for capability gating related to delete/chat across repo (delete-channel, delete-messages, etc.) ==="
rg ${RG_OPTS} "delete-channel|deleteChat|channel:delete|own_capabilities.*delete|own_capabilities.*remove|delete.*capabilit|channel.delete" src -S
echo
echo "=== Locate test files mentioning ChannelManagementActions.defaults ==="
# Use literal-glob patterns instead of regex
fd -t f "*ChannelManagementActions.defaults*" src | head -n 50
fd -t f "*ChannelManagementActions*" src | head -n 50
fd -t f "*ChannelManagementActions*test*" src | head -n 50
fd -t f "*ChannelManagement*test*" src | head -n 50
echo
echo "=== Search for tests that mention deleteChat/blockUser visibility ==="
for tf in $(fd -t f "*test*" src | head -n 200); do
rg ${RG_OPTS} "deleteChat|blockUser|ban-channel-members|leave-channel|mute-channel|delete chat" "$tf" -S >/dev/null 2>&1 && echo "MATCH: $tf"
done
echo
echo "=== Search for capability strings used for own_capabilities in this area ==="
rg ${RG_OPTS} "own_capabilities?.includes\\('([^']+)'\\)" src/components/ChannelDetail/Views/ChannelManagementView -SRepository: GetStream/stream-chat-react
Length of output: 6182
🏁 Script executed:
#!/bin/bash
set -euo pipefail
TESTFILE="src/components/ChannelDetail/__tests__/ChannelManagementActions.defaults.test.tsx"
if [ -f "$TESTFILE" ]; then
echo "=== Test file line count ==="
wc -l "$TESTFILE"
echo
echo "=== Test file around matches (400-520) ==="
sed -n '380,520p' "$TESTFILE" | cat -n
else
echo "Missing $TESTFILE"
fi
echo
echo "=== ChannelManagementView usage (around where filtering is applied) ==="
FILE2="src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx"
if [ -f "$FILE2" ]; then
wc -l "$FILE2"
rg -n "channelManagementActionSet|useBaseChannelManagementActionSetFilter|defaultChannelManagementActionSet" "$FILE2" | head -n 50
echo
sed -n '360,480p' "$FILE2" | cat -n
else
echo "Missing $FILE2"
fiRepository: GetStream/stream-chat-react
Length of output: 7851
Gate deleteChat by permissions; blockUser capability check matches its API call
useBaseChannelManagementActionSetFilteronly capability-gatesblockUser/muteChannel/muteUser/leaveChannel; for anything else (includingdeleteChat) it falls through todefault: return true, sodeleteChatremains visible even whenchannel.data.own_capabilitiesis empty (see the related unit test expecting only['deleteChat']).DeleteChatActionunconditionally callschannel.delete(), so its visibility should also be tied to the appropriateown_capabilities(instead of pass-through).- The
blockUsergating (ban-channel-membersincanBlockUser) aligns with the action invokingclient.blockUser()/unBlockUser(), so that specific mismatch isn’t supported by the code.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx`
around lines 145 - 174, Add a permission gate for the 'deleteChat' action: in
useChannelManagementActionFilterState expose a new flag (e.g., canDeleteChat)
set from ownCapabilities?.includes('delete-channel') (or the actual capability
name DeleteChatAction expects), then update
useBaseChannelManagementActionSetFilter to handle the 'deleteChat' action type
(case 'deleteChat': return canDeleteChat). Also ensure DeleteChatAction itself
checks the same capability flag before calling channel.delete(); keep the
existing blockUser check (ban-channel-members) as-is since it aligns with the
client.blockUser/unBlockUser calls.
| const initialName = channel.data?.name ?? ''; | ||
| const [name, setName] = useState(initialName); | ||
| // null = keep current avatar, File = replace it, 'removed' = clear it | ||
| const [imageEdit, setImageEdit] = useState<File | 'removed' | null>(null); | ||
| const [isSaving, setIsSaving] = useState(false); | ||
|
|
||
| const fileInputRef = useRef<HTMLInputElement>(null); | ||
|
|
||
| const pickedFile = imageEdit instanceof File ? imageEdit : null; | ||
|
|
||
| // Preview the locally picked file, releasing the object URL when it changes or unmounts. | ||
| const objectUrl = useMemo( | ||
| () => (pickedFile ? URL.createObjectURL(pickedFile) : null), | ||
| [pickedFile], | ||
| ); | ||
|
|
||
| useEffect( | ||
| () => () => { | ||
| if (objectUrl) URL.revokeObjectURL(objectUrl); | ||
| }, | ||
| [objectUrl], | ||
| ); | ||
|
|
||
| const previewImageUrl = | ||
| objectUrl ?? (imageEdit === 'removed' ? undefined : displayImage); | ||
|
|
||
| const trimmedName = name.trim(); | ||
| const nameChanged = trimmedName !== initialName.trim(); | ||
| const imageChanged = imageEdit !== null; | ||
| const hasChanges = (trimmedName.length > 0 && nameChanged) || imageChanged; | ||
| const canSubmit = trimmedName.length > 0 && !isSaving && hasChanges; |
There was a problem hiding this comment.
Reset the dirty baseline after a successful save.
initialName is captured only once, but success only clears imageEdit. After updatePartial() resolves, a renamed channel still satisfies nameChanged, so the Save button stays visible/enabled even though there are no unsaved changes left. Update the baseline to the saved values or exit edit mode on success so repeated identical writes are not possible.
Also applies to: 243-256
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx`
around lines 171 - 201, The save path leaves the original baseline (initialName)
unchanged so nameChanged stays true after updatePartial; change the component to
track a mutable baseline (e.g., replace const initialName with a state or ref
like baselineName via useState/useRef and compute nameChanged/hasChanges against
baselineName), and after a successful updatePartial() call update that baseline
to the newly saved trimmedName and clear imageEdit (setImageEdit(null)) and
isSaving as before (or alternatively exit edit mode). Update references to
initialName in comparisons to use baselineName so the Save button disables when
there are no real unsaved changes.
| <Modal | ||
| className={clsx( | ||
| 'str-chat__gallery-modal', | ||
| 'str-chat__channel-detail__media-view__viewer', | ||
| )} | ||
| onClose={closeViewer} | ||
| open={viewerOpen} | ||
| > | ||
| <Gallery | ||
| GalleryUI={GalleryUI} | ||
| initialIndex={selectedIndex} | ||
| items={galleryItems} | ||
| onRequestClose={closeViewer} | ||
| /> |
There was a problem hiding this comment.
Add an accessible name to the media viewer modal.
This dialog opens without aria-label/aria-labelledby, and the rendered Gallery here does not provide a visible dialog title. Screen readers will announce an unnamed dialog, which makes the viewer hard to identify and navigate.
♿ Suggested fix
<Modal
+ aria-label={t('Media viewer')}
className={clsx(
'str-chat__gallery-modal',
'str-chat__channel-detail__media-view__viewer',
)}
onClose={closeViewer}
open={viewerOpen}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Modal | |
| className={clsx( | |
| 'str-chat__gallery-modal', | |
| 'str-chat__channel-detail__media-view__viewer', | |
| )} | |
| onClose={closeViewer} | |
| open={viewerOpen} | |
| > | |
| <Gallery | |
| GalleryUI={GalleryUI} | |
| initialIndex={selectedIndex} | |
| items={galleryItems} | |
| onRequestClose={closeViewer} | |
| /> | |
| <Modal | |
| aria-label={t('Media viewer')} | |
| className={clsx( | |
| 'str-chat__gallery-modal', | |
| 'str-chat__channel-detail__media-view__viewer', | |
| )} | |
| onClose={closeViewer} | |
| open={viewerOpen} | |
| > | |
| <Gallery | |
| GalleryUI={GalleryUI} | |
| initialIndex={selectedIndex} | |
| items={galleryItems} | |
| onRequestClose={closeViewer} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx`
around lines 148 - 161, The Modal used in ChannelMediaView opens the media
viewer without an accessible name; update the Modal invocation in
ChannelMediaView (the Modal wrapping Gallery/GalleryUI) to include an accessible
name by adding an aria-label or aria-labelledby prop, and if you use
aria-labelledby add a visually-hidden title element (e.g., "Media viewer" or
localized string) with that id inside the Modal/GalleryUI so screen readers
announce the dialog; ensure the id is unique and referenced from Modal (and keep
existing props like open={viewerOpen} and onClose={closeViewer} intact).
| const renderWithChannel = (ui: React.ReactElement, channel: Channel = createChannel()) => | ||
| render(<ChannelDetailProvider channel={channel}>{ui}</ChannelDetailProvider>); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use the standard Chat/Channel wrapper for this component test.
Rendering through ChannelDetailProvider alone bypasses the provider stack this repo expects for component tests, so this suite can miss context-wiring regressions that only surface under <Chat>/<Channel>. As per coding guidelines, "**/__tests__/**/*.{ts,tsx}: Wrap component tests with , , and relevant sub-components; use channel.watch() after setup`."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/components/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsx`
around lines 132 - 133, The test helper renderWithChannel currently mounts the
UI inside ChannelDetailProvider only; update it to wrap the component with the
standard Chat/Channel test stack instead: render the UI inside <Chat
client={...}> and <Channel channel={channel}> (preserving ChannelDetailProvider
if still needed), and after creating the channel via createChannel() call
channel.watch() (or mock a watched channel) before rendering so the component
sees the same context wiring as real usage; adjust the helper name if desired
and update tests that use renderWithChannel to rely on the new wrapper.
Source: Coding guidelines
| const fallbackPinnedMessages = useMemo( | ||
| // sort descending by creation date | ||
| () => | ||
| channel.state?.pinnedMessages?.sort( | ||
| (a, b) => b.created_at.getTime() - a.created_at.getTime(), | ||
| ) ?? [], |
There was a problem hiding this comment.
Don’t sort channel.state.pinnedMessages in place.
sort() mutates the SDK-owned array, so this hook is reordering shared channel state just to derive a fallback view. That can leak a local presentation concern into every other consumer reading channel.state.pinnedMessages. As per coding guidelines, "**/*.{ts,tsx}: Do not change message sort order; the SDK maintains order, and local changes will conflict`."
Suggested update
const fallbackPinnedMessages = useMemo(
// sort descending by creation date
() =>
- channel.state?.pinnedMessages?.sort(
- (a, b) => b.created_at.getTime() - a.created_at.getTime(),
- ) ?? [],
+ [...(channel.state?.pinnedMessages ?? [])].sort(
+ (a, b) => b.created_at.getTime() - a.created_at.getTime(),
+ ),
[channel],
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const fallbackPinnedMessages = useMemo( | |
| // sort descending by creation date | |
| () => | |
| channel.state?.pinnedMessages?.sort( | |
| (a, b) => b.created_at.getTime() - a.created_at.getTime(), | |
| ) ?? [], | |
| const fallbackPinnedMessages = useMemo( | |
| // sort descending by creation date | |
| () => | |
| [...(channel.state?.pinnedMessages ?? [])].sort( | |
| (a, b) => b.created_at.getTime() - a.created_at.getTime(), | |
| ), | |
| [channel], | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/components/ChannelDetail/Views/PinnedMessagesView/usePinnedMessagesSearch.ts`
around lines 25 - 30, The fallbackPinnedMessages computation in
usePinnedMessagesSearch currently calls sort() directly on
channel.state?.pinnedMessages which mutates the SDK-owned array; instead create
a shallow copy of channel.state?.pinnedMessages (e.g., via slice() or spread)
and sort that copy so you don't mutate the original array; update the
fallbackPinnedMessages expression to copy then sort while keeping the same
descending-by-created_at comparator, referencing fallbackPinnedMessages and
channel.state?.pinnedMessages in your change.
Source: Coding guidelines
| const [watchers, setWatchers] = useState<ChannelState['watchers']>(() => | ||
| Object.assign({}, channel?.state?.watchers ?? {}), | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| setWatchers(Object.assign({}, channel?.state?.watchers ?? {})); | ||
| }, [channel]); | ||
|
|
||
| useEffect(() => { | ||
| if (!enabled || !channel) return; | ||
|
|
||
| const startSubscription = channel.on('user.watching.start', (event) => { | ||
| setWatchers((prev) => { | ||
| if (!event.user?.id) return prev; | ||
| if (prev[event.user.id]) return prev; | ||
| return Object.assign({ [event.user.id]: event.user }, prev); | ||
| }); | ||
| }); | ||
| const stopSubscription = channel.on('user.watching.stop', (event) => { | ||
| setWatchers((prev) => { | ||
| if (!event.user?.id || !prev[event.user.id]) return prev; | ||
|
|
||
| const next = Object.assign({}, prev); | ||
| delete next[event.user.id]; | ||
| return next; | ||
| }); | ||
| }); | ||
|
|
||
| return () => { | ||
| startSubscription.unsubscribe(); | ||
| stopSubscription.unsubscribe(); | ||
| }; | ||
| }, [channel, enabled]); | ||
|
|
||
| if (!enabled) return false; | ||
|
|
||
| return Object.keys(watchers).length > 0; |
There was a problem hiding this comment.
Exclude the current user from the watcher set.
This hook returns true when any watcher exists, including the local user. In a DM that means the header/avatar can show the other participant as online as soon as I am watching the channel, even if they are offline. Filter out client.user.id here or make the hook target the non-self member explicitly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/ChannelHeader/hooks/useChannelHasMembersOnline.ts` around
lines 17 - 53, The hook currently counts the local user as a watcher, so update
the initialization and update logic to exclude the client user id: when
constructing initial watchers state and in the effect that runs
setWatchers(Object.assign({}, channel?.state?.watchers ?? {})), filter out
client.user?.id from the object; likewise, in the 'user.watching.start' and
'user.watching.stop' handlers (startSubscription/stopSubscription) ignore events
where event.user?.id === client.user?.id and when adding/removing entries ensure
you never add the client id into watchers; finally, when computing the return
value (Object.keys(watchers).length > 0) this will then reflect only other users
watching the channel.
| .str-chat__list-item-layout__container { | ||
| --list-item-padding: var(--str-chat__spacing-xs) var(--str-chat__spacing-sm); | ||
| display: flex; | ||
| flex: 1; | ||
| align-items: center; | ||
| gap: var(--str-chat__spacing-sm); | ||
| min-width: 0; | ||
| text-align: start; | ||
| padding: var(--list-item-padding); | ||
| border-radius: var(--str-chat__radius-md); |
There was a problem hiding this comment.
Fix the new Stylelint failures.
Lines 15, 31, and 133 violate declaration-empty-line-before, so this stylesheet will fail lint as written.
🧹 Proposed fix
.str-chat__list-item-layout__container {
--list-item-padding: var(--str-chat__spacing-xs) var(--str-chat__spacing-sm);
+
display: flex;
flex: 1;
align-items: center;
gap: var(--str-chat__spacing-sm);
min-width: 0;
@@
.str-chat__list-item-layout__container:where(button) {
`@include` utils.button-reset;
+
padding: var(--list-item-padding);
cursor: pointer;
@@
.str-chat__list-item-layout__subtitle,
.str-chat__list-item-layout__description,
.str-chat__list-item-layout__title {
`@include` utils.ellipsis-text;
+
max-width: 100%;
}Based on static analysis, Stylelint 17.12.0 reports declaration-empty-line-before at Lines 15, 31, and 133.
Also applies to: 29-32, 129-134
🧰 Tools
🪛 Stylelint (17.12.0)
[error] 15-15: Expected empty line before declaration (declaration-empty-line-before)
(declaration-empty-line-before)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/ListItemLayout/styling/ListItemLayout.scss` around lines 13 -
22, Stylelint flags are due to missing empty lines before certain declarations
in this stylesheet (e.g., inside .str-chat__list-item-layout__container and the
blocks around lines 29-32 and 129-134); fix by inserting a single blank line
before the offending declaration(s) so they comply with
declaration-empty-line-before (or collapse/merge those declarations so the rule
no longer applies), targeting the selector
.str-chat__list-item-layout__container and the other nearby rule blocks reported
by the linter; re-run Stylelint to confirm no more declaration-empty-line-before
errors.
Source: Linters/SAST tools
| const memberCount = channel.data?.member_count ?? 0; | ||
| return ( | ||
| memberCount === 1 || | ||
| (memberCount === 2 && | ||
| !!ownUserId && | ||
| Object.values(channel.state?.members).some(({ user }) => user?.id === ownUserId)) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# List repo files for context
ls -la
# Inspect the target file with line numbers
echo "---- src/utils/isDmChannel.ts ----"
if [ -f "src/utils/isDmChannel.ts" ]; then
nl -ba src/utils/isDmChannel.ts | sed -n '1,200p'
else
echo "Missing src/utils/isDmChannel.ts"
exit 1
fi
# Find usages
echo "---- rg 'isDmChannel(' ----"
rg -n "isDmChannel\\s*\\(" -S src || true
# Inspect any related consumer file mentioned in the review comment
echo "---- Consumer: ChannelManagementActions.defaults.tsx ----"
if [ -f "src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx" ]; then
nl -ba src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx | sed -n '1,240p'
else
echo "Missing consumer file at expected path"
fi
# Check TypeScript types for Channel/Member shape (best-effort)
echo "---- Search for 'member_count' and 'members' types ----"
rg -n "member_count" -S src || true
rg -n "state\\.members" -S src || true
rg -n "members\\??\\:" -S src || trueRepository: GetStream/stream-chat-react
Length of output: 3450
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- src/utils/isDmChannel.ts (first 200 lines, with line numbers) ----"
if [ -f "src/utils/isDmChannel.ts" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/utils/isDmChannel.ts | sed -n '1,200p'
else
echo "Missing src/utils/isDmChannel.ts"
exit 1
fi
echo "---- rg 'isDmChannel(' in src ----"
rg -n "isDmChannel\\s*\\(" src || true
echo "---- Inspect file mentioned in review comment ----"
if [ -f "src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx | sed -n '1,260p'
else
echo "Missing consumer file path"
fi
echo "---- Search for channel.state?.members / members usage ----"
rg -n "state\\?\\.members|state\\.members|members\\?" src/utils src/components src || true
echo "---- Search for member_count usage ----"
rg -n "member_count" src || trueRepository: GetStream/stream-chat-react
Length of output: 28751
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- src/utils/__tests__/isDmChannel.test.ts ----"
if [ -f "src/utils/__tests__/isDmChannel.test.ts" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/utils/__tests__/isDmChannel.test.ts | sed -n '1,220p'
else
echo "Missing test file"
fi
echo "---- Inspect call site: useChannelHeaderOnlineStatus ----"
if [ -f "src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/components/ChannelHeader/hooks/useChannelHeaderOnlineStatus.ts | sed -n '1,140p'
fi
echo "---- Inspect call site: ChannelManagementView.tsx (DM checks) ----"
if [ -f "src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx" ]; then
rg -n "resolvedIsDmChannel\\s*=\\s*isDmChannel\\(" src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx
awk '{printf "%6d\t%s\n", NR, $0}' src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx | sed -n '40,120p'
awk '{printf "%6d\t%s\n", NR, $0}' src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx | sed -n '150,220p'
awk '{printf "%6d\t%s\n", NR, $0}' src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx | sed -n '380,460p'
fi
echo "---- Inspect call site: ChannelManagementActions.defaults.tsx (DM check line ~136) ----"
if [ -f "src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/components/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx | sed -n '110,175p'
fiRepository: GetStream/stream-chat-react
Length of output: 16701
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- Inspect src/components/Channel/channelState.ts around member initialization ----"
if [ -f "src/components/Channel/channelState.ts" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/components/Channel/channelState.ts | sed -n '1,220p'
else
echo "Missing src/components/Channel/channelState.ts"
fi
echo "---- Search for 'members: { ...channel.state.members }' ----"
rg -n "members: \\{ \\.+channel\\.state\\.members" -S src/components/Channel/channelState.ts src || true
echo "---- Inspect ChannelStateContext type for members ----"
if [ -f "src/context/ChannelStateContext.tsx" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/context/ChannelStateContext.tsx | sed -n '1,140p'
else
echo "Missing src/context/ChannelStateContext.tsx"
fi
echo "---- Inspect any code that may set state.members to undefined ----"
rg -n "state\\.members\\s*=\\s*undefined|members\\s*:\\s*undefined" -S src || trueRepository: GetStream/stream-chat-react
Length of output: 11222
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- Search for user_id fallbacks when iterating members ----"
rg -n "user_id" src/components src/utils -S || true
echo "---- Search for Object.values(channel.state?.members) usage patterns ----"
rg -n "Object\\.values\\(channel\\.state\\?\\.members\\)" src/components src/utils -S || true
echo "---- Search for Object.values(channel.state.members) usage patterns ----"
rg -n "Object\\.values\\(channel\\.state\\.members\\)" src/components src/utils -S || true
echo "---- Search for direct checks on member.user?.id within members iterations ----"
rg -n "member\\.user\\?\\.id|\\{ user \\}\\) =>" src/components src/utils -S || trueRepository: GetStream/stream-chat-react
Length of output: 8957
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- Search for ChannelStateProvider usage (where members initial state is set) ----"
rg -n "ChannelStateProvider\\b" src/components src/context -S
echo "---- Inspect src/components/Channel/Channel.tsx for initial channel state context value ----"
if [ -f "src/components/Channel/Channel.tsx" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/components/Channel/Channel.tsx | sed -n '380,620p'
else
echo "Missing src/components/Channel/Channel.tsx"
fi
echo "---- Search within Channel.tsx for 'members:' or 'state:' initialization ----"
rg -n "members\\s*:" src/components/Channel/Channel.tsx -S || true
rg -n "state\\.members|channel\\.state\\.members" src/components/Channel/Channel.tsx -S || true
echo "---- Inspect src/context/ChannelStateContext.tsx provider setup if any helper initializes members ----"
if [ -f "src/context/ChannelStateContext.tsx" ]; then
rg -n "members\\b|member" src/context/ChannelStateContext.tsx -S || true
fiRepository: GetStream/stream-chat-react
Length of output: 24564
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- Check if node_modules/stream-chat exists (to inspect types) ----"
ls -la node_modules/stream-chat 2>/dev/null || echo "node_modules/stream-chat not present"
echo "---- Inspect src/components/ChannelList/hooks/useChannelListShape.ts around members normalization ----"
if [ -f "src/components/ChannelList/hooks/useChannelListShape.ts" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/components/ChannelList/hooks/useChannelListShape.ts | sed -n '180,280p'
awk '{printf "%6d\t%s\n", NR, $0}' src/components/ChannelList/hooks/useChannelListShape.ts | sed -n '400,470p'
else
echo "Missing file src/components/ChannelList/hooks/useChannelListShape.ts"
fi
echo "---- Inspect src/components/Channel/Channel.tsx around member mapping at ~450-480 ----"
if [ -f "src/components/Channel/Channel.tsx" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/components/Channel/Channel.tsx | sed -n '430,520p'
else
echo "Missing file src/components/Channel/Channel.tsx"
fiRepository: GetStream/stream-chat-react
Length of output: 10112
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- Inspect src/utils/getChannel.ts ----"
if [ -f "src/utils/getChannel.ts" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/utils/getChannel.ts | sed -n '1,240p'
else
echo "Missing src/utils/getChannel.ts"
fi
echo "---- Search for mockChannelStateContext implementation ----"
rg -n "function\\s+mockChannelStateContext|mockChannelStateContext\\s*=" src -S
echo "---- Inspect mockChannelStateContext definition (if found) ----"
# try to open first match(s)
matches=$(rg -n "mockChannelStateContext" src -S | head -n 20)
echo "$matches"
# open any file that appears to define it
first_file=$(echo "$matches" | head -n 1 | awk -F: '{print $1}')
if [ -n "${first_file:-}" ] && [ -f "$first_file" ]; then
echo "---- Open: $first_file ----"
awk '{printf "%6d\t%s\n", NR, $0}' "$first_file" | sed -n '1,220p'
fi
echo "---- Inspect any tests that call isDmChannel with missing state.members ----"
rg -n "isDmChannel\\(.*members" src/utils/__tests__ -S || trueRepository: GetStream/stream-chat-react
Length of output: 8447
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- Inspect ChannelMembersView.utils.ts ----"
if [ -f "src/components/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.utils.ts" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/components/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.utils.ts | sed -n '1,120p'
fi
echo "---- Inspect ChannelMembersView.utils.test.ts ----"
if [ -f "src/components/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersView.utils.test.ts" ]; then
awk '{printf "%6d\t%s\n", NR, $0}' src/components/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersView.utils.test.ts | sed -n '1,140p'
fiRepository: GetStream/stream-chat-react
Length of output: 4703
Guard isDmChannel against missing members + use user_id fallback
Object.values(channel.state?.members) will throw if channel.state.members is ever undefined (needs ?? {} fallback). Also, the two-member check only compares member.user?.id and ignores member.user_id, unlike other member utilities that fall back to user_id, so partially hydrated members can be misclassified.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/isDmChannel.ts` around lines 10 - 15, The isDmChannel check should
guard against missing members and consider partially hydrated members: when
iterating members use Object.values(channel.state?.members ?? {}) instead of
Object.values(channel.state?.members) to avoid throwing when members is
undefined, and in the two-member branch check both member.user?.id and
member.user_id (e.g., compare ownUserId to (member.user?.id ?? member.user_id))
so fallback IDs are handled; keep the existing memberCount logic (memberCount
=== 1 || (memberCount === 2 && !!ownUserId && ...)) and update only the members
access and ID comparison.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx (1)
95-100:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove the leading
.from these icon class names.
classNametakes raw class tokens, not CSS selectors. With the leading dot, the rendered token is literally.str-chat__icon--destructive, so the destructive icon styles never match.Based on learnings, icon classes in this repo use plain `str-chat__icon--` tokens, so the selector-style `.` prefix is invalid here.Proposed fix
const BlockUserActionIcon = () => ( - <IconNoSign className='.str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--block-user' /> + <IconNoSign className='str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--block-user' /> ); const RemoveUserActionIcon = () => ( - <IconUserRemove className='.str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--remove-user' /> + <IconUserRemove className='str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--remove-user' /> );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx` around lines 95 - 100, The icon className strings in BlockUserActionIcon and RemoveUserActionIcon incorrectly include leading dots (treating them like CSS selectors), causing classes like ".str-chat__icon--destructive" to be rendered literally and styles to not apply; remove the leading '.' from each class token in the className props for IconNoSign and IconUserRemove so they use plain tokens (e.g., "str-chat__icon--destructive str-chat__channel-detail__action-icon ...") to restore styling.Source: Learnings
src/plugins/ChannelDetail/ChannelDetailNavButton.tsx (1)
27-33:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSet the nav button type explicitly.
Because this renders a real
<button>, omittingtypemakes it submit any surrounding form by default. That creates accidental saves ifChannelDetailNavButtonis embedded inside a form host.Proposed fix
const rootProps = useMemo( () => ({ ...props, 'aria-current': selected ? ('page' as const) : undefined, className: clsx('str-chat__channel-detail__nav-button', className), onClick: select, + type: 'button' as const, }), [className, props, select, selected], );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/ChannelDetailNavButton.tsx` around lines 27 - 33, The rootProps object in ChannelDetailNavButton (created via useMemo) is missing an explicit button type, which causes the rendered <button> to default to type="submit" when nested in a form; update the rootProps (the object returned in the useMemo where 'aria-current', className, and onClick are set) to include type: 'button' so the button will not submit surrounding forms (ensure you add this alongside the existing className and onClick entries that reference select).src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx (1)
48-62:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove the leading
.from these icon class names.Lines 49, 52, and 61 pass class names like
.str-chat__icon--destructive. React will render that literal dot into theclassattribute, so the destructive icon styles never match.Suggested fix
const BlockUserActionIcon = () => ( - <IconNoSign className='.str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--block-user' /> + <IconNoSign className='str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--block-user' /> ); const DeleteChatActionIcon = () => ( - <IconDelete className='.str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--delete-chat' /> + <IconDelete className='str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--delete-chat' /> ); const LeaveChannelActionIcon = () => ( - <IconLeave className='.str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--leave-channel' /> + <IconLeave className='str-chat__icon--destructive str-chat__channel-detail__action-icon str-chat__channel-detail__action-icon--leave-channel' /> );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx` around lines 48 - 62, The icon components BlockUserActionIcon, DeleteChatActionIcon, and LeaveChannelActionIcon include className strings that start with a literal '.' which will render into the DOM and break CSS selectors; edit these components to remove the leading '.' from their className values (e.g., change '.str-chat__icon--destructive ...' to 'str-chat__icon--destructive ...') so the correct CSS classes are applied; leave MuteActionIcon and MutedActionIcon as-is since they already use correct class names.src/plugins/ChannelDetail/ChannelDetail.tsx (1)
49-87:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRoute the new nav labels through
t()instead of hard-coded English.
Channel info,Members,Pinned messages,Photos & videos, andFilesare new visible SDK strings, but they bypass the translation pipeline here.As per coding guidelines, use
useTranslationContext()for translations and add all translatable strings tosrc/i18n/files.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/ChannelDetail.tsx` around lines 49 - 87, The nav button labels are hard-coded English strings in ChannelManagementNavButton, ChannelMembersNavButton, PinnedMessagesNavButton, ChannelMediaNavButton, and ChannelFilesNavButton; replace those title props with translated strings by calling useTranslationContext() (or the project's translation hook) to get t and pass t('...') for each label, and add the new keys ("Channel info", "Members", "Pinned messages", "Photos & videos", "Files") to the src/i18n/ resource files so translations are available. Ensure you import/use useTranslationContext() in ChannelDetail.tsx and use the same keys in the i18n JSON/TS files.Source: Coding guidelines
src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx (1)
93-110:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon’t render member rows as buttons when no selection handler is provided.
onMemberSelectis optional, but these rows always announce “View member details…” and stay keyboard-clickable even when the callback is missing. That leaves a no-op button in the accessibility tree.🛠️ Suggested fix
+ const isInteractive = !!onMemberSelect; + return ( <ListItemLayout key={memberUserId} LeadingSlot={() => ( <Avatar @@ )} - RootElement='button' - rootProps={{ - 'aria-label': t('View member details for {{ member }}', { - member: displayName, - }), - className: 'str-chat__channel-detail__channel-members-view__list-item', - onClick: () => onMemberSelect?.(member), - }} + RootElement={isInteractive ? 'button' : 'div'} + rootProps={{ + ...(isInteractive + ? { + 'aria-label': t('View member details for {{ member }}', { + member: displayName, + }), + onClick: () => onMemberSelect(member), + } + : {}), + className: 'str-chat__channel-detail__channel-members-view__list-item', + }} subtitle={getPresenceStatusText(user, t)} title={displayName}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx` around lines 93 - 110, The member rows are rendered as interactive buttons even when onMemberSelect is undefined; update the ListItemLayout usage so that when onMemberSelect is falsy you do not render a button: choose a non-interactive element (e.g., set RootElement='div' or omit RootElement) and remove rootProps.onClick, aria-label, and any keyboard/tab props in that branch; keep the existing button/aria-label/onClick only when onMemberSelect is provided (references: ListItemLayout, RootElement, rootProps.onClick, aria-label, onMemberSelect).src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx (2)
68-74:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRecompute the DM counterpart from current member state.
otherMemberUserIddepends onchannel.state.members, but this memo only keys on the stablechannelobject. If members hydrate after mount, DM mute state can stay stuck asundefinedand the info view never reflects the other member correctly.♻️ Suggested fix
- const otherMemberUserId = useMemo(() => { - if (!resolvedIsDmChannel) return; - - return Object.values(channel.state?.members ?? {}).find( - (member) => member.user?.id && member.user.id !== client.user?.id, - )?.user?.id; - }, [channel, client.user?.id, resolvedIsDmChannel]); + const otherMemberUserId = useMemo(() => { + if (!resolvedIsDmChannel) return; + + return Object.values(channel.state?.members ?? {}).find( + (member) => member.user?.id && member.user.id !== client.user?.id, + )?.user?.id; + }, [channel.state?.members, client.user?.id, resolvedIsDmChannel]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx` around lines 68 - 74, The memoized otherMemberUserId (useMemo in ChannelManagementView) only depends on channel and thus won't recompute when channel.state.members hydrates; update the dependency array to include the members collection so the find runs when members change (e.g. add channel.state?.members or Object.values(channel.state?.members) to the dependency array alongside client.user?.id and resolvedIsDmChannel) so otherMemberUserId updates when DM members hydrate.
200-204:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAllow avatar-only saves for unnamed channels.
canSubmitcurrently requires a non-empty trimmed name even when the only pending change is the avatar. That blocks image updates entirely for DMs or any channel whosechannel.data?.namestarts empty.♻️ Suggested fix
const trimmedName = name.trim(); const nameChanged = trimmedName !== initialName.trim(); const imageChanged = imageEdit !== null; - const hasChanges = (trimmedName.length > 0 && nameChanged) || imageChanged; - const canSubmit = trimmedName.length > 0 && !isSaving && hasChanges; + const hasChanges = nameChanged || imageChanged; + const hasValidName = !nameChanged || trimmedName.length > 0; + const canSubmit = hasValidName && !isSaving && hasChanges;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx` around lines 200 - 204, The submit gating currently requires a non-empty trimmedName even when only the avatar changed, blocking avatar-only saves; update the logic around trimmedName/nameChanged/imageChanged/hasChanges/canSubmit so avatar edits are allowed for unnamed channels: make hasChanges reflect any name change OR image change (use nameChanged || imageChanged) and compute canSubmit as hasChanges && !isSaving && (!nameChanged || trimmedName.length > 0) so a name change still requires a non-empty name but an image-only change can submit even if trimmedName is empty (variables: trimmedName, nameChanged, imageChanged, hasChanges, canSubmit, initialName, imageEdit, isSaving, channel.data?.name).
♻️ Duplicate comments (4)
src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx (1)
171-175:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon’t capability-gate personal block/unblock.
canBlockUseris still tied to'ban-channel-members', which hides a personal block/unblock action whenever the viewer lacks channel-moderation rights. That breaks the user-level blocking contract in ordinary channels.Based on prior review context, blocking is a per-user action and should not depend on channel moderation capabilities.Proposed fix
return { - canBlockUser: - !isCurrentUser && - !!targetUserId && - ownCapabilities?.includes('ban-channel-members'), + canBlockUser: !isCurrentUser && !!targetUserId, canMuteUser: !isCurrentUser && !!targetUserId, canRemoveUser: !isCurrentUser &&🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx` around lines 171 - 175, The canBlockUser flag currently gates personal block/unblock on the 'ban-channel-members' capability; remove that capability check so blocking is available to non-current users regardless of channel moderation rights. Update the expression that computes canBlockUser (referenced as canBlockUser, isCurrentUser, targetUserId, ownCapabilities) to only require !isCurrentUser and !!targetUserId (drop ownCapabilities?.includes('ban-channel-members')), ensuring personal block/unblock is not tied to channel moderation capabilities.src/plugins/ChannelDetail/ChannelDetailSearchInput.tsx (1)
18-20:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReset the upstream query when
resetKeychanges.The effect only clears the local input state, so consumers can be left searching with the previous query while the field looks empty. Call
onSearchChange('')here as well to keep the visible input and the active search source in sync.🔄 Proposed fix
useEffect(() => { setSearchInput(''); + onSearchChange(''); }, [resetKey]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/ChannelDetailSearchInput.tsx` around lines 18 - 20, The effect watching resetKey currently only calls setSearchInput('') and leaves the upstream query unchanged; update the useEffect in ChannelDetailSearchInput (the effect that runs on resetKey) to also call onSearchChange('') so the external search state is reset in sync with the input; ensure you reference the existing setSearchInput and onSearchChange functions and keep resetKey as the effect dependency.src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx (1)
151-158:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd an accessible name to the media viewer modal.
This dialog still opens without
aria-label/aria-labelledby, so screen readers get an unnamed modal.♿ Suggested fix
<Modal + aria-label={t('Media viewer')} className={clsx( 'str-chat__gallery-modal', 'str-chat__channel-detail__media-view__viewer', )} onClose={closeViewer} open={viewerOpen} >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx` around lines 151 - 158, The Modal used in ChannelMediaView (the Modal element with className using clsx and props onClose={closeViewer} open={viewerOpen}) lacks an accessible name; add an accessible name by passing either an aria-label (e.g. aria-label="Media viewer") or aria-labelledby that points to the modal title element’s id (create a visible heading inside the modal and set its id, then set aria-labelledby on the Modal) so screen readers announce the dialog.src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx (1)
112-137:⚠️ Potential issue | 🟠 Major | ⚡ Quick winClear the saving state on the success path too.
isSavingis still only reset incatch. If this view stays mounted afteraddMembers()succeeds, the footer button remains disabled and the previous selection persists.💡 Suggested fix
const handleSave = async () => { if (!canManageChannelMembers || !selectedUserIds.length || isSaving) return; + const addedCount = selectedUserIds.length; setIsSaving(true); try { await channel.addMembers(selectedUserIds); - onMembersAdded(selectedUserIds.length); + setSelectedUserIds([]); + setIsSaving(false); + onMembersAdded(addedCount); addNotification({ context: { channel }, emitter: 'ChannelMembersView', - message: t('{{ count }} members added', { count: selectedUserIds.length }), + message: t('{{ count }} members added', { count: addedCount }), severity: 'success', type: 'api:channel:addMembers:success', }); } catch (error) { setIsSaving(false);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx` around lines 112 - 137, The success path in handleSave never clears the saving flag, leaving isSaving true and the footer button disabled; after awaiting channel.addMembers and before returning, call setIsSaving(false) (or move setIsSaving(false) into a finally block) so the saving state is reset on success; optionally also clear the selection (e.g., reset selectedUserIds via its setter) after onMembersAdded if you want the previous selection to be cleared.
🧹 Nitpick comments (2)
src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsx (1)
62-63: ⚡ Quick winWrap this test helper with the repo’s standard Chat/Channel stack.
Lines 62-63 render through
ChannelDetailProvideronly, so the suite is not exercising the provider wiring the repo requires for component tests. As per coding guidelines,**/__tests__/**/*.{ts,tsx}: Use mock-builders pattern for tests: getTestClientWithUser(), useMockedApis(), getOrCreateChannelApi(), set connectionId and wsPromiseandWrap component tests with , , and relevant sub-components; use channel.watch() after setup`.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsx` around lines 62 - 63, The test helper renderWithChannel should be updated to use the repo's standard Chat/Channel stack and mock-builders: replace the direct ChannelDetailProvider render with wiring that creates a mocked client via getTestClientWithUser() and useMockedApis(), obtains/creates the channel via getOrCreateChannelApi(), sets the client's connectionId and wsPromise, wraps the UI in <Chat> and <Channel> providers (or the repo's ChannelDetail/Chat wrappers) and ensures channel.watch() is called after setup; keep the helper named renderWithChannel and reference ChannelDetailProvider only if still needed inside the full Chat/Channel wrapper so tests exercise the required provider wiring.Source: Coding guidelines
src/plugins/ChannelDetail/Views/ChannelMemberDetailView/__tests__/ChannelMemberDetail.test.tsx (1)
53-145: 🏗️ Heavy liftUse the repository’s standard Channel test harness in
src/plugins/ChannelDetail/Views/ChannelMemberDetailView/__tests__/ChannelMemberDetail.test.tsx,src/plugins/ChannelDetail/Views/ChannelMediaView/__tests__/ChannelMediaView.test.tsx, andsrc/plugins/ChannelDetail/Views/ChannelFilesView/__tests__/ChannelFilesView.test.tsx.These three new suites share the same root cause: they mock context/store hooks directly instead of using the repo’s required mock-builders setup and
<Chat>/<Channel>wrappers, so they do not exercise the production provider wiring consistently.As per coding guidelines,
**/__tests__/**/*.{ts,tsx}:Use mock-builders pattern for tests: getTestClientWithUser(), useMockedApis(), getOrCreateChannelApi(), set connectionId and wsPromiseandWrap component tests with <Chat>, <Channel>, and relevant sub-components; use channel.watch() after setup.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/plugins/ChannelDetail/Views/ChannelMemberDetailView/__tests__/ChannelMemberDetail.test.tsx` around lines 53 - 145, Tests currently bypass the repo's standard test harness by mocking context hooks and using ChannelDetailProvider directly (renderWithChannel, createChannel, ChannelMemberDetail); replace that with the repository mock-builders and wrappers: use getTestClientWithUser(), useMockedApis(), getOrCreateChannelApi() and ensure connectionId and wsPromise are set, create/watch the channel via channel.watch() after setup, then wrap the rendered component with <Chat> and <Channel> (instead of ChannelDetailProvider) so production provider wiring is exercised; remove the direct vi.mocked(useChatContext|useModalContext|useComponentContext) overrides and ensure tests still pass the same props (e.g., channelMemberActionSet, member) to ChannelMemberDetail.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/vite/src/index.scss`:
- Line 16: The import uses url(...) which violates the repo's quoted-import
notation; replace the `@import`
url('stream-chat-react/dist/css/channel-detail.css') layer(stream-new-plugins);
statement in examples/vite/src/index.scss with the quoted form (keep the same
module path and the layer(stream-new-plugins) suffix) so it reads as a quoted
import without the url(...) wrapper.
In `@package.json`:
- Line 194: The "build-styling" npm script currently uses a semicolon which lets
the cp step run even if the preceding sass compilation fails; update the script
(the "build-styling" entry in package.json) to chain commands so the copy only
runs on successful sass compile (e.g., replace the semicolon separator with a
conditional chain such as && or use a small shell conditional) to ensure a
failed Sass compile prevents the cp step and the script exits non‑zero.
---
Outside diff comments:
In `@src/plugins/ChannelDetail/ChannelDetail.tsx`:
- Around line 49-87: The nav button labels are hard-coded English strings in
ChannelManagementNavButton, ChannelMembersNavButton, PinnedMessagesNavButton,
ChannelMediaNavButton, and ChannelFilesNavButton; replace those title props with
translated strings by calling useTranslationContext() (or the project's
translation hook) to get t and pass t('...') for each label, and add the new
keys ("Channel info", "Members", "Pinned messages", "Photos & videos", "Files")
to the src/i18n/ resource files so translations are available. Ensure you
import/use useTranslationContext() in ChannelDetail.tsx and use the same keys in
the i18n JSON/TS files.
In `@src/plugins/ChannelDetail/ChannelDetailNavButton.tsx`:
- Around line 27-33: The rootProps object in ChannelDetailNavButton (created via
useMemo) is missing an explicit button type, which causes the rendered <button>
to default to type="submit" when nested in a form; update the rootProps (the
object returned in the useMemo where 'aria-current', className, and onClick are
set) to include type: 'button' so the button will not submit surrounding forms
(ensure you add this alongside the existing className and onClick entries that
reference select).
In
`@src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsx`:
- Around line 48-62: The icon components BlockUserActionIcon,
DeleteChatActionIcon, and LeaveChannelActionIcon include className strings that
start with a literal '.' which will render into the DOM and break CSS selectors;
edit these components to remove the leading '.' from their className values
(e.g., change '.str-chat__icon--destructive ...' to 'str-chat__icon--destructive
...') so the correct CSS classes are applied; leave MuteActionIcon and
MutedActionIcon as-is since they already use correct class names.
In
`@src/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsx`:
- Around line 68-74: The memoized otherMemberUserId (useMemo in
ChannelManagementView) only depends on channel and thus won't recompute when
channel.state.members hydrates; update the dependency array to include the
members collection so the find runs when members change (e.g. add
channel.state?.members or Object.values(channel.state?.members) to the
dependency array alongside client.user?.id and resolvedIsDmChannel) so
otherMemberUserId updates when DM members hydrate.
- Around line 200-204: The submit gating currently requires a non-empty
trimmedName even when only the avatar changed, blocking avatar-only saves;
update the logic around
trimmedName/nameChanged/imageChanged/hasChanges/canSubmit so avatar edits are
allowed for unnamed channels: make hasChanges reflect any name change OR image
change (use nameChanged || imageChanged) and compute canSubmit as hasChanges &&
!isSaving && (!nameChanged || trimmedName.length > 0) so a name change still
requires a non-empty name but an image-only change can submit even if
trimmedName is empty (variables: trimmedName, nameChanged, imageChanged,
hasChanges, canSubmit, initialName, imageEdit, isSaving, channel.data?.name).
In
`@src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx`:
- Around line 95-100: The icon className strings in BlockUserActionIcon and
RemoveUserActionIcon incorrectly include leading dots (treating them like CSS
selectors), causing classes like ".str-chat__icon--destructive" to be rendered
literally and styles to not apply; remove the leading '.' from each class token
in the className props for IconNoSign and IconUserRemove so they use plain
tokens (e.g., "str-chat__icon--destructive str-chat__channel-detail__action-icon
...") to restore styling.
In
`@src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsx`:
- Around line 93-110: The member rows are rendered as interactive buttons even
when onMemberSelect is undefined; update the ListItemLayout usage so that when
onMemberSelect is falsy you do not render a button: choose a non-interactive
element (e.g., set RootElement='div' or omit RootElement) and remove
rootProps.onClick, aria-label, and any keyboard/tab props in that branch; keep
the existing button/aria-label/onClick only when onMemberSelect is provided
(references: ListItemLayout, RootElement, rootProps.onClick, aria-label,
onMemberSelect).
---
Duplicate comments:
In `@src/plugins/ChannelDetail/ChannelDetailSearchInput.tsx`:
- Around line 18-20: The effect watching resetKey currently only calls
setSearchInput('') and leaves the upstream query unchanged; update the useEffect
in ChannelDetailSearchInput (the effect that runs on resetKey) to also call
onSearchChange('') so the external search state is reset in sync with the input;
ensure you reference the existing setSearchInput and onSearchChange functions
and keep resetKey as the effect dependency.
In `@src/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsx`:
- Around line 151-158: The Modal used in ChannelMediaView (the Modal element
with className using clsx and props onClose={closeViewer} open={viewerOpen})
lacks an accessible name; add an accessible name by passing either an aria-label
(e.g. aria-label="Media viewer") or aria-labelledby that points to the modal
title element’s id (create a visible heading inside the modal and set its id,
then set aria-labelledby on the Modal) so screen readers announce the dialog.
In
`@src/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsx`:
- Around line 171-175: The canBlockUser flag currently gates personal
block/unblock on the 'ban-channel-members' capability; remove that capability
check so blocking is available to non-current users regardless of channel
moderation rights. Update the expression that computes canBlockUser (referenced
as canBlockUser, isCurrentUser, targetUserId, ownCapabilities) to only require
!isCurrentUser and !!targetUserId (drop
ownCapabilities?.includes('ban-channel-members')), ensuring personal
block/unblock is not tied to channel moderation capabilities.
In
`@src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsx`:
- Around line 112-137: The success path in handleSave never clears the saving
flag, leaving isSaving true and the footer button disabled; after awaiting
channel.addMembers and before returning, call setIsSaving(false) (or move
setIsSaving(false) into a finally block) so the saving state is reset on
success; optionally also clear the selection (e.g., reset selectedUserIds via
its setter) after onMembersAdded if you want the previous selection to be
cleared.
---
Nitpick comments:
In
`@src/plugins/ChannelDetail/Views/ChannelMemberDetailView/__tests__/ChannelMemberDetail.test.tsx`:
- Around line 53-145: Tests currently bypass the repo's standard test harness by
mocking context hooks and using ChannelDetailProvider directly
(renderWithChannel, createChannel, ChannelMemberDetail); replace that with the
repository mock-builders and wrappers: use getTestClientWithUser(),
useMockedApis(), getOrCreateChannelApi() and ensure connectionId and wsPromise
are set, create/watch the channel via channel.watch() after setup, then wrap the
rendered component with <Chat> and <Channel> (instead of ChannelDetailProvider)
so production provider wiring is exercised; remove the direct
vi.mocked(useChatContext|useModalContext|useComponentContext) overrides and
ensure tests still pass the same props (e.g., channelMemberActionSet, member) to
ChannelMemberDetail.
In
`@src/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsx`:
- Around line 62-63: The test helper renderWithChannel should be updated to use
the repo's standard Chat/Channel stack and mock-builders: replace the direct
ChannelDetailProvider render with wiring that creates a mocked client via
getTestClientWithUser() and useMockedApis(), obtains/creates the channel via
getOrCreateChannelApi(), sets the client's connectionId and wsPromise, wraps the
UI in <Chat> and <Channel> providers (or the repo's ChannelDetail/Chat wrappers)
and ensures channel.watch() is called after setup; keep the helper named
renderWithChannel and reference ChannelDetailProvider only if still needed
inside the full Chat/Channel wrapper so tests exercise the required provider
wiring.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5d3179ed-4fc8-4e7d-a971-50ffb65595fb
📒 Files selected for processing (70)
examples/vite/package.jsonexamples/vite/src/AppSettings/tabs/ChannelDetail/channelDetailSettings.tsexamples/vite/src/ChatLayout/ConfiguredChannelDetail.tsxexamples/vite/src/index.scsspackage.jsonscripts/watch-styling.mjssrc/components/Avatar/styling/index.scsssrc/components/index.tssrc/plugins/ChannelDetail/AvatarWithChannelDetail.tsxsrc/plugins/ChannelDetail/ChannelDetail.tsxsrc/plugins/ChannelDetail/ChannelDetailContext.tsxsrc/plugins/ChannelDetail/ChannelDetailEmptyList.tsxsrc/plugins/ChannelDetail/ChannelDetailListLoadingIndicator.tsxsrc/plugins/ChannelDetail/ChannelDetailNavButton.tsxsrc/plugins/ChannelDetail/ChannelDetailSearchInput.tsxsrc/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesEmptyList.tsxsrc/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.tsxsrc/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.utils.tssrc/plugins/ChannelDetail/Views/ChannelFilesView/__tests__/ChannelFilesView.test.tsxsrc/plugins/ChannelDetail/Views/ChannelFilesView/index.tssrc/plugins/ChannelDetail/Views/ChannelFilesView/useChannelFilesSearch.tssrc/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementActions.defaults.tsxsrc/plugins/ChannelDetail/Views/ChannelManagementView/ChannelManagementView.tsxsrc/plugins/ChannelDetail/Views/ChannelManagementView/index.tssrc/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaEmptyList.tsxsrc/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.tsxsrc/plugins/ChannelDetail/Views/ChannelMediaView/ChannelMediaView.utils.tssrc/plugins/ChannelDetail/Views/ChannelMediaView/__tests__/ChannelMediaView.test.tsxsrc/plugins/ChannelDetail/Views/ChannelMediaView/index.tssrc/plugins/ChannelDetail/Views/ChannelMediaView/useChannelMediaSearch.tssrc/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberActions.defaults.tsxsrc/plugins/ChannelDetail/Views/ChannelMemberDetailView/ChannelMemberDetail.tsxsrc/plugins/ChannelDetail/Views/ChannelMemberDetailView/__tests__/ChannelMemberDetail.test.tsxsrc/plugins/ChannelDetail/Views/ChannelMemberDetailView/index.tssrc/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersAddView.tsxsrc/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersBrowseView.tsxsrc/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersHeaderActions.defaults.tsxsrc/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersRemoveView.tsxsrc/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.tsxsrc/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.utils.tssrc/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersAddView.test.tsxsrc/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersBrowseView.test.tsxsrc/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersHeaderActions.defaults.test.tsxsrc/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersRemoveView.test.tsxsrc/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersView.test.tsxsrc/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/ChannelMembersView.utils.test.tssrc/plugins/ChannelDetail/Views/ChannelMembersView/__tests__/testUtils.tsxsrc/plugins/ChannelDetail/Views/ChannelMembersView/index.tssrc/plugins/ChannelDetail/Views/ChannelMembersView/useChannelMembersSearch.tssrc/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesEmptyList.tsxsrc/plugins/ChannelDetail/Views/PinnedMessagesView/PinnedMessagesView.tsxsrc/plugins/ChannelDetail/Views/PinnedMessagesView/__tests__/PinnedMessagesView.test.tsxsrc/plugins/ChannelDetail/Views/PinnedMessagesView/index.tssrc/plugins/ChannelDetail/Views/PinnedMessagesView/usePinnedMessagesSearch.tssrc/plugins/ChannelDetail/__tests__/ChannelDetail.test.tsxsrc/plugins/ChannelDetail/__tests__/ChannelManagementActions.defaults.test.tsxsrc/plugins/ChannelDetail/__tests__/ChannelManagementView.test.tsxsrc/plugins/ChannelDetail/index.tssrc/plugins/ChannelDetail/styling/AvatarWithChannelDetail.scsssrc/plugins/ChannelDetail/styling/ChannelDetail.scsssrc/plugins/ChannelDetail/styling/ChannelFilesView.scsssrc/plugins/ChannelDetail/styling/ChannelManagementView.scsssrc/plugins/ChannelDetail/styling/ChannelMediaView.scsssrc/plugins/ChannelDetail/styling/ChannelMemberDetailView.scsssrc/plugins/ChannelDetail/styling/ChannelMembersView.scsssrc/plugins/ChannelDetail/styling/ChannelMembersViewListFooter.scsssrc/plugins/ChannelDetail/styling/PinnedMessagesView.scsssrc/plugins/ChannelDetail/styling/index.scsssrc/styling/index.scssvite.config.ts
💤 Files with no reviewable changes (26)
- src/plugins/ChannelDetail/styling/AvatarWithChannelDetail.scss
- src/plugins/ChannelDetail/Views/ChannelManagementView/index.ts
- src/plugins/ChannelDetail/styling/ChannelMembersViewListFooter.scss
- src/components/Avatar/styling/index.scss
- src/plugins/ChannelDetail/Views/PinnedMessagesView/index.ts
- src/components/index.ts
- src/plugins/ChannelDetail/Views/ChannelFilesView/useChannelFilesSearch.ts
- src/plugins/ChannelDetail/Views/ChannelMediaView/index.ts
- src/plugins/ChannelDetail/Views/ChannelMembersView/ChannelMembersView.utils.ts
- src/plugins/ChannelDetail/Views/ChannelMembersView/index.ts
- src/plugins/ChannelDetail/styling/ChannelDetail.scss
- src/plugins/ChannelDetail/Views/ChannelMembersView/tests/ChannelMembersView.utils.test.ts
- src/plugins/ChannelDetail/styling/ChannelFilesView.scss
- src/plugins/ChannelDetail/ChannelDetailContext.tsx
- src/plugins/ChannelDetail/Views/PinnedMessagesView/usePinnedMessagesSearch.ts
- src/plugins/ChannelDetail/Views/ChannelMemberDetailView/index.ts
- src/plugins/ChannelDetail/styling/ChannelMemberDetailView.scss
- src/plugins/ChannelDetail/Views/ChannelFilesView/ChannelFilesView.utils.ts
- src/plugins/ChannelDetail/Views/ChannelFilesView/index.ts
- src/plugins/ChannelDetail/Views/ChannelMembersView/useChannelMembersSearch.ts
- src/plugins/ChannelDetail/styling/ChannelMembersView.scss
- src/plugins/ChannelDetail/Views/ChannelMediaView/useChannelMediaSearch.ts
- src/plugins/ChannelDetail/Views/ChannelMembersView/tests/testUtils.tsx
- src/plugins/ChannelDetail/styling/ChannelMediaView.scss
- src/plugins/ChannelDetail/styling/ChannelManagementView.scss
- src/plugins/ChannelDetail/styling/PinnedMessagesView.scss
✅ Files skipped from review due to trivial changes (4)
- examples/vite/package.json
- src/plugins/ChannelDetail/index.ts
- src/plugins/ChannelDetail/styling/index.scss
- src/plugins/ChannelDetail/AvatarWithChannelDetail.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- examples/vite/src/AppSettings/tabs/ChannelDetail/channelDetailSettings.ts
- examples/vite/src/ChatLayout/ConfiguredChannelDetail.tsx
| @import url('./AccessibilityNavigation/ReturnToSkipNavigation.scss') | ||
| layer(stream-app-overrides); | ||
| @import url('stream-chat-react/dist/css/emoji-picker.css') layer(stream-new-plugins); | ||
| @import url('stream-chat-react/dist/css/channel-detail.css') layer(stream-new-plugins); |
There was a problem hiding this comment.
Rewrite this import to the repo’s expected notation.
Stylelint is already flagging this line: the new import uses url(...), but this file is configured to require quoted import notation. That will keep the examples stylesheet in a failing state.
Proposed fix
-@import url('stream-chat-react/dist/css/channel-detail.css') layer(stream-new-plugins);
+@import 'stream-chat-react/dist/css/channel-detail.css' layer(stream-new-plugins);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @import url('stream-chat-react/dist/css/channel-detail.css') layer(stream-new-plugins); | |
| `@import` 'stream-chat-react/dist/css/channel-detail.css' layer(stream-new-plugins); |
🧰 Tools
🪛 Stylelint (17.12.0)
[error] 16-16: Expected "url('stream-chat-react/dist/css/channel-detail.css')" to be "'stream-chat-react/dist/css/channel-detail.css'" (import-notation)
(import-notation)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/vite/src/index.scss` at line 16, The import uses url(...) which
violates the repo's quoted-import notation; replace the `@import`
url('stream-chat-react/dist/css/channel-detail.css') layer(stream-new-plugins);
statement in examples/vite/src/index.scss with the quoted form (keep the same
module path and the layer(stream-new-plugins) suffix) so it reads as a quoted
import without the url(...) wrapper.
Sources: Coding guidelines, Linters/SAST tools
| "clean": "rm -rf dist", | ||
| "build": "yarn clean && concurrently 'yarn build-translations' 'vite build' 'tsc --project tsconfig.lib.json' 'yarn build-styling'", | ||
| "build-styling": "sass src/styling/index.scss:dist/css/index.css src/styling/_emoji-replacement.scss:dist/css/emoji-replacement.css src/plugins/Emojis/styling/index.scss:dist/css/emoji-picker.css; cp -r src/styling/assets dist/css/assets", | ||
| "build-styling": "sass src/styling/index.scss:dist/css/index.css src/styling/_emoji-replacement.scss:dist/css/emoji-replacement.css src/plugins/Emojis/styling/index.scss:dist/css/emoji-picker.css src/plugins/ChannelDetail/styling/index.scss:dist/css/channel-detail.css; cp -r src/styling/assets dist/css/assets", |
There was a problem hiding this comment.
Don’t mask Sass failures in build-styling.
Using ; here means cp still runs after a Sass compile error, and the script can exit successfully with only the copy step’s status. That can publish a broken channel-detail.css while yarn build still passes.
Proposed fix
- "build-styling": "sass src/styling/index.scss:dist/css/index.css src/styling/_emoji-replacement.scss:dist/css/emoji-replacement.css src/plugins/Emojis/styling/index.scss:dist/css/emoji-picker.css src/plugins/ChannelDetail/styling/index.scss:dist/css/channel-detail.css; cp -r src/styling/assets dist/css/assets",
+ "build-styling": "sass src/styling/index.scss:dist/css/index.css src/styling/_emoji-replacement.scss:dist/css/emoji-replacement.css src/plugins/Emojis/styling/index.scss:dist/css/emoji-picker.css src/plugins/ChannelDetail/styling/index.scss:dist/css/channel-detail.css && cp -r src/styling/assets dist/css/assets",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "build-styling": "sass src/styling/index.scss:dist/css/index.css src/styling/_emoji-replacement.scss:dist/css/emoji-replacement.css src/plugins/Emojis/styling/index.scss:dist/css/emoji-picker.css src/plugins/ChannelDetail/styling/index.scss:dist/css/channel-detail.css; cp -r src/styling/assets dist/css/assets", | |
| "build-styling": "sass src/styling/index.scss:dist/css/index.css src/styling/_emoji-replacement.scss:dist/css/emoji-replacement.css src/plugins/Emojis/styling/index.scss:dist/css/emoji-picker.css src/plugins/ChannelDetail/styling/index.scss:dist/css/channel-detail.css && cp -r src/styling/assets dist/css/assets", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@package.json` at line 194, The "build-styling" npm script currently uses a
semicolon which lets the cp step run even if the preceding sass compilation
fails; update the script (the "build-styling" entry in package.json) to chain
commands so the copy only runs on successful sass compile (e.g., replace the
semicolon separator with a conditional chain such as && or use a small shell
conditional) to ensure a failed Sass compile prevents the cp step and the script
exits non‑zero.
| import { | ||
| SECTION_NAVIGATOR_LAYOUT, | ||
| SectionNavigator, | ||
| type SectionNavigatorLayout, | ||
| type SectionNavigatorNavButtonProps, | ||
| type SectionNavigatorProps, | ||
| type SectionNavigatorSection, | ||
| } from '../../components/SectionNavigator'; |
There was a problem hiding this comment.
Should these components be bundled together with the plugin? Do they have usage outside of ChannelDetails?
| export * from './Poll'; | ||
| export * from './Reactions'; | ||
| export * from './SafeAnchor'; | ||
| export * from './SectionNavigator'; |
| import React, { type ComponentType, useMemo } from 'react'; | ||
|
|
||
| import type { SectionNavigatorNavButtonProps } from '../../components/SectionNavigator'; | ||
| import { ListItemLayout } from '../../components/ListItemLayout'; |
There was a problem hiding this comment.
Maybe bundle this one together with the plugin too.
| /> | ||
| ); | ||
|
|
||
| export const defaultChannelDetailSections: SectionNavigatorSection[] = [ |
There was a problem hiding this comment.
By watching the demo video attached to this PR, I feel like we'll benefit from some additional "navigation header" in the mobile view. Maybe the Channel Header can sit above the navigation items?
What do you think @jurgenploeger?
🎯 Goal
Add a complete, composable Channel Detail experience to the React SDK — a single
ChannelDetailsurface that lets users inspect and manage a channel (info, members, pinned messages, shared media and files) and that works both as a docked panel and as a responsive modal. The feature is built on reusable layout primitives (SectionNavigator,ListItemLayout,Prompt,FileIcon) so the same building blocks can be reused across the SDK and by integrators.Closes REACT-832
Depends on GetStream/stream-chat-js#1770
🛠 Implementation details
New top-level component:
ChannelDetailChannelDetail+ChannelDetailContextorchestrate a set of views through a newSectionNavigator, which switches between an inline (single-column, hamburger-driven drawer) and tabs/docked layout based on available width (viaResizeObserver), exposing itslayoutstate to children.ChannelManagementView— channel info + an edit mode (rename, avatar upload/delete), membership state, and destructive actions (leave/delete, block/unblock) gated behind confirmation dialogs. TheSavebutton only renders when there are unsaved changes.ChannelMembersView— browse / add / remove members with paginated search and pluggable header actions; guards against firing a search whenmember_count === 0.ChannelMemberDetailView— per-member actions (send DM, mute/unmute, block/unblock, remove).PinnedMessagesView— paginated pinned messages with its own timestamp formatting key; guards against searching when there are no pinned messages.ChannelMediaView— images/videos grid with avatars, video durations, and a full-screen viewer.ChannelFilesView— files/audio grouped by month, with per-typeFileIcons (scraped attachments and images filtered out).Search & pagination
stream-chatsearch sources (MessageSearchSource,ChannelMemberSearchSource) via dedicated hooks (useChannelMediaSearch,useChannelFilesSearch,useChannelMembersSearch,usePinnedMessagesSearch).Reusable primitives & shared changes
SectionNavigator+SectionNavigatorHeader(generic header with a leading hamburger slot, back button, title/description, trailing controls) — also adopted by the demoAppSettingstabs.ListItemLayout— now wraps an outer spacing buffer around an inner interactive container so the focus ring (2px offset) clears adjacent rows/overflow ancestors; the inner container is thebutton/a.Prompt(Dialog) header reworked to a robust flex/grid layout (vertically-centered first row, description on a second row, dedicated leading-content slot) and a scrollable body.FileIconmime-type mapping hardened (fallback guards +image/mapping).AvatarWithChannelDetail;ChannelAvatarnow prefers an explicit channelimageover derived member avatars.Checkbox;SwitchFieldupdates.GlobalModal,DialogPortal, andDialogManagerContextupdated accordingly.Hooks & utils
useChannelHasMembersOnline,useChannelHeaderOnlineStatus,useIsUserMuted,isDmChannel, and a smalluseChannelPreviewInfoextension.Demo (
examples/vite)AppSettingsmigrated toSectionNavigator; added a Channel Detail settings tab and made channel-detail actions configurable.RDzu.webm
Summary by CodeRabbit
Release Notes
New Features
Improvements
Bug Fixes
Internationalization