mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-03 05:10:25 +03:00
fix(app): tell the user when a search query has nothing to search (#1901)
This commit is contained in:
@@ -260,9 +260,6 @@ export const ChannelHeader = observer(
|
||||
);
|
||||
const handleSearchSubmit = useCallback(() => {
|
||||
const query = latestSearchQueryRef.current;
|
||||
if (!query.trim()) {
|
||||
return;
|
||||
}
|
||||
if (onSearchSubmit) {
|
||||
onSearchSubmit(query, latestSearchSegmentsRef.current);
|
||||
return;
|
||||
|
||||
@@ -20,13 +20,16 @@ import {
|
||||
renderScopeIcon,
|
||||
renderSortIcon,
|
||||
} 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 {SearchResultsPagination} from '@app/features/channel/components/channel_search_results/SearchResultsPagination';
|
||||
import {
|
||||
SearchEmptyState,
|
||||
SearchErrorState,
|
||||
SearchIndexingState,
|
||||
SearchUnappliedQueryState,
|
||||
} 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 {
|
||||
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 {useMessageListKeyboardNavigation} from '@app/features/messaging/hooks/useMessageListKeyboardNavigation';
|
||||
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 {
|
||||
applyChannelSearchHighlight,
|
||||
clearChannelSearchHighlight,
|
||||
} from '@app/features/messaging/utils/ChannelSearchHighlight';
|
||||
import {focusChannelTextareaAfterNavigation} from '@app/features/messaging/utils/ChannelTextareaFocusUtils';
|
||||
import {getCollapsedMessageGroupKey} from '@app/features/messaging/utils/MessageGroupingUtils';
|
||||
import LocalUserSpamOverride from '@app/features/moderation/state/LocalUserSpamOverride';
|
||||
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
|
||||
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 {
|
||||
isIndexing,
|
||||
@@ -95,6 +94,10 @@ export const ChannelSearchResults = observer(
|
||||
({channel, searchQuery, searchSegments, onClose, refreshKey}: ChannelSearchResultsProps) => {
|
||||
const {i18n} = useLingui();
|
||||
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
||||
const resolveSearchResultsScrollSurface = useMemo(
|
||||
() => () => scrollerRef.current?.getViewportElement() ?? null,
|
||||
[],
|
||||
);
|
||||
const pollingTimeout = useRef<number | null>(null);
|
||||
const currentChannelId = useRef(channel.id);
|
||||
const currentSearchQuery = useRef(searchQuery);
|
||||
@@ -114,6 +117,7 @@ export const ChannelSearchResults = observer(
|
||||
const machineState = searchContext?.machineState ?? {status: 'loading' as const};
|
||||
const scrollPosition = searchContext?.scrollPosition ?? 0;
|
||||
const lastKnownScrollPosition = useRef(scrollPosition);
|
||||
const unsearchableQuery = searchContext?.unsearchableQuery ?? '';
|
||||
const successMachineState = machineState.status === 'success' ? machineState : null;
|
||||
const indexingMachineState = machineState.status === 'indexing' ? machineState : null;
|
||||
const normalizedRefreshKey = refreshKey ?? null;
|
||||
@@ -559,6 +563,16 @@ export const ChannelSearchResults = observer(
|
||||
},
|
||||
[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(
|
||||
(page: number) => {
|
||||
resetScrollerToTop();
|
||||
@@ -567,6 +581,14 @@ export const ChannelSearchResults = observer(
|
||||
[resetScrollerToTop, performSearch],
|
||||
);
|
||||
const renderContent = useCallback(() => {
|
||||
if (unsearchableQuery !== '') {
|
||||
return (
|
||||
<SearchUnappliedQueryState
|
||||
query={unsearchableQuery}
|
||||
data-flx="channel.channel-search-results.render-content.search-unapplied-query-state"
|
||||
/>
|
||||
);
|
||||
}
|
||||
switch (machineState.status) {
|
||||
case 'idle':
|
||||
case 'loading':
|
||||
@@ -592,132 +614,147 @@ export const ChannelSearchResults = observer(
|
||||
value={collapsedMessageVisibility}
|
||||
data-flx="channel.channel-search-results.render-content.collapsed-message-visibility-provider"
|
||||
>
|
||||
<Scroller
|
||||
ref={setScrollerRef}
|
||||
className={styles.resultsScroller}
|
||||
onScroll={handleScrollerScroll}
|
||||
fade={false}
|
||||
key="channel-search-results-scroller-desktop"
|
||||
onCopy={onCopySelectedMessages}
|
||||
data-message-selection-root="true"
|
||||
data-flx="channel.channel-search-results.render-content.results-scroller"
|
||||
>
|
||||
{resultGroups.map((resultGroup) => {
|
||||
const renderData = getSearchResultChannelRenderData(
|
||||
resultGroup.channelId,
|
||||
searchChannelsById,
|
||||
(activeScope ?? DEFAULT_SCOPE_VALUE) as MessageSearchScope,
|
||||
);
|
||||
if (!renderData) return null;
|
||||
const {messageChannel, showGuildMeta} = renderData;
|
||||
const renderMessageActions = (message: Message) => (
|
||||
<FocusRing
|
||||
offset={-2}
|
||||
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"
|
||||
<NearViewportSurfaceContext.Provider value={resolveSearchResultsScrollSurface}>
|
||||
<Scroller
|
||||
ref={setScrollerRef}
|
||||
className={styles.resultsScroller}
|
||||
onScroll={handleScrollerScroll}
|
||||
fade={false}
|
||||
key="channel-search-results-scroller-desktop"
|
||||
onCopy={onCopySelectedMessages}
|
||||
data-message-selection-root="true"
|
||||
data-flx="channel.channel-search-results.render-content.results-scroller"
|
||||
>
|
||||
{resultGroups.map((resultGroup) => {
|
||||
const renderData = getSearchResultChannelRenderData(
|
||||
resultGroup.channelId,
|
||||
searchChannelsById,
|
||||
(activeScope ?? DEFAULT_SCOPE_VALUE) as MessageSearchScope,
|
||||
);
|
||||
if (!renderData) return null;
|
||||
const {messageChannel, showGuildMeta} = renderData;
|
||||
const renderMessageActions = (message: Message) => (
|
||||
<FocusRing
|
||||
offset={-2}
|
||||
ringClassName={styles.focusRingTight}
|
||||
data-flx="channel.channel-search-results.render-message-actions.focus-ring"
|
||||
>
|
||||
{i18n._(JUMP_DESCRIPTOR)}
|
||||
</button>
|
||||
</FocusRing>
|
||||
);
|
||||
const renderMessageWrapper = ({
|
||||
message,
|
||||
index,
|
||||
isGroupStart,
|
||||
children,
|
||||
}: MessageGroupRenderWrapperProps) => (
|
||||
<div
|
||||
data-message-index={index}
|
||||
data-message-id={message.id}
|
||||
data-is-group-start={isGroupStart}
|
||||
className={styles.messageItem}
|
||||
data-flx="channel.channel-search-results.render-message-wrapper.message-item"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<React.Fragment key={resultGroup.key}>
|
||||
<MessageContextPrefix
|
||||
channel={messageChannel}
|
||||
showGuildMeta={showGuildMeta}
|
||||
onClick={() => {
|
||||
void handleSearchChannelOpen(messageChannel);
|
||||
<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>
|
||||
</FocusRing>
|
||||
);
|
||||
const renderMessageWrapper = ({
|
||||
message,
|
||||
index,
|
||||
isGroupStart,
|
||||
children,
|
||||
}: MessageGroupRenderWrapperProps) => (
|
||||
// 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
|
||||
data-message-index={index}
|
||||
data-message-id={message.id}
|
||||
data-is-group-start={isGroupStart}
|
||||
className={styles.messageItem}
|
||||
onClick={(event) => {
|
||||
handleResultRowClick(event, messageChannel, message);
|
||||
}}
|
||||
data-flx="channel.channel-search-results.render-content.message-context-prefix"
|
||||
/>
|
||||
<SearchResultMessageList
|
||||
channel={messageChannel}
|
||||
messages={resultGroup.messages}
|
||||
revealedGroupKeys={revealedGroupKeys}
|
||||
onGroupRevealChange={handleCollapsedGroupRevealChange}
|
||||
collapsedGroupClassName={styles.collapsedMessageGroup}
|
||||
messagePreviewContext={MessagePreviewContext.LIST_POPOUT}
|
||||
messageBehaviorOverrides={DETACHED_MESSAGE_BEHAVIOR}
|
||||
messageActionsClassName={styles.actionButtons}
|
||||
renderMessageActions={renderMessageActions}
|
||||
renderMessageWrapper={renderMessageWrapper}
|
||||
spammerOverrideVersion={spammerOverrideVersion}
|
||||
renderMessage={(message) => (
|
||||
<div
|
||||
className={styles.messageItem}
|
||||
data-message-id={message.id}
|
||||
data-is-group-start="true"
|
||||
data-flx="channel.channel-search-results.render-content.message-item"
|
||||
>
|
||||
<MessageComponent
|
||||
message={message}
|
||||
channel={messageChannel}
|
||||
previewContext={MessagePreviewContext.LIST_POPOUT}
|
||||
behaviorOverrides={DETACHED_MESSAGE_BEHAVIOR}
|
||||
data-flx="channel.channel-search-results.render-content.message-component"
|
||||
/>
|
||||
data-flx="channel.channel-search-results.render-message-wrapper.message-item"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<React.Fragment key={resultGroup.key}>
|
||||
<MessageContextPrefix
|
||||
channel={messageChannel}
|
||||
showGuildMeta={showGuildMeta}
|
||||
onClick={() => {
|
||||
void handleSearchChannelOpen(messageChannel);
|
||||
}}
|
||||
data-flx="channel.channel-search-results.render-content.message-context-prefix"
|
||||
/>
|
||||
<SearchResultMessageList
|
||||
channel={messageChannel}
|
||||
messages={resultGroup.messages}
|
||||
revealedGroupKeys={revealedGroupKeys}
|
||||
onGroupRevealChange={handleCollapsedGroupRevealChange}
|
||||
collapsedGroupClassName={styles.collapsedMessageGroup}
|
||||
messagePreviewContext={MessagePreviewContext.LIST_POPOUT}
|
||||
messageBehaviorOverrides={DETACHED_MESSAGE_BEHAVIOR}
|
||||
messageActionsClassName={styles.actionButtons}
|
||||
renderMessageActions={renderMessageActions}
|
||||
renderMessageWrapper={renderMessageWrapper}
|
||||
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
|
||||
className={styles.actionButtons}
|
||||
data-flx="channel.channel-search-results.render-content.action-buttons"
|
||||
className={styles.messageItem}
|
||||
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>
|
||||
)}
|
||||
data-flx="channel.channel-search-results.render-content.search-result-message-list"
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
className={styles.resultsSpacer}
|
||||
data-flx="channel.channel-search-results.render-content.results-spacer"
|
||||
/>
|
||||
<SearchResultsPagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
visiblePageSlots={visiblePageSlots}
|
||||
onJumpToPage={handlePaginationJump}
|
||||
data-flx="channel.channel-search-results.render-content.search-results-pagination"
|
||||
/>
|
||||
</Scroller>
|
||||
)}
|
||||
data-flx="channel.channel-search-results.render-content.search-result-message-list"
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
className={styles.resultsSpacer}
|
||||
data-flx="channel.channel-search-results.render-content.results-spacer"
|
||||
/>
|
||||
<SearchResultsPagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
visiblePageSlots={visiblePageSlots}
|
||||
onJumpToPage={handlePaginationJump}
|
||||
data-flx="channel.channel-search-results.render-content.search-results-pagination"
|
||||
/>
|
||||
</Scroller>
|
||||
</NearViewportSurfaceContext.Provider>
|
||||
</CollapsedMessageVisibilityProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
unsearchableQuery,
|
||||
machineState,
|
||||
performSearch,
|
||||
setScrollerRef,
|
||||
resolveSearchResultsScrollSurface,
|
||||
handleScrollerScroll,
|
||||
visiblePageSlots,
|
||||
activeScope,
|
||||
handleSearchChannelOpen,
|
||||
handleSearchMessageJump,
|
||||
handleResultRowClick,
|
||||
handlePaginationJump,
|
||||
onCopySelectedMessages,
|
||||
collapsedMessageVisibility,
|
||||
@@ -822,24 +859,12 @@ export const ChannelSearchResults = observer(
|
||||
updateScrollPosition(lastKnownScrollPosition.current);
|
||||
};
|
||||
}, [stopPolling, updateScrollPosition]);
|
||||
useEffect(() => {
|
||||
if (machineState.status !== 'success' || !searchQuery.trim()) {
|
||||
clearChannelSearchHighlight();
|
||||
return;
|
||||
}
|
||||
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]);
|
||||
useChannelSearchHighlight({
|
||||
isSuccess: machineState.status === 'success',
|
||||
searchQuery,
|
||||
resultsRevision: successMachineState?.results,
|
||||
getHighlightRoot: () => scrollerRef.current?.getViewportElement() ?? null,
|
||||
});
|
||||
return (
|
||||
<div className={styles.container} data-flx="channel.channel-search-results.container">
|
||||
<SearchResultsHeader
|
||||
|
||||
+9
@@ -79,6 +79,15 @@ export const TRY_A_DIFFERENT_SEARCH_QUERY_DESCRIPTOR = msg({
|
||||
message: 'Try a different search 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({
|
||||
message: 'Go to page {page}',
|
||||
comment: 'Accessible label of each pagination page button. page is the 1-indexed page number.',
|
||||
|
||||
+21
@@ -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;
|
||||
};
|
||||
+43
-1
@@ -4,14 +4,16 @@ import styles from '@app/features/channel/components/ChannelSearchResults.module
|
||||
import {
|
||||
ERROR_DESCRIPTOR,
|
||||
INDEXING_CHANNEL_DESCRIPTOR,
|
||||
NO_PART_OF_THIS_QUERY_APPLIED_DESCRIPTOR,
|
||||
NO_RESULTS_DESCRIPTOR,
|
||||
NOTHING_TO_SEARCH_FOR_DESCRIPTOR,
|
||||
TRY_A_DIFFERENT_SEARCH_QUERY_DESCRIPTOR,
|
||||
WE_RE_INDEXING_THIS_CHANNEL_FOR_THE_FIRST_DESCRIPTOR,
|
||||
} from '@app/features/channel/components/channel_search_results/ChannelSearchResultsShared';
|
||||
import {TRY_AGAIN_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
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';
|
||||
|
||||
export const SearchIndexingState: React.FC = () => {
|
||||
@@ -95,3 +97,43 @@ export const SearchEmptyState: React.FC = () => {
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
+38
@@ -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 SelectedGuild from '@app/features/navigation/state/SelectedGuild';
|
||||
import type {SearchSegment} from '@app/features/search/utils/SearchSegmentManager';
|
||||
import {hasSearchableParams, parseSearchQueryWithSegments} from '@app/features/search/utils/SearchUtils';
|
||||
import {useCallback, useMemo} from 'react';
|
||||
|
||||
interface UseChannelSearchStateReturn {
|
||||
@@ -40,10 +41,24 @@ export const useChannelSearchState = (channel?: Channel): UseChannelSearchStateR
|
||||
if (!contextId) {
|
||||
return;
|
||||
}
|
||||
const submitContext = ChannelSearch.getContext(contextId);
|
||||
if (submitContext?.machineState.status === 'loading' && submitContext.activeSearchQuery === query) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
},
|
||||
[contextId],
|
||||
[contextId, channel?.id, channel?.guildId],
|
||||
);
|
||||
const handleSearchClose = useCallback(() => {
|
||||
if (!contextId) {
|
||||
|
||||
@@ -24,6 +24,7 @@ class ChannelSearchContext {
|
||||
activeSearchQuery: string = '';
|
||||
activeSearchSegments: Array<SearchSegment> = [];
|
||||
isSearchActive = false;
|
||||
unsearchableQuery = '';
|
||||
isInputFocused = false;
|
||||
searchRefreshKey = 0;
|
||||
machineSnapshot: SearchMachineSnapshot = createSearchMachineSnapshot();
|
||||
@@ -74,10 +75,28 @@ class ChannelSearch {
|
||||
const context = this.getContext(contextId);
|
||||
context.activeSearchQuery = query;
|
||||
context.activeSearchSegments = [...segments];
|
||||
context.unsearchableQuery = '';
|
||||
context.isSearchActive = true;
|
||||
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 {
|
||||
const context = this.getContext(contextId);
|
||||
context.isSearchActive = value;
|
||||
@@ -94,6 +113,7 @@ class ChannelSearch {
|
||||
context.searchSegments = [];
|
||||
context.activeSearchQuery = '';
|
||||
context.activeSearchSegments = [];
|
||||
context.unsearchableQuery = '';
|
||||
context.isSearchActive = false;
|
||||
context.searchRefreshKey = 0;
|
||||
context.lastSearchRefreshKey = null;
|
||||
|
||||
Reference in New Issue
Block a user