Skip to content

Commit d545207

Browse files
authored
feat: add general purpose search components (#2588)
1 parent 1c445de commit d545207

File tree

67 files changed

+2746
-505
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+2746
-505
lines changed

eslint.config.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ export default tseslint.config(
4848
'no-console': 'off',
4949
'no-mixed-spaces-and-tabs': 'warn',
5050
'no-self-compare': 'error',
51-
'no-underscore-dangle': ['error', { allowAfterThis: true }],
51+
'no-underscore-dangle': [
52+
'error',
53+
{ allow: ['_internalState'], allowAfterThis: true },
54+
],
5255
'no-use-before-define': 'off',
5356
'no-useless-concat': 'error',
5457
'no-var': 'error',

examples/vite/src/App.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@ import {
66
ChannelHeader,
77
ChannelList,
88
Chat,
9+
ChatView,
910
MessageInput,
10-
VirtualizedMessageList as MessageList,
11+
StreamMessage,
1112
Thread,
12-
Window,
13-
useCreateChatClient,
1413
ThreadList,
15-
ChatView,
14+
useCreateChatClient,
15+
VirtualizedMessageList as MessageList,
16+
Window,
1617
} from 'stream-chat-react';
1718

18-
const params = (new Proxy(new URLSearchParams(window.location.search), {
19+
const params = new Proxy(new URLSearchParams(window.location.search), {
1920
get: (searchParams, property) => searchParams.get(property as string),
20-
}) as unknown) as Record<string, string | null>;
21+
}) as unknown as Record<string, string | null>;
2122

2223
const parseUserIdFromToken = (token: string) => {
2324
const [, payload] = token.split('.');
@@ -63,6 +64,9 @@ type StreamChatGenerics = {
6364
userType: LocalUserType;
6465
};
6566

67+
const isMessageAIGenerated = (message: StreamMessage<StreamChatGenerics>) =>
68+
!!message?.ai_generated;
69+
6670
const App = () => {
6771
const chatClient = useCreateChatClient<StreamChatGenerics>({
6872
apiKey,
@@ -73,7 +77,7 @@ const App = () => {
7377
if (!chatClient) return <>Loading...</>;
7478

7579
return (
76-
<Chat client={chatClient} isMessageAIGenerated={(message) => !!message?.ai_generated}>
80+
<Chat client={chatClient} isMessageAIGenerated={isMessageAIGenerated}>
7781
<ChatView>
7882
<ChatView.Selector />
7983
<ChatView.Channels>

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145
"emoji-mart": "^5.4.0",
146146
"react": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.8.0",
147147
"react-dom": "^19.0.0 || ^18.0.0 || ^17.0.0 || ^16.8.0",
148-
"stream-chat": "^8.50.0"
148+
"stream-chat": "^8.55.0"
149149
},
150150
"peerDependenciesMeta": {
151151
"@breezystack/lamejs": {
@@ -187,7 +187,6 @@
187187
"@semantic-release/changelog": "^6.0.2",
188188
"@semantic-release/exec": "^6.0.3",
189189
"@semantic-release/git": "^10.0.1",
190-
"@stream-io/rollup-plugin-node-builtins": "^2.1.5",
191190
"@stream-io/stream-chat-css": "^5.7.0",
192191
"@testing-library/dom": "^10.4.0",
193192
"@testing-library/jest-dom": "^6.6.3",
@@ -242,7 +241,7 @@
242241
"react": "^19.0.0",
243242
"react-dom": "^19.0.0",
244243
"semantic-release": "^19.0.5",
245-
"stream-chat": "^8.50.0",
244+
"stream-chat": "^8.55.0",
246245
"ts-jest": "^29.2.5",
247246
"typescript": "^5.4.5",
248247
"typescript-eslint": "^8.17.0"

src/components/Channel/Channel.tsx

Lines changed: 71 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,6 @@ import React, {
1313
import debounce from 'lodash.debounce';
1414
import defaultsDeep from 'lodash.defaultsdeep';
1515
import throttle from 'lodash.throttle';
16-
import {
17-
APIErrorResponse,
18-
ChannelAPIResponse,
19-
ChannelMemberResponse,
20-
ChannelQueryOptions,
21-
ChannelState,
22-
ErrorFromResponse,
23-
Event,
24-
EventAPIResponse,
25-
Message,
26-
MessageResponse,
27-
SendMessageAPIResponse,
28-
Channel as StreamChannel,
29-
StreamChat,
30-
UpdatedMessage,
31-
UserResponse,
32-
} from 'stream-chat';
3316
import { nanoid } from 'nanoid';
3417
import clsx from 'clsx';
3518

@@ -62,6 +45,7 @@ import {
6245
WithComponents,
6346
} from '../../context';
6447

48+
import { CHANNEL_CONTAINER_ID } from './constants';
6549
import {
6650
DEFAULT_HIGHLIGHT_DURATION,
6751
DEFAULT_INITIAL_CHANNEL_PAGE_SIZE,
@@ -77,10 +61,27 @@ import {
7761
useImageFlagEmojisOnWindowsClass,
7862
} from './hooks/useChannelContainerClasses';
7963
import { findInMsgSetByDate, findInMsgSetById, makeAddNotifications } from './utils';
64+
import { useThreadContext } from '../Threads';
8065
import { getChannel } from '../../utils';
8166

67+
import type {
68+
APIErrorResponse,
69+
ChannelAPIResponse,
70+
ChannelMemberResponse,
71+
ChannelQueryOptions,
72+
ChannelState,
73+
ErrorFromResponse,
74+
Event,
75+
EventAPIResponse,
76+
Message,
77+
MessageResponse,
78+
SendMessageAPIResponse,
79+
Channel as StreamChannel,
80+
StreamChat,
81+
UpdatedMessage,
82+
UserResponse,
83+
} from 'stream-chat';
8284
import type { MessageInputProps } from '../MessageInput';
83-
8485
import type {
8586
ChannelUnreadUiState,
8687
CustomTrigger,
@@ -96,8 +97,7 @@ import {
9697
getVideoAttachmentConfiguration,
9798
} from '../Attachment/attachment-sizing';
9899
import type { URLEnrichmentConfig } from '../MessageInput/hooks/useLinkPreviews';
99-
import { useThreadContext } from '../Threads';
100-
import { CHANNEL_CONTAINER_ID } from './constants';
100+
import { useSearchFocusedMessage } from '../../experimental/Search/hooks';
101101

102102
type ChannelPropsForwardedToComponentContext<
103103
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
@@ -360,7 +360,7 @@ const ChannelInner = <
360360
[propChannelQueryOptions],
361361
);
362362

363-
const { client, customClasses, latestMessageDatesByChannels, mutes } =
363+
const { client, customClasses, latestMessageDatesByChannels, mutes, searchController } =
364364
useChatContext<StreamChatGenerics>('Channel');
365365
const { t } = useTranslationContext('Channel');
366366
const chatContainerClass = getChatContainerClass(customClasses?.chatContainer);
@@ -387,13 +387,17 @@ const ChannelInner = <
387387
loading: !channel.initialized,
388388
},
389389
);
390-
390+
const jumpToMessageFromSearch = useSearchFocusedMessage();
391391
const isMounted = useIsMounted();
392392

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

397+
const clearHighlightedMessageTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
398+
null,
399+
);
400+
397401
const channelCapabilitiesArray = channel.data?.own_capabilities as string[];
398402

399403
const throttledCopyStateFromChannel = throttle(
@@ -647,6 +651,38 @@ const ChannelInner = <
647651
if (message) dispatch({ message, type: 'setThread' });
648652
}, [state.messages, state.thread]);
649653

654+
const handleHighlightedMessageChange = useCallback(
655+
({
656+
highlightDuration,
657+
highlightedMessageId,
658+
}: {
659+
highlightedMessageId: string;
660+
highlightDuration?: number;
661+
}) => {
662+
dispatch({
663+
channel,
664+
highlightedMessageId,
665+
type: 'jumpToMessageFinished',
666+
});
667+
if (clearHighlightedMessageTimeoutId.current) {
668+
clearTimeout(clearHighlightedMessageTimeoutId.current);
669+
}
670+
clearHighlightedMessageTimeoutId.current = setTimeout(() => {
671+
if (searchController._internalState.getLatestValue().focusedMessage) {
672+
searchController._internalState.partialNext({ focusedMessage: undefined });
673+
}
674+
clearHighlightedMessageTimeoutId.current = null;
675+
dispatch({ type: 'clearHighlightedMessage' });
676+
}, highlightDuration ?? DEFAULT_HIGHLIGHT_DURATION);
677+
},
678+
[channel, searchController],
679+
);
680+
681+
useEffect(() => {
682+
if (!jumpToMessageFromSearch?.id) return;
683+
handleHighlightedMessageChange({ highlightedMessageId: jumpToMessageFromSearch.id });
684+
}, [jumpToMessageFromSearch, handleHighlightedMessageChange]);
685+
650686
/** MESSAGE */
651687

652688
// Adds a temporary notification to message list, will be removed after 5 seconds
@@ -745,10 +781,6 @@ const ChannelInner = <
745781
return queryResponse.messages.length;
746782
};
747783

748-
const clearHighlightedMessageTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
749-
null,
750-
);
751-
752784
const jumpToMessage: ChannelActionContextValue<StreamChatGenerics>['jumpToMessage'] =
753785
useCallback(
754786
async (
@@ -760,22 +792,12 @@ const ChannelInner = <
760792
await channel.state.loadMessageIntoState(messageId, undefined, messageLimit);
761793

762794
loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages);
763-
dispatch({
764-
hasMoreNewer: channel.state.messagePagination.hasNext,
795+
handleHighlightedMessageChange({
796+
highlightDuration,
765797
highlightedMessageId: messageId,
766-
type: 'jumpToMessageFinished',
767798
});
768-
769-
if (clearHighlightedMessageTimeoutId.current) {
770-
clearTimeout(clearHighlightedMessageTimeoutId.current);
771-
}
772-
773-
clearHighlightedMessageTimeoutId.current = setTimeout(() => {
774-
clearHighlightedMessageTimeoutId.current = null;
775-
dispatch({ type: 'clearHighlightedMessage' });
776-
}, highlightDuration);
777799
},
778-
[channel, loadMoreFinished],
800+
[channel, handleHighlightedMessageChange, loadMoreFinished],
779801
);
780802

781803
const jumpToLatestMessage: ChannelActionContextValue<StreamChatGenerics>['jumpToLatestMessage'] =
@@ -916,23 +938,19 @@ const ChannelInner = <
916938
first_unread_message_id: firstUnreadMessageId,
917939
last_read_message_id: lastReadMessageId,
918940
});
919-
920-
dispatch({
921-
hasMoreNewer: channel.state.messagePagination.hasNext,
941+
handleHighlightedMessageChange({
942+
highlightDuration,
922943
highlightedMessageId: firstUnreadMessageId,
923-
type: 'jumpToMessageFinished',
924944
});
925-
926-
if (clearHighlightedMessageTimeoutId.current) {
927-
clearTimeout(clearHighlightedMessageTimeoutId.current);
928-
}
929-
930-
clearHighlightedMessageTimeoutId.current = setTimeout(() => {
931-
clearHighlightedMessageTimeoutId.current = null;
932-
dispatch({ type: 'clearHighlightedMessage' });
933-
}, highlightDuration);
934945
},
935-
[addNotification, channel, loadMoreFinished, t, channelUnreadUiState],
946+
[
947+
addNotification,
948+
channel,
949+
handleHighlightedMessageChange,
950+
loadMoreFinished,
951+
t,
952+
channelUnreadUiState,
953+
],
936954
);
937955

938956
const deleteMessage = useCallback(

src/components/Channel/__tests__/Channel.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { nanoid } from 'nanoid';
22
import React, { useEffect } from 'react';
3+
import { SearchController } from 'stream-chat';
34
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
45
import '@testing-library/jest-dom';
56

@@ -181,6 +182,7 @@ describe('Channel', () => {
181182
setQueryInProgress: jest.fn(),
182183
},
183184
client: chatClient,
185+
searchController: new SearchController(),
184186
}}
185187
>
186188
<Channel channel={channel}>{childrenContent}</Channel>

src/components/Channel/channelState.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export type ChannelStateReducerAction<
2727
type: 'copyStateFromChannelOnEvent';
2828
}
2929
| {
30-
hasMoreNewer: boolean;
30+
channel: Channel<StreamChatGenerics>;
3131
highlightedMessageId: string;
3232
type: 'jumpToMessageFinished';
3333
}
@@ -161,8 +161,9 @@ export const makeChannelReducer =
161161
case 'jumpToMessageFinished': {
162162
return {
163163
...state,
164-
hasMoreNewer: action.hasMoreNewer,
164+
hasMoreNewer: action.channel.state.messagePagination.hasNext,
165165
highlightedMessageId: action.highlightedMessageId,
166+
messages: action.channel.state.messages,
166167
};
167168
}
168169

0 commit comments

Comments
 (0)