fix(app): tell the user when a search query has nothing to search (#1901)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:42:20 +02:00
1 parent bb34c73069
commit b71ced9b2e
8 files changed
+303 -136

No files matched your search

@@ -260,9 +260,6 @@ export const ChannelHeader = observer(
); );
const handleSearchSubmit = useCallback(() => { const handleSearchSubmit = useCallback(() => {
const query = latestSearchQueryRef.current; const query = latestSearchQueryRef.current;
if (!query.trim()) {
return;
}
if (onSearchSubmit) { if (onSearchSubmit) {
onSearchSubmit(query, latestSearchSegmentsRef.current); onSearchSubmit(query, latestSearchSegmentsRef.current);
return; return;
@@ -20,13 +20,16 @@ import {
renderScopeIcon, renderScopeIcon,
renderSortIcon, renderSortIcon,
} from '@app/features/channel/components/channel_search_results/ChannelSearchResultsShared'; } from '@app/features/channel/components/channel_search_results/ChannelSearchResultsShared';
import {shouldSearchResultRowJump} from '@app/features/channel/components/channel_search_results/SearchResultRowClick';
import {SearchResultsHeader} from '@app/features/channel/components/channel_search_results/SearchResultsHeader'; import {SearchResultsHeader} from '@app/features/channel/components/channel_search_results/SearchResultsHeader';
import {SearchResultsPagination} from '@app/features/channel/components/channel_search_results/SearchResultsPagination'; import {SearchResultsPagination} from '@app/features/channel/components/channel_search_results/SearchResultsPagination';
import { import {
SearchEmptyState, SearchEmptyState,
SearchErrorState, SearchErrorState,
SearchIndexingState, SearchIndexingState,
SearchUnappliedQueryState,
} from '@app/features/channel/components/channel_search_results/SearchResultsStateViews'; } from '@app/features/channel/components/channel_search_results/SearchResultsStateViews';
import {useChannelSearchHighlight} from '@app/features/channel/components/channel_search_results/useChannelSearchHighlight';
import type {MessageGroupRenderWrapperProps} from '@app/features/channel/components/MessageGroup'; import type {MessageGroupRenderWrapperProps} from '@app/features/channel/components/MessageGroup';
import { import {
buildSearchResultGroups, buildSearchResultGroups,
@@ -47,17 +50,13 @@ import * as MessageCommands from '@app/features/messaging/commands/MessageComman
import {MessageContextPrefix} from '@app/features/messaging/components/message_context_prefix/MessageContextPrefix'; import {MessageContextPrefix} from '@app/features/messaging/components/message_context_prefix/MessageContextPrefix';
import {useMessageListKeyboardNavigation} from '@app/features/messaging/hooks/useMessageListKeyboardNavigation'; import {useMessageListKeyboardNavigation} from '@app/features/messaging/hooks/useMessageListKeyboardNavigation';
import {useMessageSelectionCopyForMessages} from '@app/features/messaging/hooks/useMessageSelectionCopy'; import {useMessageSelectionCopyForMessages} from '@app/features/messaging/hooks/useMessageSelectionCopy';
import {NearViewportSurfaceContext} from '@app/features/messaging/hooks/useNearViewport';
import type {Message} from '@app/features/messaging/models/MessagingMessage'; import type {Message} from '@app/features/messaging/models/MessagingMessage';
import {
applyChannelSearchHighlight,
clearChannelSearchHighlight,
} from '@app/features/messaging/utils/ChannelSearchHighlight';
import {focusChannelTextareaAfterNavigation} from '@app/features/messaging/utils/ChannelTextareaFocusUtils'; import {focusChannelTextareaAfterNavigation} from '@app/features/messaging/utils/ChannelTextareaFocusUtils';
import {getCollapsedMessageGroupKey} from '@app/features/messaging/utils/MessageGroupingUtils'; import {getCollapsedMessageGroupKey} from '@app/features/messaging/utils/MessageGroupingUtils';
import LocalUserSpamOverride from '@app/features/moderation/state/LocalUserSpamOverride'; import LocalUserSpamOverride from '@app/features/moderation/state/LocalUserSpamOverride';
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands'; import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
import {SearchResultOpenFailedModal} from '@app/features/search/components/alerts/SearchResultOpenFailedModal'; import {SearchResultOpenFailedModal} from '@app/features/search/components/alerts/SearchResultOpenFailedModal';
import {tokenizeSearchQuery} from '@app/features/search/utils/SearchQueryTokenizer';
import type {SearchSegment} from '@app/features/search/utils/SearchSegmentManager'; import type {SearchSegment} from '@app/features/search/utils/SearchSegmentManager';
import { import {
isIndexing, isIndexing,
@@ -95,6 +94,10 @@ export const ChannelSearchResults = observer(
({channel, searchQuery, searchSegments, onClose, refreshKey}: ChannelSearchResultsProps) => { ({channel, searchQuery, searchSegments, onClose, refreshKey}: ChannelSearchResultsProps) => {
const {i18n} = useLingui(); const {i18n} = useLingui();
const scrollerRef = useRef<ScrollerHandle | null>(null); const scrollerRef = useRef<ScrollerHandle | null>(null);
const resolveSearchResultsScrollSurface = useMemo(
() => () => scrollerRef.current?.getViewportElement() ?? null,
[],
);
const pollingTimeout = useRef<number | null>(null); const pollingTimeout = useRef<number | null>(null);
const currentChannelId = useRef(channel.id); const currentChannelId = useRef(channel.id);
const currentSearchQuery = useRef(searchQuery); const currentSearchQuery = useRef(searchQuery);
@@ -114,6 +117,7 @@ export const ChannelSearchResults = observer(
const machineState = searchContext?.machineState ?? {status: 'loading' as const}; const machineState = searchContext?.machineState ?? {status: 'loading' as const};
const scrollPosition = searchContext?.scrollPosition ?? 0; const scrollPosition = searchContext?.scrollPosition ?? 0;
const lastKnownScrollPosition = useRef(scrollPosition); const lastKnownScrollPosition = useRef(scrollPosition);
const unsearchableQuery = searchContext?.unsearchableQuery ?? '';
const successMachineState = machineState.status === 'success' ? machineState : null; const successMachineState = machineState.status === 'success' ? machineState : null;
const indexingMachineState = machineState.status === 'indexing' ? machineState : null; const indexingMachineState = machineState.status === 'indexing' ? machineState : null;
const normalizedRefreshKey = refreshKey ?? null; const normalizedRefreshKey = refreshKey ?? null;
@@ -559,6 +563,16 @@ export const ChannelSearchResults = observer(
}, },
[ensureSearchChannelReady, navigateToSearchMessage, showSearchResultOpenFailedModal], [ensureSearchChannelReady, navigateToSearchMessage, showSearchResultOpenFailedModal],
); );
const handleResultRowClick = useCallback(
(event: React.MouseEvent<HTMLDivElement>, targetChannel: Channel, message: Message) => {
if (!shouldSearchResultRowJump(event.target as Node, event.currentTarget)) {
event.stopPropagation();
return;
}
void handleSearchMessageJump(targetChannel, message);
},
[handleSearchMessageJump],
);
const handlePaginationJump = useCallback( const handlePaginationJump = useCallback(
(page: number) => { (page: number) => {
resetScrollerToTop(); resetScrollerToTop();
@@ -567,6 +581,14 @@ export const ChannelSearchResults = observer(
[resetScrollerToTop, performSearch], [resetScrollerToTop, performSearch],
); );
const renderContent = useCallback(() => { const renderContent = useCallback(() => {
if (unsearchableQuery !== '') {
return (
<SearchUnappliedQueryState
query={unsearchableQuery}
data-flx="channel.channel-search-results.render-content.search-unapplied-query-state"
/>
);
}
switch (machineState.status) { switch (machineState.status) {
case 'idle': case 'idle':
case 'loading': case 'loading':
@@ -592,132 +614,147 @@ export const ChannelSearchResults = observer(
value={collapsedMessageVisibility} value={collapsedMessageVisibility}
data-flx="channel.channel-search-results.render-content.collapsed-message-visibility-provider" data-flx="channel.channel-search-results.render-content.collapsed-message-visibility-provider"
> >
<Scroller <NearViewportSurfaceContext.Provider value={resolveSearchResultsScrollSurface}>
ref={setScrollerRef} <Scroller
className={styles.resultsScroller} ref={setScrollerRef}
onScroll={handleScrollerScroll} className={styles.resultsScroller}
fade={false} onScroll={handleScrollerScroll}
key="channel-search-results-scroller-desktop" fade={false}
onCopy={onCopySelectedMessages} key="channel-search-results-scroller-desktop"
data-message-selection-root="true" onCopy={onCopySelectedMessages}
data-flx="channel.channel-search-results.render-content.results-scroller" data-message-selection-root="true"
> data-flx="channel.channel-search-results.render-content.results-scroller"
{resultGroups.map((resultGroup) => { >
const renderData = getSearchResultChannelRenderData( {resultGroups.map((resultGroup) => {
resultGroup.channelId, const renderData = getSearchResultChannelRenderData(
searchChannelsById, resultGroup.channelId,
(activeScope ?? DEFAULT_SCOPE_VALUE) as MessageSearchScope, searchChannelsById,
); (activeScope ?? DEFAULT_SCOPE_VALUE) as MessageSearchScope,
if (!renderData) return null; );
const {messageChannel, showGuildMeta} = renderData; if (!renderData) return null;
const renderMessageActions = (message: Message) => ( const {messageChannel, showGuildMeta} = renderData;
<FocusRing const renderMessageActions = (message: Message) => (
offset={-2} <FocusRing
ringClassName={styles.focusRingTight} offset={-2}
data-flx="channel.channel-search-results.render-message-actions.focus-ring" ringClassName={styles.focusRingTight}
> data-flx="channel.channel-search-results.render-message-actions.focus-ring"
<button
type="button"
className={styles.jumpButton}
onClick={() => {
void handleSearchMessageJump(messageChannel, message);
}}
data-flx="channel.channel-search-results.render-message-actions.jump-button"
> >
{i18n._(JUMP_DESCRIPTOR)} <button
</button> type="button"
</FocusRing> className={styles.jumpButton}
); onClick={() => {
const renderMessageWrapper = ({ void handleSearchMessageJump(messageChannel, message);
message, }}
index, data-flx="channel.channel-search-results.render-message-actions.jump-button"
isGroupStart, >
children, {i18n._(JUMP_DESCRIPTOR)}
}: MessageGroupRenderWrapperProps) => ( </button>
<div </FocusRing>
data-message-index={index} );
data-message-id={message.id} const renderMessageWrapper = ({
data-is-group-start={isGroupStart} message,
className={styles.messageItem} index,
data-flx="channel.channel-search-results.render-message-wrapper.message-item" isGroupStart,
> children,
{children} }: MessageGroupRenderWrapperProps) => (
</div> // biome-ignore lint/a11y/noStaticElementInteractions: mouse convenience; the row's Jump button is the keyboard affordance.
); // biome-ignore lint/a11y/useKeyWithClickEvents: mouse convenience; the row's Jump button is the keyboard affordance.
return ( <div
<React.Fragment key={resultGroup.key}> data-message-index={index}
<MessageContextPrefix data-message-id={message.id}
channel={messageChannel} data-is-group-start={isGroupStart}
showGuildMeta={showGuildMeta} className={styles.messageItem}
onClick={() => { onClick={(event) => {
void handleSearchChannelOpen(messageChannel); handleResultRowClick(event, messageChannel, message);
}} }}
data-flx="channel.channel-search-results.render-content.message-context-prefix" data-flx="channel.channel-search-results.render-message-wrapper.message-item"
/> >
<SearchResultMessageList {children}
channel={messageChannel} </div>
messages={resultGroup.messages} );
revealedGroupKeys={revealedGroupKeys} return (
onGroupRevealChange={handleCollapsedGroupRevealChange} <React.Fragment key={resultGroup.key}>
collapsedGroupClassName={styles.collapsedMessageGroup} <MessageContextPrefix
messagePreviewContext={MessagePreviewContext.LIST_POPOUT} channel={messageChannel}
messageBehaviorOverrides={DETACHED_MESSAGE_BEHAVIOR} showGuildMeta={showGuildMeta}
messageActionsClassName={styles.actionButtons} onClick={() => {
renderMessageActions={renderMessageActions} void handleSearchChannelOpen(messageChannel);
renderMessageWrapper={renderMessageWrapper} }}
spammerOverrideVersion={spammerOverrideVersion} data-flx="channel.channel-search-results.render-content.message-context-prefix"
renderMessage={(message) => ( />
<div <SearchResultMessageList
className={styles.messageItem} channel={messageChannel}
data-message-id={message.id} messages={resultGroup.messages}
data-is-group-start="true" revealedGroupKeys={revealedGroupKeys}
data-flx="channel.channel-search-results.render-content.message-item" onGroupRevealChange={handleCollapsedGroupRevealChange}
> collapsedGroupClassName={styles.collapsedMessageGroup}
<MessageComponent messagePreviewContext={MessagePreviewContext.LIST_POPOUT}
message={message} messageBehaviorOverrides={DETACHED_MESSAGE_BEHAVIOR}
channel={messageChannel} messageActionsClassName={styles.actionButtons}
previewContext={MessagePreviewContext.LIST_POPOUT} renderMessageActions={renderMessageActions}
behaviorOverrides={DETACHED_MESSAGE_BEHAVIOR} renderMessageWrapper={renderMessageWrapper}
data-flx="channel.channel-search-results.render-content.message-component" spammerOverrideVersion={spammerOverrideVersion}
/> renderMessage={(message) => (
// biome-ignore lint/a11y/noStaticElementInteractions: mouse convenience; the row's Jump button is the keyboard affordance.
// biome-ignore lint/a11y/useKeyWithClickEvents: mouse convenience; the row's Jump button is the keyboard affordance.
<div <div
className={styles.actionButtons} className={styles.messageItem}
data-flx="channel.channel-search-results.render-content.action-buttons" data-message-id={message.id}
data-is-group-start="true"
onClick={(event) => {
handleResultRowClick(event, messageChannel, message);
}}
data-flx="channel.channel-search-results.render-content.message-item"
> >
{renderMessageActions(message)} <MessageComponent
message={message}
channel={messageChannel}
previewContext={MessagePreviewContext.LIST_POPOUT}
behaviorOverrides={DETACHED_MESSAGE_BEHAVIOR}
data-flx="channel.channel-search-results.render-content.message-component"
/>
<div
className={styles.actionButtons}
data-flx="channel.channel-search-results.render-content.action-buttons"
>
{renderMessageActions(message)}
</div>
</div> </div>
</div> )}
)} data-flx="channel.channel-search-results.render-content.search-result-message-list"
data-flx="channel.channel-search-results.render-content.search-result-message-list" />
/> </React.Fragment>
</React.Fragment> );
); })}
})} <div
<div className={styles.resultsSpacer}
className={styles.resultsSpacer} data-flx="channel.channel-search-results.render-content.results-spacer"
data-flx="channel.channel-search-results.render-content.results-spacer" />
/> <SearchResultsPagination
<SearchResultsPagination currentPage={currentPage}
currentPage={currentPage} totalPages={totalPages}
totalPages={totalPages} visiblePageSlots={visiblePageSlots}
visiblePageSlots={visiblePageSlots} onJumpToPage={handlePaginationJump}
onJumpToPage={handlePaginationJump} data-flx="channel.channel-search-results.render-content.search-results-pagination"
data-flx="channel.channel-search-results.render-content.search-results-pagination" />
/> </Scroller>
</Scroller> </NearViewportSurfaceContext.Provider>
</CollapsedMessageVisibilityProvider> </CollapsedMessageVisibilityProvider>
); );
} }
} }
}, [ }, [
unsearchableQuery,
machineState, machineState,
performSearch, performSearch,
setScrollerRef, setScrollerRef,
resolveSearchResultsScrollSurface,
handleScrollerScroll, handleScrollerScroll,
visiblePageSlots, visiblePageSlots,
activeScope, activeScope,
handleSearchChannelOpen, handleSearchChannelOpen,
handleSearchMessageJump, handleSearchMessageJump,
handleResultRowClick,
handlePaginationJump, handlePaginationJump,
onCopySelectedMessages, onCopySelectedMessages,
collapsedMessageVisibility, collapsedMessageVisibility,
@@ -822,24 +859,12 @@ export const ChannelSearchResults = observer(
updateScrollPosition(lastKnownScrollPosition.current); updateScrollPosition(lastKnownScrollPosition.current);
}; };
}, [stopPolling, updateScrollPosition]); }, [stopPolling, updateScrollPosition]);
useEffect(() => { useChannelSearchHighlight({
if (machineState.status !== 'success' || !searchQuery.trim()) { isSuccess: machineState.status === 'success',
clearChannelSearchHighlight(); searchQuery,
return; resultsRevision: successMachineState?.results,
} getHighlightRoot: () => scrollerRef.current?.getViewportElement() ?? null,
const scrollerNode = scrollerRef.current?.getViewportElement(); });
if (!scrollerNode) return;
const timer = setTimeout(() => {
const terms = tokenizeSearchQuery(searchQuery);
if (terms.length > 0) {
applyChannelSearchHighlight(scrollerNode, terms);
}
}, 50);
return () => {
clearTimeout(timer);
clearChannelSearchHighlight();
};
}, [machineState.status, searchQuery, successMachineState?.results]);
return ( return (
<div className={styles.container} data-flx="channel.channel-search-results.container"> <div className={styles.container} data-flx="channel.channel-search-results.container">
<SearchResultsHeader <SearchResultsHeader
@@ -79,6 +79,15 @@ export const TRY_A_DIFFERENT_SEARCH_QUERY_DESCRIPTOR = msg({
message: 'Try a different search query.', message: 'Try a different search query.',
comment: 'Body copy under the no-results heading suggesting the user adjust their query.', comment: 'Body copy under the no-results heading suggesting the user adjust their query.',
}); });
export const NOTHING_TO_SEARCH_FOR_DESCRIPTOR = msg({
message: 'Nothing to search for',
comment: 'Heading shown when the submitted search query produced no filter and no text the server could search on.',
});
export const NO_PART_OF_THIS_QUERY_APPLIED_DESCRIPTOR = msg({
message: 'No part of {query} could be applied. Fix the underlined value or add something to search for.',
comment:
'Body copy under the nothing-to-search-for heading. Preserve {query}; it is the text the user submitted and is inserted by code.',
});
export const GO_TO_PAGE_DESCRIPTOR = msg({ export const GO_TO_PAGE_DESCRIPTOR = msg({
message: 'Go to page {page}', message: 'Go to page {page}',
comment: 'Accessible label of each pagination page button. page is the 1-indexed page number.', comment: 'Accessible label of each pagination page button. page is the 1-indexed page number.',
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const isElementNode = (node: Node): boolean => {
const view = node.ownerDocument?.defaultView;
if (view == null) return false;
return node instanceof view.Element;
};
export const shouldSearchResultRowJump = (target: Node | null, container: Node | null): boolean => {
const selection = window?.getSelection();
if (selection != null && !selection.isCollapsed) return false;
if (target == null || container == null) return true;
let node: Node | null = target;
while (node != null && isElementNode(node) && node !== container) {
const {tagName} = node as Element;
if (tagName === 'A' || tagName === 'BUTTON') return false;
if (tagName === 'IMG' && (node as Element).getAttribute('aria-hidden') !== 'true') return false;
node = node.parentNode;
}
return true;
};
@@ -4,14 +4,16 @@ import styles from '@app/features/channel/components/ChannelSearchResults.module
import { import {
ERROR_DESCRIPTOR, ERROR_DESCRIPTOR,
INDEXING_CHANNEL_DESCRIPTOR, INDEXING_CHANNEL_DESCRIPTOR,
NO_PART_OF_THIS_QUERY_APPLIED_DESCRIPTOR,
NO_RESULTS_DESCRIPTOR, NO_RESULTS_DESCRIPTOR,
NOTHING_TO_SEARCH_FOR_DESCRIPTOR,
TRY_A_DIFFERENT_SEARCH_QUERY_DESCRIPTOR, TRY_A_DIFFERENT_SEARCH_QUERY_DESCRIPTOR,
WE_RE_INDEXING_THIS_CHANNEL_FOR_THE_FIRST_DESCRIPTOR, WE_RE_INDEXING_THIS_CHANNEL_FOR_THE_FIRST_DESCRIPTOR,
} from '@app/features/channel/components/channel_search_results/ChannelSearchResultsShared'; } from '@app/features/channel/components/channel_search_results/ChannelSearchResultsShared';
import {TRY_AGAIN_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import {TRY_AGAIN_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {Button} from '@app/features/ui/button/Button'; import {Button} from '@app/features/ui/button/Button';
import {useLingui} from '@lingui/react/macro'; import {useLingui} from '@lingui/react/macro';
import {CircleNotchIcon, MagnifyingGlassIcon} from '@phosphor-icons/react'; import {CircleNotchIcon, MagnifyingGlassIcon, WarningCircleIcon} from '@phosphor-icons/react';
import type React from 'react'; import type React from 'react';
export const SearchIndexingState: React.FC = () => { export const SearchIndexingState: React.FC = () => {
@@ -95,3 +97,43 @@ export const SearchEmptyState: React.FC = () => {
</div> </div>
); );
}; };
interface SearchUnappliedQueryStateProps {
query: string;
}
export const SearchUnappliedQueryState: React.FC<SearchUnappliedQueryStateProps> = ({query}) => {
const {i18n} = useLingui();
return (
<div className={styles.emptyState} data-flx="channel.channel-search-results.render-content.unapplied-state">
<div
className={styles.emptyStateContent}
data-flx="channel.channel-search-results.render-content.unapplied-state-content"
>
<WarningCircleIcon
className={styles.emptyStateIcon}
data-flx="channel.channel-search-results.render-content.unapplied-state-icon"
/>
<div
className={styles.emptyStateTextWrapper}
data-flx="channel.channel-search-results.render-content.unapplied-state-text-wrapper"
>
<h3
className={styles.emptyStateHeading}
role="status"
aria-live="polite"
data-flx="channel.channel-search-results.render-content.unapplied-state-heading"
>
{i18n._(NOTHING_TO_SEARCH_FOR_DESCRIPTOR)}
</h3>
<p
className={styles.emptyStateText}
data-flx="channel.channel-search-results.render-content.unapplied-state-text"
>
{i18n._(NO_PART_OF_THIS_QUERY_APPLIED_DESCRIPTOR, {query})}
</p>
</div>
</div>
</div>
);
};
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
applyChannelSearchHighlight,
clearChannelSearchHighlight,
} from '@app/features/messaging/utils/ChannelSearchHighlight';
import {tokenizeSearchQuery} from '@app/features/search/utils/SearchQueryTokenizer';
import {useLayoutEffect} from 'react';
interface UseChannelSearchHighlightRequest {
isSuccess: boolean;
searchQuery: string;
resultsRevision: unknown;
getHighlightRoot: () => HTMLElement | null;
}
export function useChannelSearchHighlight({
isSuccess,
searchQuery,
resultsRevision,
getHighlightRoot,
}: UseChannelSearchHighlightRequest): void {
useLayoutEffect(() => {
if (!isSuccess || !searchQuery.trim()) {
clearChannelSearchHighlight();
return;
}
const root = getHighlightRoot();
if (root == null) return;
const terms = tokenizeSearchQuery(searchQuery);
if (terms.length > 0) {
applyChannelSearchHighlight(root, terms);
}
return () => {
clearChannelSearchHighlight();
};
}, [isSuccess, searchQuery, resultsRevision]);
}
@@ -4,6 +4,7 @@ import type {Channel} from '@app/features/channel/models/Channel';
import ChannelSearch, {getChannelSearchContextId} from '@app/features/channel/state/ChannelSearch'; import ChannelSearch, {getChannelSearchContextId} from '@app/features/channel/state/ChannelSearch';
import SelectedGuild from '@app/features/navigation/state/SelectedGuild'; import SelectedGuild from '@app/features/navigation/state/SelectedGuild';
import type {SearchSegment} from '@app/features/search/utils/SearchSegmentManager'; import type {SearchSegment} from '@app/features/search/utils/SearchSegmentManager';
import {hasSearchableParams, parseSearchQueryWithSegments} from '@app/features/search/utils/SearchUtils';
import {useCallback, useMemo} from 'react'; import {useCallback, useMemo} from 'react';
interface UseChannelSearchStateReturn { interface UseChannelSearchStateReturn {
@@ -40,10 +41,24 @@ export const useChannelSearchState = (channel?: Channel): UseChannelSearchStateR
if (!contextId) { if (!contextId) {
return; return;
} }
const submitContext = ChannelSearch.getContext(contextId);
if (submitContext?.machineState.status === 'loading' && submitContext.activeSearchQuery === query) {
return;
}
ChannelSearch.setSearchInput(contextId, query, segments); ChannelSearch.setSearchInput(contextId, query, segments);
const params = parseSearchQueryWithSegments(query, segments, {
historyKey: contextId,
guildId: channel?.guildId,
});
if (!hasSearchableParams(params)) {
if (query.trim().length > 0) {
ChannelSearch.setUnsearchableSearch(contextId, query);
}
return;
}
ChannelSearch.setActiveSearch(contextId, query, segments); ChannelSearch.setActiveSearch(contextId, query, segments);
}, },
[contextId], [contextId, channel?.id, channel?.guildId],
); );
const handleSearchClose = useCallback(() => { const handleSearchClose = useCallback(() => {
if (!contextId) { if (!contextId) {
@@ -24,6 +24,7 @@ class ChannelSearchContext {
activeSearchQuery: string = ''; activeSearchQuery: string = '';
activeSearchSegments: Array<SearchSegment> = []; activeSearchSegments: Array<SearchSegment> = [];
isSearchActive = false; isSearchActive = false;
unsearchableQuery = '';
isInputFocused = false; isInputFocused = false;
searchRefreshKey = 0; searchRefreshKey = 0;
machineSnapshot: SearchMachineSnapshot = createSearchMachineSnapshot(); machineSnapshot: SearchMachineSnapshot = createSearchMachineSnapshot();
@@ -74,10 +75,28 @@ class ChannelSearch {
const context = this.getContext(contextId); const context = this.getContext(contextId);
context.activeSearchQuery = query; context.activeSearchQuery = query;
context.activeSearchSegments = [...segments]; context.activeSearchSegments = [...segments];
context.unsearchableQuery = '';
context.isSearchActive = true; context.isSearchActive = true;
context.searchRefreshKey += 1; context.searchRefreshKey += 1;
} }
setUnsearchableSearch(contextId: string, query: string): void {
const context = this.getContext(contextId);
context.activeSearchQuery = '';
context.activeSearchSegments = [];
context.unsearchableQuery = query;
context.isSearchActive = true;
context.searchRefreshKey += 1;
context.lastSearchQuery = '';
context.lastSearchSegments = [];
context.lastSearchRefreshKey = null;
context.lastSearchScope = null;
context.lastSearchSortMode = null;
context.machineSnapshot = transitionSearchMachineSnapshot(context.machineSnapshot, {
type: 'channelSearch.reset',
});
}
setIsSearchActive(contextId: string, value: boolean): void { setIsSearchActive(contextId: string, value: boolean): void {
const context = this.getContext(contextId); const context = this.getContext(contextId);
context.isSearchActive = value; context.isSearchActive = value;
@@ -94,6 +113,7 @@ class ChannelSearch {
context.searchSegments = []; context.searchSegments = [];
context.activeSearchQuery = ''; context.activeSearchQuery = '';
context.activeSearchSegments = []; context.activeSearchSegments = [];
context.unsearchableQuery = '';
context.isSearchActive = false; context.isSearchActive = false;
context.searchRefreshKey = 0; context.searchRefreshKey = 0;
context.lastSearchRefreshKey = null; context.lastSearchRefreshKey = null;