Skip to content
Merged

v10.1.0 #1762

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions docusaurus/docs/React/custom-code-examples/channel-search.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className='app-menu__container'>
<ul className='app-menu__item-list'>
<li className='app-menu__item' onClick={handleSelect}>Profile</li>
<li className='app-menu__item' onClick={handleSelect}>New Group</li>
<li className='app-menu__item' onClick={handleSelect}>Sign Out</li>
</ul>
</div>
);
}
```

```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 = () => (
<Chat client={chatClient}>
<ChannelList
// highlight-next-line
additionalChannelSearchProps={{AppMenu}}
showChannelSearch
/>
<Channel>
<Window>
<ChannelHeader />
<MessageList />
<MessageInput />
</Window>
<Thread />
</Channel>
</Chat>
);
```
8 changes: 4 additions & 4 deletions docusaurus/docs/React/utility-components/channel-search.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/components/ChannelList/hooks/usePaginatedChannels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const usePaginatedChannels = <
const [channels, setChannels] = useState<Array<Channel<StreamChatGenerics>>>([]);
const [hasNextPage, setHasNextPage] = useState(true);

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

Expand Down
10 changes: 8 additions & 2 deletions src/components/ChannelSearch/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLButtonElement>;
Expand Down Expand Up @@ -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<AppMenuProps>;
/** 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. */
Expand Down Expand Up @@ -133,6 +137,8 @@ export const SearchBar = (props: SearchBarProps) => {
inputProps.inputRef.current?.focus();
}, []);

const closeAppMenu = useCallback(() => setMenuIsOpen(false), []);

return (
<div className='str-chat__channel-search-bar' data-testid='search-bar' ref={searchBarRef}>
{inputIsFocused ? (
Expand Down Expand Up @@ -172,7 +178,7 @@ export const SearchBar = (props: SearchBarProps) => {
</div>
{menuIsOpen && AppMenu && (
<div ref={appMenuRef}>
<AppMenu />
<AppMenu close={closeAppMenu} />
</div>
)}
</div>
Expand Down
31 changes: 30 additions & 1 deletion src/components/ChannelSearch/__tests__/SearchBar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ jest.spyOn(window, 'getComputedStyle').mockReturnValue({
let client;
const inputText = new Date().getTime().toString();

const AppMenu = () => <div>AppMenu</div>;
const AppMenu = ({ close }) => (
<div>
AppMenu
<div data-testid='menu-item' onClick={close} />
</div>
);
const ClearInputIcon = () => <div>CustomClearInputIcon</div>;
const MenuIcon = () => <div>CustomMenuIcon</div>;
const SearchInputIcon = () => <div>CustomSearchInputIcon</div>;
Expand Down Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/components/ChannelSearch/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './ChannelSearch';
export * from './SearchBar';
export * from './SearchInput';
export * from './SearchResults';
export * from './utils';
9 changes: 6 additions & 3 deletions src/components/MessageList/VirtualizedMessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ const VirtualizedMessageListWithContext = <
const groupStylesFn = groupStyles || getGroupStyles;
const messageGroupStyles = useMemo(
() =>
processedMessages.reduce((acc, message, i) => {
processedMessages.reduce<Record<string, GroupStyle>>((acc, message, i) => {
const style = groupStylesFn(
message,
processedMessages[i - 1],
Expand All @@ -205,7 +205,8 @@ const VirtualizedMessageListWithContext = <
);
if (style) acc[message.id] = style;
return acc;
}, {} as Record<string, GroupStyle>),
}, {}),
// processedMessages were incorrectly rebuilt with a new object identity at some point, hence the .length usage
[processedMessages.length, shouldGroupByUser],
);

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
]);

Expand Down
4 changes: 2 additions & 2 deletions src/components/MessageList/hooks/useEnrichedMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const useEnrichedMessages = <
const groupStylesFn = groupStyles || getGroupStyles;
const messageGroupStyles = useMemo(
() =>
messagesWithDates.reduce((acc, message, i) => {
messagesWithDates.reduce<Record<string, GroupStyle>>((acc, message, i) => {
const style = groupStylesFn(
message,
messagesWithDates[i - 1],
Expand All @@ -74,7 +74,7 @@ export const useEnrichedMessages = <
);
if (style) acc[message.id] = style;
return acc;
}, {} as Record<string, GroupStyle>),
}, {}),
[messagesWithDates, noGroupByUser],
);

Expand Down
8 changes: 4 additions & 4 deletions src/components/Reactions/hooks/useProcessReactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,21 @@ export const useProcessReactions = <

const latestReactionTypes = useMemo(
() =>
latestReactions.reduce((reactionTypes, { type }) => {
latestReactions.reduce<string[]>((reactionTypes, { type }) => {
if (reactionTypes.indexOf(type) === -1) {
reactionTypes.push(type);
}
return reactionTypes;
}, [] as string[]),
}, []),
[latestReactions],
);

const supportedReactionMap = useMemo(
() =>
reactionOptions.reduce((acc, { id }) => {
reactionOptions.reduce<Record<string, boolean>>((acc, { id }) => {
acc[id] = true;
return acc;
}, {} as Record<string, boolean>),
}, {}),
[reactionOptions],
);

Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down