Skip to content
Open
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
7 changes: 5 additions & 2 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import { humanId } from 'human-id';
import { chatViewSelectorItemSet } from './Sidebar/ChatViewSelectorItemSet.tsx';
import { useAppSettingsState } from './AppSettings';

import { Search } from 'stream-chat-react/experimental';

init({ data });

const parseUserIdFromToken = (token: string) => {
Expand Down Expand Up @@ -67,7 +69,7 @@ const options: ChannelOptions = {
state: true,
};
// pinned_at param leads to BE not returning (empty) channels
const sort: ChannelSort = { last_message_at: -1, updated_at: -1 };
const sort: ChannelSort = { pinned_at: -1, last_message_at: -1, updated_at: -1 };

// @ts-ignore
const isMessageAIGenerated = (message: LocalMessage) => !!message?.ai_generated;
Expand Down Expand Up @@ -140,6 +142,7 @@ const App = () => {
},
{ type: 'public' },
],
archived: false,
}),
[userId],
);
Expand Down Expand Up @@ -204,12 +207,12 @@ const App = () => {
/>
<ChatView.Channels>
<ChannelList
ChannelSearch={Search}
Avatar={ChannelAvatar}
filters={filters}
options={options}
sort={sort}
showChannelSearch
additionalChannelSearchProps={{ searchForChannels: true }}
/>
<Channel>
<WithDragAndDropUpload>
Expand Down
4 changes: 2 additions & 2 deletions examples/vite/src/stream-imports-layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//@use 'stream-chat-react/dist/scss/v2/ChannelHeader/ChannelHeader-layout';
//@use 'stream-chat-react/dist/scss/v2/ChannelList/ChannelList-layout';
//@use 'stream-chat-react/dist/scss/v2/ChannelPreview/ChannelPreview-layout';
@use 'stream-chat-react/dist/scss/v2/ChannelSearch/ChannelSearch-layout';
// @use 'stream-chat-react/dist/scss/v2/ChannelSearch/ChannelSearch-layout';
@use 'stream-chat-react/dist/scss/v2/common/CTAButton/CTAButton-layout';
@use 'stream-chat-react/dist/scss/v2/common/CircleFAButton/CircleFAButton-layout';
//@use 'stream-chat-react/dist/scss/v2/Dialog/Dialog-layout';
Expand All @@ -39,7 +39,7 @@
@use 'stream-chat-react/dist/scss/v2/Notification/NotificationList-layout';
@use 'stream-chat-react/dist/scss/v2/Notification/Notification-layout';
//@use 'stream-chat-react/dist/scss/v2/Poll/Poll-layout';
@use 'stream-chat-react/dist/scss/v2/Search/Search-layout';
// @use 'stream-chat-react/dist/scss/v2/Search/Search-layout';
@use 'stream-chat-react/dist/scss/v2/Thread/Thread-layout';
@use 'stream-chat-react/dist/scss/v2/Tooltip/Tooltip-layout';
@use 'stream-chat-react/dist/scss/v2/TypingIndicator/TypingIndicator-layout';
Expand Down
4 changes: 2 additions & 2 deletions examples/vite/src/stream-imports-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//@use 'stream-chat-react/dist/scss/v2/ChannelHeader/ChannelHeader-theme';
//@use 'stream-chat-react/dist/scss/v2/ChannelList/ChannelList-theme';
//@use 'stream-chat-react/dist/scss/v2/ChannelPreview/ChannelPreview-theme';
@use 'stream-chat-react/dist/scss/v2/ChannelSearch/ChannelSearch-theme';
// @use 'stream-chat-react/dist/scss/v2/ChannelSearch/ChannelSearch-theme';
//@use 'stream-chat-react/dist/scss/v2/Dialog/Dialog-theme';
//@use 'stream-chat-react/dist/scss/v2/DragAndDropContainer/DragAndDropContainer-theme';
//@use 'stream-chat-react/dist/scss/v2/DropzoneContainer/DropzoneContainer-theme';
Expand All @@ -33,7 +33,7 @@
@use 'stream-chat-react/dist/scss/v2/Notification/NotificationList-theme';
@use 'stream-chat-react/dist/scss/v2/Notification/Notification-theme';
//@use 'stream-chat-react/dist/scss/v2/Poll/Poll-theme';
@use 'stream-chat-react/dist/scss/v2/Search/Search-theme';
// @use 'stream-chat-react/dist/scss/v2/Search/Search-theme';
@use 'stream-chat-react/dist/scss/v2/Thread/Thread-theme';
@use 'stream-chat-react/dist/scss/v2/Tooltip/Tooltip-theme';
@use 'stream-chat-react/dist/scss/v2/TypingIndicator/TypingIndicator-theme';
Expand Down
105 changes: 56 additions & 49 deletions src/components/ChannelList/ChannelList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { LoadingChannels } from '../Loading/LoadingChannels';
import { LoadMorePaginator } from '../LoadMore/LoadMorePaginator';
import {
ChannelListContextProvider,
DialogManagerProvider,
useChatContext,
useComponentContext,
} from '../../context';
Expand All @@ -43,6 +44,7 @@ import type { ChannelAvatarProps } from '../Avatar';
import type { TranslationContextValue } from '../../context/TranslationContext';
import type { PaginatorProps } from '../../types/types';
import type { LoadingErrorIndicatorProps } from '../Loading';
import { useStableId } from '../UtilityComponents/useStableId';

const DEFAULT_FILTERS = {};
const DEFAULT_OPTIONS = {};
Expand Down Expand Up @@ -226,6 +228,9 @@ const UnMemoizedChannelList = (props: ChannelListProps) => {
searchController.state,
searchControllerStateSelector,
);

const stableId = useStableId();

/**
* Set a channel with id {customActiveChannel} as active and move it to the top of the list.
* If customActiveChannel prop is absent, then set the first channel in list as active channel.
Expand Down Expand Up @@ -378,57 +383,59 @@ const UnMemoizedChannelList = (props: ChannelListProps) => {
const showChannelList =
(!searchActive && !searchIsActive) || additionalChannelSearchProps?.popupResults;
return (
<ChannelListContextProvider
value={{ channels, hasNextPage, loadNextPage, setChannels }}
>
<div className={className} ref={channelListRef}>
{showChannelSearch &&
(Search ? (
<Search
directMessagingChannelType={additionalChannelSearchProps?.channelType}
disabled={additionalChannelSearchProps?.disabled}
exitSearchOnInputBlur={
additionalChannelSearchProps?.clearSearchOnClickOutside
<DialogManagerProvider id={`channel-list-dialog-manager-${stableId}`}>
<ChannelListContextProvider
value={{ channels, hasNextPage, loadNextPage, setChannels }}
>
<div className={className} ref={channelListRef}>
{showChannelSearch &&
(Search ? (
<Search
directMessagingChannelType={additionalChannelSearchProps?.channelType}
disabled={additionalChannelSearchProps?.disabled}
exitSearchOnInputBlur={
additionalChannelSearchProps?.clearSearchOnClickOutside
}
placeholder={additionalChannelSearchProps?.placeholder}
/>
) : (
<ChannelSearch
onSearch={onSearch}
onSearchExit={onSearchExit}
setChannels={setChannels}
{...additionalChannelSearchProps}
/>
))}
{showChannelList && (
<List
error={channelsQueryState.error}
loadedChannels={sendChannelsToList ? loadedChannels : undefined}
loading={
!!channelsQueryState.queryInProgress &&
['reload', 'uninitialized'].includes(channelsQueryState.queryInProgress)
}
placeholder={additionalChannelSearchProps?.placeholder}
/>
) : (
<ChannelSearch
onSearch={onSearch}
onSearchExit={onSearchExit}
LoadingErrorIndicator={LoadingErrorIndicator}
LoadingIndicator={LoadingIndicator}
setChannels={setChannels}
{...additionalChannelSearchProps}
/>
))}
{showChannelList && (
<List
error={channelsQueryState.error}
loadedChannels={sendChannelsToList ? loadedChannels : undefined}
loading={
!!channelsQueryState.queryInProgress &&
['reload', 'uninitialized'].includes(channelsQueryState.queryInProgress)
}
LoadingErrorIndicator={LoadingErrorIndicator}
LoadingIndicator={LoadingIndicator}
setChannels={setChannels}
>
{!loadedChannels?.length ? (
<EmptyStateIndicator listType='channel' />
) : (
<Paginator
hasNextPage={hasNextPage}
isLoading={channelsQueryState.queryInProgress === 'load-more'}
loadNextPage={loadNextPage}
>
{renderChannels
? renderChannels(loadedChannels, renderChannel)
: loadedChannels.map((channel) => renderChannel(channel))}
</Paginator>
)}
</List>
)}
</div>
</ChannelListContextProvider>
>
{!loadedChannels?.length ? (
<EmptyStateIndicator listType='channel' />
) : (
<Paginator
hasNextPage={hasNextPage}
isLoading={channelsQueryState.queryInProgress === 'load-more'}
loadNextPage={loadNextPage}
>
{renderChannels
? renderChannels(loadedChannels, renderChannel)
: loadedChannels.map((channel) => renderChannel(channel))}
</Paginator>
)}
</List>
)}
</div>
</ChannelListContextProvider>
</DialogManagerProvider>
);
};

Expand Down
87 changes: 63 additions & 24 deletions src/components/ChannelPreview/ChannelPreviewActionButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import type { Channel } from 'stream-chat';
import { useChannelMembershipState } from '../ChannelList';
import { useTranslationContext } from '../../context';
import { Button } from '../Button';
import { IconArchive, IconMute, IconPin } from '../Icons';
import { IconArchive, IconDotGrid1x3Horizontal, IconMute, IconPin } from '../Icons';

import clsx from 'clsx';
import { useIsChannelMuted } from './hooks/useIsChannelMuted';
import {
ContextMenu,
ContextMenuButton,
useDialogIsOpen,
useDialogOnNearestManager,
} from '../Dialog';

export type ChannelPreviewActionButtonsProps = {
channel: Channel;
Expand All @@ -25,40 +31,33 @@ export function ChannelPreviewActionButtons({
channel.data?.member_count === 2 &&
channel.id?.startsWith('!members-');

// const buttonRef = useRef<ComponentRef<'button'>>(null);
// const dialogId = `channel-action-buttons-${channel.id}`;
// const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId });
// const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id);
const [referenceElement, setReferenceElement] =
React.useState<HTMLButtonElement | null>(null);
const dialogId = `channel-action-buttons-${channel.id}`;
const { dialog, dialogManager } = useDialogOnNearestManager({ id: dialogId });
const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager?.id);

return (
<div
className={clsx('str-chat__channel-preview__action-buttons', {
'str-chat__channel-preview__action-buttons--active': false, // dialogIsOpen,
'str-chat__channel-preview__action-buttons--active': dialogIsOpen,
})}
>
<Button
appearance='ghost'
aria-label={membership.pinned_at ? t('Unpin') : t('Pin')}
aria-pressed={typeof membership.pinned_at === 'string'}
// aria-expanded={dialogIsOpen}
// aria-pressed={dialogIsOpen}
aria-expanded={dialogIsOpen}
aria-pressed={dialogIsOpen}
circular
// onClick={() => dialog.toggle()}
// ref={buttonRef}
onClick={(e) => {
e.stopPropagation();
if (membership.pinned_at) {
channel.unpin();
} else {
channel.pin();
}

dialog.toggle();
}}
ref={setReferenceElement}
size='sm'
title={membership.pinned_at ? t('Unpin') : t('Pin')}
variant='secondary'
>
{/* <IconDotGrid1x3Horizontal /> */}
<IconPin />
<IconDotGrid1x3Horizontal />
</Button>
{isDirectMessageChannel ? (
<Button
Expand Down Expand Up @@ -102,13 +101,16 @@ export function ChannelPreviewActionButtons({
</Button>
)}

{/* <ContextMenu
{/* TODO: clean this mess up (make ContextMenu accept children) */}
<ContextMenu
className='str-chat__channel-preview__action-buttons-context-menu'
dialogManagerId={dialogManager?.id}
id={dialog.id}
items={[
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arnautov-anton maybe for ChannelPreviewActionButtons we could use the same pattern as we used for MessageActions - have default structure defined + filter hook / logic + possibility to provide custom actions.

() => (
<ContextMenuButton
aria-label={membership.pinned_at ? t('Unpin') : t('Pin')}
Icon={IconPin}
key='pin-button'
onClick={(e) => {
e.stopPropagation();
Expand All @@ -117,19 +119,56 @@ export function ChannelPreviewActionButtons({
} else {
channel.pin();
}
dialog?.close();
}}
title={membership.pinned_at ? t('Unpin') : t('Pin')}
>
{membership.pinned_at ? t('Unpin') : t('Pin')}
</ContextMenuButton>
),
() => (
<ContextMenuButton
aria-label={isMuted ? t('Unmute') : t('Mute')}
Icon={IconMute}
key='mute-button'
onClick={(e) => {
e.stopPropagation();
if (isMuted) {
channel.unmute();
} else {
channel.mute();
}
}}
title={isMuted ? t('Unmute') : t('Mute')}
>
{isMuted ? t('Unmute') : t('Mute')}
</ContextMenuButton>
),
() => (
<ContextMenuButton
aria-label={membership.archived_at ? t('Unarchive') : t('Archive')}
Icon={IconArchive}
key='archive-button'
onClick={(e) => {
e.stopPropagation();
if (membership.archived_at) {
channel.unarchive();
} else {
channel.archive();
}
}}
title={membership.archived_at ? t('Unarchive') : t('Archive')}
>
{membership.archived_at ? t('Unarchive') : t('Archive')}
</ContextMenuButton>
),
]}
onClose={dialog?.close}
placement={'bottom-end'}
referenceElement={buttonRef.current}
placement='bottom-start'
referenceElement={referenceElement}
tabIndex={-1}
trapFocus
/> */}
/>
</div>
);
}
5 changes: 5 additions & 0 deletions src/components/ChannelPreview/styling/ChannelPreview.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
padding: var(--spacing-xxs);
position: relative;

&:has(.str-chat__channel-preview__action-buttons--active),
&:hover {
.str-chat__channel-preview__action-buttons {
display: flex;
Expand Down Expand Up @@ -35,6 +36,10 @@
border-bottom: 1px solid var(--border-core-subtle);
}

.str-chat__channel-preview__action-buttons-context-menu {
min-width: 150px;
}

.str-chat__channel-preview {
display: flex;
gap: var(--spacing-md);
Expand Down
4 changes: 2 additions & 2 deletions src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ export const Chat = (props: PropsWithChildren<ChatProps>) => {
new SearchController({
sources: [
new ChannelSearchSource(client),
new UserSearchSource(client),
new MessageSearchSource(client),
// new UserSearchSource(client),
// new MessageSearchSource(client),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep message search source because it is essentially search for channels by message text. In the preview text we would show the matching text by which the item was selected to the search results.

],
}),
[client, customChannelSearchController],
Expand Down
Loading
Loading