Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add general purpose search components #2588

Open
wants to merge 54 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
fc8866e
feat: first iteration of Search revamp
MartinCupela Nov 29, 2024
2152ea6
feat: add Search components to ComponentContext
MartinCupela Dec 13, 2024
f6d1b59
feat: prioritize ChannelAvatar rendering in ChannelPreview and Channe…
MartinCupela Dec 13, 2024
9dc2f5f
feat: prioritize Search rendering in ChannelList over ChannelSearch
MartinCupela Dec 13, 2024
2f8e384
feat: adapt SearchBar to a new design and control the UI with searchC…
MartinCupela Dec 13, 2024
99554ce
fix: handle search UI states well
MartinCupela Dec 13, 2024
674dd16
feat: allow to override active prop on ChannelPreview
MartinCupela Dec 17, 2024
04e03d6
feat: add jump to selected search result message
MartinCupela Dec 17, 2024
18944f9
feat: allow to override query filters in search source instances
MartinCupela Dec 17, 2024
26db687
feat: allow to search for multiple sources at the same time
MartinCupela Dec 19, 2024
ddeaca7
feat: mark search query completed once all the debounced source queri…
MartinCupela Dec 19, 2024
6a1baef
feat: remove SearchSourceResultsError component and export all search…
MartinCupela Dec 19, 2024
12f4388
refactor: move Search related components to experimental folder add S…
MartinCupela Dec 19, 2024
5ea9177
fix: prevent deactivating the last active search source
MartinCupela Dec 20, 2024
ba85fa0
feat: add search source list footer and search source results header
MartinCupela Dec 20, 2024
a8183d0
feat: add SearchSourceResultsContext
MartinCupela Dec 20, 2024
c9c4430
refactor: rename SearchSourceLoadingResults to SearchSourceResultsLoa…
MartinCupela Dec 20, 2024
5e99bf7
feat: implement own debounce and useSearchQueriesInProgress hook
MartinCupela Dec 20, 2024
c44aab3
fix: prevent infinite loop in search
MartinCupela Dec 20, 2024
50274cb
docs: remove SearchController usage example
MartinCupela Dec 20, 2024
13ff728
refactor: get rid of Sources generic and keep single active search so…
MartinCupela Jan 10, 2025
333e4a9
fix: export SearchController
MartinCupela Jan 10, 2025
764d242
refactor: move SearchController to JS client
MartinCupela Jan 14, 2025
0e6713f
refactor: rename the Avatar alias passed to ChannelHeader via props
MartinCupela Jan 14, 2025
cfe4ec8
test: fix existing tests
MartinCupela Jan 14, 2025
5c0c0d9
test: add tests for Search components
MartinCupela Jan 15, 2025
e38c060
fix: add missing effect dependencies in ChannelPreview
MartinCupela Jan 15, 2025
e014579
feat: add translations
MartinCupela Jan 16, 2025
5ae55d0
refactor: remove onSearch and onChange callbacks from Search props
MartinCupela Jan 16, 2025
9bc0575
test: add ChannelHeader tests
MartinCupela Jan 16, 2025
8ec0835
test: add ChannelList test for Search display
MartinCupela Jan 16, 2025
d8c9a94
test: add ChannelPreview tests
MartinCupela Jan 16, 2025
629d8d4
docs: fix docstring type
MartinCupela Jan 16, 2025
b7ef5e9
chore: remove fixed todos
MartinCupela Jan 16, 2025
cb0ad05
test: fix ChannelHeader and ChannelPreview tests
MartinCupela Jan 16, 2025
34f3c69
refactor: do not use SearchController to keep reference to search inp…
MartinCupela Jan 16, 2025
d57017d
Merge remote-tracking branch 'origin/master'
MartinCupela Jan 16, 2025
2076005
fix: import execSync in getPackageVersion.mjs script
MartinCupela Jan 17, 2025
f9e24eb
fix(examples): memoize isMessageAIGenerated function
MartinCupela Jan 17, 2025
fc8e7cd
fix: do not prefer ChannelAvatar from component context in ChannelHeader
MartinCupela Jan 17, 2025
32a6b37
fix: do not prefer ChannelAvatar from component context in ChannelHeader
MartinCupela Jan 17, 2025
2bfc86c
Merge remote-tracking branch 'origin/feat/search-channel-by-message-t…
MartinCupela Jan 17, 2025
6684eaf
docs: fix prop docstrings
MartinCupela Jan 20, 2025
0f8016c
refactor: upon source activation perform search request in click handler
MartinCupela Jan 28, 2025
7f06fd5
refactor: rename pagination indicator in SearchController from hasMor…
MartinCupela Jan 28, 2025
dddf226
refactor: subscribe to search controller's internal state to watch fo…
MartinCupela Jan 31, 2025
a3a4139
refactor: remove component context from Chat component
MartinCupela Jan 31, 2025
30d6d41
Merge remote-tracking branch 'origin/master'
MartinCupela Jan 31, 2025
adc82fc
refactor: rename SearchController.internalState to _internalState
MartinCupela Jan 31, 2025
eb38a26
chore(deps): upgrade stream-chat to 8.55.0 and stream-chat-css to 5.7.0
MartinCupela Jan 31, 2025
3346611
chore: allow name _internalState for eslint
MartinCupela Jan 31, 2025
634de23
chore(deps): remove @stream-io/rollup-plugin-node-builtins
MartinCupela Jan 31, 2025
30746c0
refactor: remove ChannelAvatar key from component context
MartinCupela Feb 3, 2025
ed3f7da
refactor: move isMessageAIGenerated to global scope in vite App
MartinCupela Feb 4, 2025
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
5 changes: 4 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ export default tseslint.config(
'no-console': 'off',
'no-mixed-spaces-and-tabs': 'warn',
'no-self-compare': 'error',
'no-underscore-dangle': ['error', { allowAfterThis: true }],
'no-underscore-dangle': [
'error',
{ allow: ['_internalState'], allowAfterThis: true },
],
'no-use-before-define': 'off',
'no-useless-concat': 'error',
'no-var': 'error',
Expand Down
18 changes: 11 additions & 7 deletions examples/vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import {
ChannelHeader,
ChannelList,
Chat,
ChatView,
MessageInput,
VirtualizedMessageList as MessageList,
StreamMessage,
Thread,
Window,
useCreateChatClient,
ThreadList,
ChatView,
useCreateChatClient,
VirtualizedMessageList as MessageList,
Window,
} from 'stream-chat-react';

const params = (new Proxy(new URLSearchParams(window.location.search), {
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, property) => searchParams.get(property as string),
}) as unknown) as Record<string, string | null>;
}) as unknown as Record<string, string | null>;

const parseUserIdFromToken = (token: string) => {
const [, payload] = token.split('.');
Expand Down Expand Up @@ -63,6 +64,9 @@ type StreamChatGenerics = {
userType: LocalUserType;
};

const isMessageAIGenerated = (message: StreamMessage<StreamChatGenerics>) =>
!!message?.ai_generated;

const App = () => {
const chatClient = useCreateChatClient<StreamChatGenerics>({
apiKey,
Expand All @@ -73,7 +77,7 @@ const App = () => {
if (!chatClient) return <>Loading...</>;

return (
<Chat client={chatClient} isMessageAIGenerated={(message) => !!message?.ai_generated}>
<Chat client={chatClient} isMessageAIGenerated={isMessageAIGenerated}>
<ChatView>
<ChatView.Selector />
<ChatView.Channels>
Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
"emoji-mart": "^5.4.0",
"react": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.8.0",
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.8.0",
"stream-chat": "^8.50.0"
"stream-chat": "^8.55.0"
},
"peerDependenciesMeta": {
"@breezystack/lamejs": {
Expand Down Expand Up @@ -187,8 +187,7 @@
"@semantic-release/changelog": "^6.0.2",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@stream-io/rollup-plugin-node-builtins": "^2.1.5",
"@stream-io/stream-chat-css": "^5.6.0",
"@stream-io/stream-chat-css": "^5.7.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
Expand Down Expand Up @@ -242,7 +241,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"semantic-release": "^19.0.5",
"stream-chat": "^8.50.0",
"stream-chat": "^8.55.0",
"ts-jest": "^29.2.5",
"typescript": "^5.4.5",
"typescript-eslint": "^8.17.0"
Expand Down
124 changes: 71 additions & 53 deletions src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,6 @@
import debounce from 'lodash.debounce';
import defaultsDeep from 'lodash.defaultsdeep';
import throttle from 'lodash.throttle';
import {
APIErrorResponse,
ChannelAPIResponse,
ChannelMemberResponse,
ChannelQueryOptions,
ChannelState,
ErrorFromResponse,
Event,
EventAPIResponse,
Message,
MessageResponse,
SendMessageAPIResponse,
Channel as StreamChannel,
StreamChat,
UpdatedMessage,
UserResponse,
} from 'stream-chat';
import { nanoid } from 'nanoid';
import clsx from 'clsx';

Expand Down Expand Up @@ -62,6 +45,7 @@
WithComponents,
} from '../../context';

import { CHANNEL_CONTAINER_ID } from './constants';
import {
DEFAULT_HIGHLIGHT_DURATION,
DEFAULT_INITIAL_CHANNEL_PAGE_SIZE,
Expand All @@ -77,10 +61,27 @@
useImageFlagEmojisOnWindowsClass,
} from './hooks/useChannelContainerClasses';
import { findInMsgSetByDate, findInMsgSetById, makeAddNotifications } from './utils';
import { useThreadContext } from '../Threads';
import { getChannel } from '../../utils';

import type {
APIErrorResponse,
ChannelAPIResponse,
ChannelMemberResponse,
ChannelQueryOptions,
ChannelState,
ErrorFromResponse,
Event,
EventAPIResponse,
Message,
MessageResponse,
SendMessageAPIResponse,
Channel as StreamChannel,
StreamChat,
UpdatedMessage,
UserResponse,
} from 'stream-chat';
import type { MessageInputProps } from '../MessageInput';

import type {
ChannelUnreadUiState,
CustomTrigger,
Expand All @@ -96,8 +97,7 @@
getVideoAttachmentConfiguration,
} from '../Attachment/attachment-sizing';
import type { URLEnrichmentConfig } from '../MessageInput/hooks/useLinkPreviews';
import { useThreadContext } from '../Threads';
import { CHANNEL_CONTAINER_ID } from './constants';
import { useSearchFocusedMessage } from '../../experimental/Search/hooks';

type ChannelPropsForwardedToComponentContext<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
Expand Down Expand Up @@ -359,7 +359,7 @@
[propChannelQueryOptions],
);

const { client, customClasses, latestMessageDatesByChannels, mutes } =
const { client, customClasses, latestMessageDatesByChannels, mutes, searchController } =
useChatContext<StreamChatGenerics>('Channel');
const { t } = useTranslationContext('Channel');
const chatContainerClass = getChatContainerClass(customClasses?.chatContainer);
Expand All @@ -386,13 +386,17 @@
loading: !channel.initialized,
},
);

const jumpToMessageFromSearch = useSearchFocusedMessage();
const isMounted = useIsMounted();

const originalTitle = useRef('');
const lastRead = useRef<Date | undefined>(undefined);
const online = useRef(true);

const clearHighlightedMessageTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
null,
);

const channelCapabilitiesArray = channel.data?.own_capabilities as string[];

const throttledCopyStateFromChannel = throttle(
Expand Down Expand Up @@ -646,6 +650,38 @@
if (message) dispatch({ message, type: 'setThread' });
}, [state.messages, state.thread]);

const handleHighlightedMessageChange = useCallback(
({
highlightDuration,
highlightedMessageId,
}: {
highlightedMessageId: string;
highlightDuration?: number;
}) => {
dispatch({
channel,
highlightedMessageId,
type: 'jumpToMessageFinished',
});
if (clearHighlightedMessageTimeoutId.current) {
clearTimeout(clearHighlightedMessageTimeoutId.current);

Check warning on line 667 in src/components/Channel/Channel.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Channel/Channel.tsx#L667

Added line #L667 was not covered by tests
}
clearHighlightedMessageTimeoutId.current = setTimeout(() => {
if (searchController._internalState.getLatestValue().focusedMessage) {
searchController._internalState.partialNext({ focusedMessage: undefined });

Check warning on line 671 in src/components/Channel/Channel.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Channel/Channel.tsx#L671

Added line #L671 was not covered by tests
}
clearHighlightedMessageTimeoutId.current = null;
dispatch({ type: 'clearHighlightedMessage' });
}, highlightDuration ?? DEFAULT_HIGHLIGHT_DURATION);
},
[channel, searchController],
);

useEffect(() => {
if (!jumpToMessageFromSearch?.id) return;
handleHighlightedMessageChange({ highlightedMessageId: jumpToMessageFromSearch.id });

Check warning on line 682 in src/components/Channel/Channel.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Channel/Channel.tsx#L682

Added line #L682 was not covered by tests
}, [jumpToMessageFromSearch, handleHighlightedMessageChange]);

/** MESSAGE */

// Adds a temporary notification to message list, will be removed after 5 seconds
Expand Down Expand Up @@ -744,10 +780,6 @@
return queryResponse.messages.length;
};

const clearHighlightedMessageTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
null,
);

const jumpToMessage: ChannelActionContextValue<StreamChatGenerics>['jumpToMessage'] =
useCallback(
async (
Expand All @@ -759,22 +791,12 @@
await channel.state.loadMessageIntoState(messageId, undefined, messageLimit);

loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages);
dispatch({
hasMoreNewer: channel.state.messagePagination.hasNext,
handleHighlightedMessageChange({

Check warning on line 794 in src/components/Channel/Channel.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Channel/Channel.tsx#L794

Added line #L794 was not covered by tests
highlightDuration,
highlightedMessageId: messageId,
type: 'jumpToMessageFinished',
});

if (clearHighlightedMessageTimeoutId.current) {
clearTimeout(clearHighlightedMessageTimeoutId.current);
}

clearHighlightedMessageTimeoutId.current = setTimeout(() => {
clearHighlightedMessageTimeoutId.current = null;
dispatch({ type: 'clearHighlightedMessage' });
}, highlightDuration);
},
[channel, loadMoreFinished],
[channel, handleHighlightedMessageChange, loadMoreFinished],
);

const jumpToLatestMessage: ChannelActionContextValue<StreamChatGenerics>['jumpToLatestMessage'] =
Expand Down Expand Up @@ -915,23 +937,19 @@
first_unread_message_id: firstUnreadMessageId,
last_read_message_id: lastReadMessageId,
});

dispatch({
hasMoreNewer: channel.state.messagePagination.hasNext,
handleHighlightedMessageChange({
highlightDuration,
highlightedMessageId: firstUnreadMessageId,
type: 'jumpToMessageFinished',
});

if (clearHighlightedMessageTimeoutId.current) {
clearTimeout(clearHighlightedMessageTimeoutId.current);
}

clearHighlightedMessageTimeoutId.current = setTimeout(() => {
clearHighlightedMessageTimeoutId.current = null;
dispatch({ type: 'clearHighlightedMessage' });
}, highlightDuration);
},
[addNotification, channel, loadMoreFinished, t, channelUnreadUiState],
[
addNotification,
channel,
handleHighlightedMessageChange,
loadMoreFinished,
t,
channelUnreadUiState,
],
);

const deleteMessage = useCallback(
Expand Down
2 changes: 2 additions & 0 deletions src/components/Channel/__tests__/Channel.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { nanoid } from 'nanoid';
import React, { useEffect } from 'react';
import { SearchController } from 'stream-chat';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';

Expand Down Expand Up @@ -181,6 +182,7 @@ describe('Channel', () => {
setQueryInProgress: jest.fn(),
},
client: chatClient,
searchController: new SearchController(),
}}
>
<Channel channel={channel}>{childrenContent}</Channel>
Expand Down
5 changes: 3 additions & 2 deletions src/components/Channel/channelState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type ChannelStateReducerAction<
type: 'copyStateFromChannelOnEvent';
}
| {
hasMoreNewer: boolean;
channel: Channel<StreamChatGenerics>;
highlightedMessageId: string;
type: 'jumpToMessageFinished';
}
Expand Down Expand Up @@ -161,8 +161,9 @@ export const makeChannelReducer =
case 'jumpToMessageFinished': {
return {
...state,
hasMoreNewer: action.hasMoreNewer,
hasMoreNewer: action.channel.state.messagePagination.hasNext,
highlightedMessageId: action.highlightedMessageId,
messages: action.channel.state.messages,
};
}

Expand Down
Loading