diff --git a/docusaurus/docs/React/custom-code-examples/channel-search.mdx b/docusaurus/docs/React/custom-code-examples/channel-search.mdx
index 7490bdf8b7..6d47b9b865 100644
--- a/docusaurus/docs/React/custom-code-examples/channel-search.mdx
+++ b/docusaurus/docs/React/custom-code-examples/channel-search.mdx
@@ -339,3 +339,92 @@ const customSearchFunction = async (props: ChannelSearchFunctionParams, event: {
additionalChannelSearchProps={{searchFunction: customSearchFunction}}
/>
```
+
+### Adding menu
+
+As of the version 10.0.0, users can add app menu into the `SearchBar`. In case you would like to display menu button next to the search input, you can do that by adding [`AppMenu` component](../utility-components/channel-search.mdx/#appmenu) to the `ChannelSearch` props. The display of `AppMenu` is then toggled by clicking on the menu button. `AppMenu` can be rendered as a drop-down or even a modal. In our example we will render a drop-down menu.
+
+:::caution
+The SDK does not provide any default `AppMenu` component and so you will have to write your CSS for it to be styled correctly.
+:::
+
+```tsx
+import React, { useCallback } from 'react';
+import type { AppMenuProps } from 'stream-chat-react';
+
+import './AppMenu.scss';
+
+export const AppMenu = ({close}: AppMenuProps) => {
+
+ const handleSelect = useCallback(() => {
+ // custom logic...
+ close?.();
+ }, [close]);
+
+ return (
+
+
+ - Profile
+ - New Group
+ - Sign Out
+
+
+ );
+}
+```
+
+```scss
+.str-chat__channel-search-bar-button.str-chat__channel-search-bar-button--menu {
+ position: relative;
+}
+
+.app-menu {
+ &__container {
+ position: absolute;
+ top: 50px;
+ left: 10px;
+ background-color: white;
+ border-radius: 5px;
+ box-shadow: 0 0 8px var(--str-chat__box-shadow-color);
+ }
+
+ &__item-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ &__item {
+ list-style: none;
+ margin: 0;
+ padding: .5rem 1rem;
+
+ &:hover {
+ background-color: lightgrey;
+ cursor: pointer;
+ }
+ }
+}
+```
+
+```jsx
+import { AppMenu } from './components/AppMenu';
+
+const App = () => (
+
+
+
+
+
+
+
+
+
+
+
+);
+```
diff --git a/docusaurus/docs/React/utility-components/channel-search.mdx b/docusaurus/docs/React/utility-components/channel-search.mdx
index 2e635f4be8..19ecef47d3 100644
--- a/docusaurus/docs/React/utility-components/channel-search.mdx
+++ b/docusaurus/docs/React/utility-components/channel-search.mdx
@@ -158,11 +158,11 @@ The `ChannelSearch` offers possibility to keep the search results open meanwhile
### AppMenu
-Application menu / drop-down to be displayed when clicked on [`MenuIcon`](./#menuicon). Prop is consumed only by the [`SearchBar` component](./#searchbar). The `SearchBar` component is rendered with `themeVersion` `'2'` only. No default component provided by the SDK. The library does not provide any CSS for `AppMenu`.
+Application menu / drop-down to be displayed when clicked on [`MenuIcon`](./#menuicon). Prop is consumed only by the [`SearchBar` component](./#searchbar). The `SearchBar` component is only available with the use of the [theming v2](../theming/introduction.mdx). No default component is provided by the SDK. The library does not provide any CSS for `AppMenu`. Consult the customization tutorial on how to [add AppMenu to your application](../custom-code-examples/channel-search.mdx/#adding-menu). The component is passed a prop `close`, which is a function that can be called to hide the app menu (e.g. on menu item selection).
-| Type | Default |
-| ------------------- | ------------ |
-| `React.ComponentType` | `undefined` |
+| Type | Default |
+|-----------------------|-------------|
+| `React.ComponentType` | `undefined` |
### channelType
diff --git a/package.json b/package.json
index fa407f47c2..65c15616e4 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,7 @@
"dependencies": {
"@braintree/sanitize-url": "6.0.0",
"@popperjs/core": "^2.11.5",
- "@stream-io/stream-chat-css": "^3.1.0",
+ "@stream-io/stream-chat-css": "3.1.1",
"clsx": "^1.1.1",
"dayjs": "^1.10.4",
"emoji-mart": "3.0.1",
diff --git a/src/components/ChannelList/hooks/usePaginatedChannels.ts b/src/components/ChannelList/hooks/usePaginatedChannels.ts
index 14665451b5..4f9dc5f3d0 100644
--- a/src/components/ChannelList/hooks/usePaginatedChannels.ts
+++ b/src/components/ChannelList/hooks/usePaginatedChannels.ts
@@ -26,6 +26,7 @@ export const usePaginatedChannels = <
const [channels, setChannels] = useState>>([]);
const [hasNextPage, setHasNextPage] = useState(true);
+ // memoize props
const filterString = useMemo(() => JSON.stringify(filters), [filters]);
const sortString = useMemo(() => JSON.stringify(sort), [sort]);
diff --git a/src/components/ChannelSearch/SearchBar.tsx b/src/components/ChannelSearch/SearchBar.tsx
index 83d679767f..40aa584900 100644
--- a/src/components/ChannelSearch/SearchBar.tsx
+++ b/src/components/ChannelSearch/SearchBar.tsx
@@ -16,6 +16,10 @@ import {
} from './icons';
import { SearchInput as DefaultSearchInput, SearchInputProps } from './SearchInput';
+export type AppMenuProps = {
+ close?: () => void;
+};
+
type SearchBarButtonProps = {
className?: string;
onClick?: MouseEventHandler;
@@ -48,7 +52,7 @@ export type SearchBarController = {
export type AdditionalSearchBarProps = {
/** Application menu to be displayed when clicked on MenuIcon */
- AppMenu?: React.ComponentType;
+ AppMenu?: React.ComponentType;
/** Custom icon component used to clear the input value on click. Displayed within the search input wrapper. */
ClearInputIcon?: React.ComponentType;
/** Custom icon component used to terminate the search UI session on click. */
@@ -133,6 +137,8 @@ export const SearchBar = (props: SearchBarProps) => {
inputProps.inputRef.current?.focus();
}, []);
+ const closeAppMenu = useCallback(() => setMenuIsOpen(false), []);
+
return (
{inputIsFocused ? (
@@ -172,7 +178,7 @@ export const SearchBar = (props: SearchBarProps) => {
{menuIsOpen && AppMenu && (
)}
diff --git a/src/components/ChannelSearch/__tests__/SearchBar.test.js b/src/components/ChannelSearch/__tests__/SearchBar.test.js
index 977b4deea5..9632443b14 100644
--- a/src/components/ChannelSearch/__tests__/SearchBar.test.js
+++ b/src/components/ChannelSearch/__tests__/SearchBar.test.js
@@ -22,7 +22,12 @@ jest.spyOn(window, 'getComputedStyle').mockReturnValue({
let client;
const inputText = new Date().getTime().toString();
-const AppMenu = () => AppMenu
;
+const AppMenu = ({ close }) => (
+
+);
const ClearInputIcon = () => CustomClearInputIcon
;
const MenuIcon = () => CustomMenuIcon
;
const SearchInputIcon = () => CustomSearchInputIcon
;
@@ -301,6 +306,30 @@ describe('SearchBar', () => {
});
});
+ it('should close the app menu on menu item click', async () => {
+ await act(() => {
+ renderComponent({
+ client,
+ props: { AppMenu },
+ searchParams: { disabled: false },
+ });
+ });
+ const menuIcon = screen.queryByTestId('menu-icon');
+ await act(() => {
+ fireEvent.click(menuIcon);
+ });
+
+ const menuItem = screen.queryByTestId('menu-item');
+
+ await act(() => {
+ fireEvent.click(menuItem);
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByText('AppMenu')).not.toBeInTheDocument();
+ });
+ });
+
it.each([
[
'on click outside',
diff --git a/src/components/ChannelSearch/index.ts b/src/components/ChannelSearch/index.ts
index e8561aae22..ee626c4f79 100644
--- a/src/components/ChannelSearch/index.ts
+++ b/src/components/ChannelSearch/index.ts
@@ -1,4 +1,5 @@
export * from './ChannelSearch';
+export * from './SearchBar';
export * from './SearchInput';
export * from './SearchResults';
export * from './utils';
diff --git a/src/components/MessageList/VirtualizedMessageList.tsx b/src/components/MessageList/VirtualizedMessageList.tsx
index f1bfcaf033..1abcc1886e 100644
--- a/src/components/MessageList/VirtualizedMessageList.tsx
+++ b/src/components/MessageList/VirtualizedMessageList.tsx
@@ -196,7 +196,7 @@ const VirtualizedMessageListWithContext = <
const groupStylesFn = groupStyles || getGroupStyles;
const messageGroupStyles = useMemo(
() =>
- processedMessages.reduce((acc, message, i) => {
+ processedMessages.reduce>((acc, message, i) => {
const style = groupStylesFn(
message,
processedMessages[i - 1],
@@ -205,7 +205,8 @@ const VirtualizedMessageListWithContext = <
);
if (style) acc[message.id] = style;
return acc;
- }, {} as Record),
+ }, {}),
+ // processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage
[processedMessages.length, shouldGroupByUser],
);
@@ -234,6 +235,7 @@ const VirtualizedMessageListWithContext = <
virtuoso,
processedMessages,
setNewMessagesNotification,
+ // processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage
processedMessages.length,
hasMoreNewer,
jumpToLatestMessage,
@@ -375,8 +377,9 @@ const VirtualizedMessageListWithContext = <
return Item;
}, [
customClasses?.virtualMessage,
- Object.keys(messageGroupStyles),
+ messageGroupStyles,
numItemsPrepended,
+ // processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage
processedMessages.length,
]);
diff --git a/src/components/MessageList/hooks/useEnrichedMessages.ts b/src/components/MessageList/hooks/useEnrichedMessages.ts
index 95d24b9731..7db78b4486 100644
--- a/src/components/MessageList/hooks/useEnrichedMessages.ts
+++ b/src/components/MessageList/hooks/useEnrichedMessages.ts
@@ -65,7 +65,7 @@ export const useEnrichedMessages = <
const groupStylesFn = groupStyles || getGroupStyles;
const messageGroupStyles = useMemo(
() =>
- messagesWithDates.reduce((acc, message, i) => {
+ messagesWithDates.reduce>((acc, message, i) => {
const style = groupStylesFn(
message,
messagesWithDates[i - 1],
@@ -74,7 +74,7 @@ export const useEnrichedMessages = <
);
if (style) acc[message.id] = style;
return acc;
- }, {} as Record),
+ }, {}),
[messagesWithDates, noGroupByUser],
);
diff --git a/src/components/Reactions/hooks/useProcessReactions.tsx b/src/components/Reactions/hooks/useProcessReactions.tsx
index bc15e2e2d3..d253ebba14 100644
--- a/src/components/Reactions/hooks/useProcessReactions.tsx
+++ b/src/components/Reactions/hooks/useProcessReactions.tsx
@@ -55,21 +55,21 @@ export const useProcessReactions = <
const latestReactionTypes = useMemo(
() =>
- latestReactions.reduce((reactionTypes, { type }) => {
+ latestReactions.reduce((reactionTypes, { type }) => {
if (reactionTypes.indexOf(type) === -1) {
reactionTypes.push(type);
}
return reactionTypes;
- }, [] as string[]),
+ }, []),
[latestReactions],
);
const supportedReactionMap = useMemo(
() =>
- reactionOptions.reduce((acc, { id }) => {
+ reactionOptions.reduce>((acc, { id }) => {
acc[id] = true;
return acc;
- }, {} as Record),
+ }, {}),
[reactionOptions],
);
diff --git a/yarn.lock b/yarn.lock
index 655988bf52..5d56ee00f6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2078,10 +2078,10 @@
crypto-browserify "^3.11.0"
process-es6 "^0.11.2"
-"@stream-io/stream-chat-css@^3.1.0":
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/@stream-io/stream-chat-css/-/stream-chat-css-3.1.0.tgz#f04c71f7eff7c98801656a4c791b19f25f1114f4"
- integrity sha512-rcyKzSsA4NGbOjzdDGayFHEJNX8/a8dtwZkVVIhqaIc7lO1Zpoph3wVYByKmtQxHl1SUIw/LYJLf0SHfsTZlYg==
+"@stream-io/stream-chat-css@3.1.1":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@stream-io/stream-chat-css/-/stream-chat-css-3.1.1.tgz#c19271b12cfb5a1f62bd5e2547d7be1ab7fc49b1"
+ integrity sha512-/K8QJGeIqluDpTvCmimenZRnET/LMMmqo5a/8Wam+gupHPTOBWB5+noB3x3FlKzg0w1CuKEufdRK1BZxhQK7wA==
"@stream-io/transliterate@^1.5.5":
version "1.5.5"