Skip to content

Commit 04e03d6

Browse files
committed
feat: add jump to selected search result message
1 parent 674dd16 commit 04e03d6

File tree

4 files changed

+102
-58
lines changed

4 files changed

+102
-58
lines changed

src/components/Channel/Channel.tsx

Lines changed: 65 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ import {
9898
import type { URLEnrichmentConfig } from '../MessageInput/hooks/useLinkPreviews';
9999
import { useThreadContext } from '../Threads';
100100
import { CHANNEL_CONTAINER_ID } from './constants';
101+
import {
102+
DefaultSearchSources,
103+
SearchControllerState,
104+
SearchSource,
105+
} from '../Search/SearchController';
106+
import { useStateStore } from '../../store';
107+
108+
const searchControllerStateSelector = <
109+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
110+
Sources extends SearchSource[] = DefaultSearchSources<StreamChatGenerics>
111+
>(
112+
nextValue: SearchControllerState<StreamChatGenerics, Sources>,
113+
) => ({ jumpToMessageFromSearch: nextValue.focusedMessage });
101114

102115
export type ChannelPropsForwardedToComponentContext<
103116
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
@@ -363,6 +376,7 @@ const ChannelInner = <
363376
customClasses,
364377
latestMessageDatesByChannels,
365378
mutes,
379+
searchController,
366380
} = useChatContext<StreamChatGenerics>('Channel');
367381
const { t } = useTranslationContext('Channel');
368382
const chatContainerClass = getChatContainerClass(customClasses?.chatContainer);
@@ -387,12 +401,19 @@ const ChannelInner = <
387401
},
388402
);
389403

404+
const { jumpToMessageFromSearch } = useStateStore(
405+
searchController.state,
406+
searchControllerStateSelector,
407+
);
408+
390409
const isMounted = useIsMounted();
391410

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

415+
const clearHighlightedMessageTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
416+
396417
const channelCapabilitiesArray = channel.data?.own_capabilities as string[];
397418

398419
const throttledCopyStateFromChannel = throttle(
@@ -632,6 +653,38 @@ const ChannelInner = <
632653
if (message) dispatch({ message, type: 'setThread' });
633654
}, [state.messages, state.thread]);
634655

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

637690
// Adds a temporary notification to message list, will be removed after 5 seconds
@@ -718,8 +771,6 @@ const ChannelInner = <
718771
return queryResponse.messages.length;
719772
};
720773

721-
const clearHighlightedMessageTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
722-
723774
const jumpToMessage: ChannelActionContextValue<StreamChatGenerics>['jumpToMessage'] = useCallback(
724775
async (
725776
messageId,
@@ -730,22 +781,9 @@ const ChannelInner = <
730781
await channel.state.loadMessageIntoState(messageId, undefined, messageLimit);
731782

732783
loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages);
733-
dispatch({
734-
hasMoreNewer: channel.state.messagePagination.hasNext,
735-
highlightedMessageId: messageId,
736-
type: 'jumpToMessageFinished',
737-
});
738-
739-
if (clearHighlightedMessageTimeoutId.current) {
740-
clearTimeout(clearHighlightedMessageTimeoutId.current);
741-
}
742-
743-
clearHighlightedMessageTimeoutId.current = setTimeout(() => {
744-
clearHighlightedMessageTimeoutId.current = null;
745-
dispatch({ type: 'clearHighlightedMessage' });
746-
}, highlightDuration);
784+
handleHighlightedMessageChange({ highlightDuration, highlightedMessageId: messageId });
747785
},
748-
[channel, loadMoreFinished],
786+
[channel, handleHighlightedMessageChange, loadMoreFinished],
749787
);
750788

751789
const jumpToLatestMessage: ChannelActionContextValue<StreamChatGenerics>['jumpToLatestMessage'] = useCallback(async () => {
@@ -864,23 +902,19 @@ const ChannelInner = <
864902
first_unread_message_id: firstUnreadMessageId,
865903
last_read_message_id: lastReadMessageId,
866904
});
867-
868-
dispatch({
869-
hasMoreNewer: channel.state.messagePagination.hasNext,
905+
handleHighlightedMessageChange({
906+
highlightDuration,
870907
highlightedMessageId: firstUnreadMessageId,
871-
type: 'jumpToMessageFinished',
872908
});
873-
874-
if (clearHighlightedMessageTimeoutId.current) {
875-
clearTimeout(clearHighlightedMessageTimeoutId.current);
876-
}
877-
878-
clearHighlightedMessageTimeoutId.current = setTimeout(() => {
879-
clearHighlightedMessageTimeoutId.current = null;
880-
dispatch({ type: 'clearHighlightedMessage' });
881-
}, highlightDuration);
882909
},
883-
[addNotification, channel, loadMoreFinished, t, channelUnreadUiState],
910+
[
911+
addNotification,
912+
channel,
913+
handleHighlightedMessageChange,
914+
loadMoreFinished,
915+
t,
916+
channelUnreadUiState,
917+
],
884918
);
885919

886920
const deleteMessage = useCallback(

src/components/Channel/channelState.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export type ChannelStateReducerAction<
2424
type: 'copyStateFromChannelOnEvent';
2525
}
2626
| {
27-
hasMoreNewer: boolean;
27+
channel: Channel<StreamChatGenerics>;
2828
highlightedMessageId: string;
2929
type: 'jumpToMessageFinished';
3030
}
@@ -160,8 +160,9 @@ export const channelReducer = <
160160
case 'jumpToMessageFinished': {
161161
return {
162162
...state,
163-
hasMoreNewer: action.hasMoreNewer,
163+
hasMoreNewer: action.channel.state.messagePagination.hasNext,
164164
highlightedMessageId: action.highlightedMessageId,
165+
messages: action.channel.state.messages,
165166
};
166167
}
167168

src/components/Search/SearchController.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { StateStore } from 'stream-chat';
21
import type {
32
Channel,
43
ChannelFilters,
4+
MessageFilters,
55
MessageResponse,
66
SearchMessageSort,
77
SearchOptions,
@@ -10,6 +10,7 @@ import type {
1010
UserResponse,
1111
UserSort,
1212
} from 'stream-chat';
13+
import { StateStore } from 'stream-chat';
1314
import type { DefaultStreamChatGenerics } from '../../types';
1415
import debounce from 'lodash.debounce';
1516
import type { DebouncedFunc } from 'lodash';
@@ -248,12 +249,18 @@ export class MessageSearchSource<
248249

249250
protected async query(searchQuery: string) {
250251
if (!this.client.userID) return { items: [] };
251-
// @ts-ignore
252+
252253
const channelFilters: ChannelFilters<StreamChatGenerics> = {
253254
members: { $in: [this.client.userID] },
254-
};
255-
const messageFilters = searchQuery;
255+
} as ChannelFilters<StreamChatGenerics>;
256+
257+
const messageFilters: MessageFilters<StreamChatGenerics> = {
258+
text: searchQuery,
259+
type: 'regular', // todo: type: 'reply'
260+
} as MessageFilters<StreamChatGenerics>;
261+
256262
const sort: SearchMessageSort<StreamChatGenerics> = { created_at: -1 };
263+
257264
const options = {
258265
limit: this.pageSize,
259266
next: this.next,
@@ -352,10 +359,13 @@ export type SearchControllerState<
352359
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
353360
Sources extends SearchSource[] = DefaultSearchSources<StreamChatGenerics>
354361
> = {
355-
isActive: boolean; // todo: cancel query execution
362+
isActive: boolean;
356363
queriesInProgress: Array<Sources[number]['type']>;
357364
searchQuery: string;
358365
activeSource?: Sources[number];
366+
// FIXME: focusedMessage should live in a MessageListController class that does not exist yet.
367+
// This state prop should be then removed
368+
focusedMessage?: MessageResponse<StreamChatGenerics>;
359369
input?: HTMLInputElement;
360370
};
361371

src/components/Search/SearchResults/SearchResultItem.tsx

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@ import uniqBy from 'lodash.uniqby';
22
import React, { ComponentType, useCallback, useMemo } from 'react';
33
import { Channel, MessageResponse, User } from 'stream-chat';
44
import { Avatar } from '../../Avatar';
5-
import {
6-
ChannelPreview,
7-
ChannelPreviewMessenger,
8-
useChannelPreviewInfo,
9-
} from '../../ChannelPreview';
5+
import { ChannelPreview } from '../../ChannelPreview';
106
import { useChannelListContext, useChatContext, useSearchContext } from '../../../context';
117
import type { DefaultStreamChatGenerics } from '../../../types';
128
import type {
139
DefaultSearchSources,
1410
InferSearchQueryResult,
1511
SearchSource,
1612
} from '../SearchController';
13+
import { DEFAULT_JUMP_TO_PAGE_SIZE } from '../../../constants/limits';
1714

1815
export type ChannelSearchResultItemProps<
1916
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
@@ -44,12 +41,13 @@ export type ChannelByMessageSearchResultItemProps<
4441
};
4542

4643
export const MessageSearchResultItem = <
47-
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
44+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
45+
SearchSources extends SearchSource[] = DefaultSearchSources<StreamChatGenerics>
4846
>({
4947
item,
5048
}: ChannelByMessageSearchResultItemProps<StreamChatGenerics>) => {
51-
const { client } = useChatContext<StreamChatGenerics>();
52-
const { setActiveChannel } = useChatContext<StreamChatGenerics>();
49+
const { client, searchController } = useChatContext<StreamChatGenerics, SearchSources>();
50+
const { channel: activeChannel, setActiveChannel } = useChatContext<StreamChatGenerics>();
5351
const { setChannels } = useChannelListContext<StreamChatGenerics>();
5452

5553
const channel = useMemo(() => {
@@ -59,28 +57,29 @@ export const MessageSearchResultItem = <
5957
return client.channel(type, id);
6058
}, [client, item]);
6159

62-
const onSelect = useCallback(() => {
60+
const onSelect = useCallback(async () => {
6361
if (!channel) return;
64-
62+
await channel.state.loadMessageIntoState(item.id, undefined, DEFAULT_JUMP_TO_PAGE_SIZE);
63+
// FIXME: message focus should be handled by yet non-existent msg list controller in client packaged
64+
searchController.state.partialNext({ focusedMessage: item });
6565
setActiveChannel(channel);
6666
setChannels?.((channels) => uniqBy([channel, ...channels], 'cid'));
67-
// todo: jumpToMessage
68-
}, [channel, setActiveChannel, setChannels]);
67+
}, [channel, item, searchController, setActiveChannel, setChannels]);
6968

70-
const { displayImage, displayTitle, groupChannelDisplayInfo } = useChannelPreviewInfo({
71-
channel,
72-
});
69+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
70+
const getLatestMessagePreview = useCallback(() => item.text!, [item]);
7371

7472
if (!channel) return;
7573

7674
return (
77-
<ChannelPreviewMessenger
75+
<ChannelPreview
76+
active={
77+
channel.cid === activeChannel?.cid &&
78+
item.id === searchController.state.getLatestValue().focusedMessage?.id
79+
}
7880
channel={channel}
7981
className='str-chat__search-result'
80-
displayImage={displayImage}
81-
displayTitle={displayTitle}
82-
groupChannelDisplayInfo={groupChannelDisplayInfo}
83-
latestMessagePreview={item.text}
82+
getLatestMessagePreview={getLatestMessagePreview}
8483
onSelect={onSelect}
8584
/>
8685
);

0 commit comments

Comments
 (0)