Skip to content

Commit 871aead

Browse files
authored
Merge pull request #1762 from GetStream/develop
v10.1.0
2 parents 47c752e + 37716d5 commit 871aead

File tree

11 files changed

+150
-21
lines changed

11 files changed

+150
-21
lines changed

docusaurus/docs/React/custom-code-examples/channel-search.mdx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,92 @@ const customSearchFunction = async (props: ChannelSearchFunctionParams, event: {
339339
additionalChannelSearchProps={{searchFunction: customSearchFunction}}
340340
/>
341341
```
342+
343+
### Adding menu
344+
345+
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.
346+
347+
:::caution
348+
The SDK does not provide any default `AppMenu` component and so you will have to write your CSS for it to be styled correctly.
349+
:::
350+
351+
```tsx
352+
import React, { useCallback } from 'react';
353+
import type { AppMenuProps } from 'stream-chat-react';
354+
355+
import './AppMenu.scss';
356+
357+
export const AppMenu = ({close}: AppMenuProps) => {
358+
359+
const handleSelect = useCallback(() => {
360+
// custom logic...
361+
close?.();
362+
}, [close]);
363+
364+
return (
365+
<div className='app-menu__container'>
366+
<ul className='app-menu__item-list'>
367+
<li className='app-menu__item' onClick={handleSelect}>Profile</li>
368+
<li className='app-menu__item' onClick={handleSelect}>New Group</li>
369+
<li className='app-menu__item' onClick={handleSelect}>Sign Out</li>
370+
</ul>
371+
</div>
372+
);
373+
}
374+
```
375+
376+
```scss
377+
.str-chat__channel-search-bar-button.str-chat__channel-search-bar-button--menu {
378+
position: relative;
379+
}
380+
381+
.app-menu {
382+
&__container {
383+
position: absolute;
384+
top: 50px;
385+
left: 10px;
386+
background-color: white;
387+
border-radius: 5px;
388+
box-shadow: 0 0 8px var(--str-chat__box-shadow-color);
389+
}
390+
391+
&__item-list {
392+
list-style: none;
393+
margin: 0;
394+
padding: 0;
395+
}
396+
397+
&__item {
398+
list-style: none;
399+
margin: 0;
400+
padding: .5rem 1rem;
401+
402+
&:hover {
403+
background-color: lightgrey;
404+
cursor: pointer;
405+
}
406+
}
407+
}
408+
```
409+
410+
```jsx
411+
import { AppMenu } from './components/AppMenu';
412+
413+
const App = () => (
414+
<Chat client={chatClient}>
415+
<ChannelList
416+
// highlight-next-line
417+
additionalChannelSearchProps={{AppMenu}}
418+
showChannelSearch
419+
/>
420+
<Channel>
421+
<Window>
422+
<ChannelHeader />
423+
<MessageList />
424+
<MessageInput />
425+
</Window>
426+
<Thread />
427+
</Channel>
428+
</Chat>
429+
);
430+
```

docusaurus/docs/React/utility-components/channel-search.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,11 @@ The `ChannelSearch` offers possibility to keep the search results open meanwhile
158158

159159
### AppMenu
160160

161-
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`.
161+
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).
162162

163-
| Type | Default |
164-
| ------------------- | ------------ |
165-
| `React.ComponentType` | `undefined` |
163+
| Type | Default |
164+
|-----------------------|-------------|
165+
| `React.ComponentType` | `undefined` |
166166

167167
### channelType
168168

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"dependencies": {
3131
"@braintree/sanitize-url": "6.0.0",
3232
"@popperjs/core": "^2.11.5",
33-
"@stream-io/stream-chat-css": "^3.1.0",
33+
"@stream-io/stream-chat-css": "3.1.1",
3434
"clsx": "^1.1.1",
3535
"dayjs": "^1.10.4",
3636
"emoji-mart": "3.0.1",

src/components/ChannelList/hooks/usePaginatedChannels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const usePaginatedChannels = <
2626
const [channels, setChannels] = useState<Array<Channel<StreamChatGenerics>>>([]);
2727
const [hasNextPage, setHasNextPage] = useState(true);
2828

29+
// memoize props
2930
const filterString = useMemo(() => JSON.stringify(filters), [filters]);
3031
const sortString = useMemo(() => JSON.stringify(sort), [sort]);
3132

src/components/ChannelSearch/SearchBar.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import {
1616
} from './icons';
1717
import { SearchInput as DefaultSearchInput, SearchInputProps } from './SearchInput';
1818

19+
export type AppMenuProps = {
20+
close?: () => void;
21+
};
22+
1923
type SearchBarButtonProps = {
2024
className?: string;
2125
onClick?: MouseEventHandler<HTMLButtonElement>;
@@ -48,7 +52,7 @@ export type SearchBarController = {
4852

4953
export type AdditionalSearchBarProps = {
5054
/** Application menu to be displayed when clicked on MenuIcon */
51-
AppMenu?: React.ComponentType;
55+
AppMenu?: React.ComponentType<AppMenuProps>;
5256
/** Custom icon component used to clear the input value on click. Displayed within the search input wrapper. */
5357
ClearInputIcon?: React.ComponentType;
5458
/** Custom icon component used to terminate the search UI session on click. */
@@ -133,6 +137,8 @@ export const SearchBar = (props: SearchBarProps) => {
133137
inputProps.inputRef.current?.focus();
134138
}, []);
135139

140+
const closeAppMenu = useCallback(() => setMenuIsOpen(false), []);
141+
136142
return (
137143
<div className='str-chat__channel-search-bar' data-testid='search-bar' ref={searchBarRef}>
138144
{inputIsFocused ? (
@@ -172,7 +178,7 @@ export const SearchBar = (props: SearchBarProps) => {
172178
</div>
173179
{menuIsOpen && AppMenu && (
174180
<div ref={appMenuRef}>
175-
<AppMenu />
181+
<AppMenu close={closeAppMenu} />
176182
</div>
177183
)}
178184
</div>

src/components/ChannelSearch/__tests__/SearchBar.test.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ jest.spyOn(window, 'getComputedStyle').mockReturnValue({
2222
let client;
2323
const inputText = new Date().getTime().toString();
2424

25-
const AppMenu = () => <div>AppMenu</div>;
25+
const AppMenu = ({ close }) => (
26+
<div>
27+
AppMenu
28+
<div data-testid='menu-item' onClick={close} />
29+
</div>
30+
);
2631
const ClearInputIcon = () => <div>CustomClearInputIcon</div>;
2732
const MenuIcon = () => <div>CustomMenuIcon</div>;
2833
const SearchInputIcon = () => <div>CustomSearchInputIcon</div>;
@@ -301,6 +306,30 @@ describe('SearchBar', () => {
301306
});
302307
});
303308

309+
it('should close the app menu on menu item click', async () => {
310+
await act(() => {
311+
renderComponent({
312+
client,
313+
props: { AppMenu },
314+
searchParams: { disabled: false },
315+
});
316+
});
317+
const menuIcon = screen.queryByTestId('menu-icon');
318+
await act(() => {
319+
fireEvent.click(menuIcon);
320+
});
321+
322+
const menuItem = screen.queryByTestId('menu-item');
323+
324+
await act(() => {
325+
fireEvent.click(menuItem);
326+
});
327+
328+
await waitFor(() => {
329+
expect(screen.queryByText('AppMenu')).not.toBeInTheDocument();
330+
});
331+
});
332+
304333
it.each([
305334
[
306335
'on click outside',
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './ChannelSearch';
2+
export * from './SearchBar';
23
export * from './SearchInput';
34
export * from './SearchResults';
45
export * from './utils';

src/components/MessageList/VirtualizedMessageList.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ const VirtualizedMessageListWithContext = <
196196
const groupStylesFn = groupStyles || getGroupStyles;
197197
const messageGroupStyles = useMemo(
198198
() =>
199-
processedMessages.reduce((acc, message, i) => {
199+
processedMessages.reduce<Record<string, GroupStyle>>((acc, message, i) => {
200200
const style = groupStylesFn(
201201
message,
202202
processedMessages[i - 1],
@@ -205,7 +205,8 @@ const VirtualizedMessageListWithContext = <
205205
);
206206
if (style) acc[message.id] = style;
207207
return acc;
208-
}, {} as Record<string, GroupStyle>),
208+
}, {}),
209+
// processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage
209210
[processedMessages.length, shouldGroupByUser],
210211
);
211212

@@ -234,6 +235,7 @@ const VirtualizedMessageListWithContext = <
234235
virtuoso,
235236
processedMessages,
236237
setNewMessagesNotification,
238+
// processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage
237239
processedMessages.length,
238240
hasMoreNewer,
239241
jumpToLatestMessage,
@@ -375,8 +377,9 @@ const VirtualizedMessageListWithContext = <
375377
return Item;
376378
}, [
377379
customClasses?.virtualMessage,
378-
Object.keys(messageGroupStyles),
380+
messageGroupStyles,
379381
numItemsPrepended,
382+
// processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage
380383
processedMessages.length,
381384
]);
382385

src/components/MessageList/hooks/useEnrichedMessages.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const useEnrichedMessages = <
6565
const groupStylesFn = groupStyles || getGroupStyles;
6666
const messageGroupStyles = useMemo(
6767
() =>
68-
messagesWithDates.reduce((acc, message, i) => {
68+
messagesWithDates.reduce<Record<string, GroupStyle>>((acc, message, i) => {
6969
const style = groupStylesFn(
7070
message,
7171
messagesWithDates[i - 1],
@@ -74,7 +74,7 @@ export const useEnrichedMessages = <
7474
);
7575
if (style) acc[message.id] = style;
7676
return acc;
77-
}, {} as Record<string, GroupStyle>),
77+
}, {}),
7878
[messagesWithDates, noGroupByUser],
7979
);
8080

src/components/Reactions/hooks/useProcessReactions.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,21 @@ export const useProcessReactions = <
5555

5656
const latestReactionTypes = useMemo(
5757
() =>
58-
latestReactions.reduce((reactionTypes, { type }) => {
58+
latestReactions.reduce<string[]>((reactionTypes, { type }) => {
5959
if (reactionTypes.indexOf(type) === -1) {
6060
reactionTypes.push(type);
6161
}
6262
return reactionTypes;
63-
}, [] as string[]),
63+
}, []),
6464
[latestReactions],
6565
);
6666

6767
const supportedReactionMap = useMemo(
6868
() =>
69-
reactionOptions.reduce((acc, { id }) => {
69+
reactionOptions.reduce<Record<string, boolean>>((acc, { id }) => {
7070
acc[id] = true;
7171
return acc;
72-
}, {} as Record<string, boolean>),
72+
}, {}),
7373
[reactionOptions],
7474
);
7575

0 commit comments

Comments
 (0)