mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(app): tell the user when a search query has nothing to search (#1901)
This commit is contained in:
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
|
||||||
|
|||||||
+9
@@ -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.',
|
||||||
|
|||||||
+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 {
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+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 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;
|
||||||
|
|||||||
Reference in new issue
Block a user