diff --git a/fluxer_app/src/features/channel/components/ChannelSearchResults.tsx b/fluxer_app/src/features/channel/components/ChannelSearchResults.tsx index ec4abe7ad..86f804842 100644 --- a/fluxer_app/src/features/channel/components/ChannelSearchResults.tsx +++ b/fluxer_app/src/features/channel/components/ChannelSearchResults.tsx @@ -254,7 +254,7 @@ export const ChannelSearchResults = observer( (page: number, scopeOverride?: MessageSearchScope | null): MessageSearchParams => { const params: MessageSearchParams = { ...parseSearchQueryWithSegments(currentSearchQuery.current, currentSearchSegments.current, { - channelId: channelId.current, + historyKey: contextId ?? undefined, guildId: channelGuildId.current, }), page, @@ -265,7 +265,7 @@ export const ChannelSearchResults = observer( applyMatureContentToParamsIfNeeded(params, channelId.current); return params; }, - [applyScopeToParams], + [applyScopeToParams, contextId], ); const performSearch = useCallback( async (page = 1, sortOverride?: ChannelSearchSortMode, scopeOverride?: MessageSearchScope | null) => { diff --git a/fluxer_app/src/features/channel/hooks/useChannelSearch.ts b/fluxer_app/src/features/channel/hooks/useChannelSearch.ts index bbd2e5c1d..c21832fe8 100644 --- a/fluxer_app/src/features/channel/hooks/useChannelSearch.ts +++ b/fluxer_app/src/features/channel/hooks/useChannelSearch.ts @@ -13,6 +13,7 @@ import { transitionSearchMachineSnapshot, } from '@app/features/channel/components/SearchStateMachine'; import type {Channel} from '@app/features/channel/models/Channel'; +import {getChannelSearchContextId} from '@app/features/channel/state/ChannelSearch'; import Channels from '@app/features/channel/state/Channels'; import {getChannelSearchIndexingPollInterval} from '@app/features/channel/utils/ChannelSearchPolling'; import GuildMatureContentAgree from '@app/features/guild/state/GuildMatureContentAgree'; @@ -269,7 +270,7 @@ export const useChannelSearch = ({ currentSegmentsRef.current = segments; currentFiltersRef.current = null; const params = parseSearchQueryWithSegments(query, segments, { - channelId: channel.id, + historyKey: getChannelSearchContextId(channel) ?? undefined, guildId: channel.guildId ?? null, }); await executeSearch(params, page, overrides); @@ -293,7 +294,7 @@ export const useChannelSearch = ({ executeSearch(params, page); } else if (currentQueryRef.current) { const params = parseSearchQueryWithSegments(currentQueryRef.current, currentSegmentsRef.current, { - channelId: channel.id, + historyKey: getChannelSearchContextId(channel) ?? undefined, guildId: channel.guildId ?? null, }); executeSearch(params, page); @@ -364,7 +365,7 @@ export const useChannelSearch = ({ params = filtersToParams(currentFiltersRef.current); } else { params = parseSearchQueryWithSegments(currentQueryRef.current, currentSegmentsRef.current, { - channelId: channel.id, + historyKey: getChannelSearchContextId(channel) ?? undefined, guildId: channel.guildId ?? null, }); } diff --git a/fluxer_app/src/features/search/state/SearchHistory.ts b/fluxer_app/src/features/search/state/SearchHistory.ts index 7a5930e2c..407e174f7 100644 --- a/fluxer_app/src/features/search/state/SearchHistory.ts +++ b/fluxer_app/src/features/search/state/SearchHistory.ts @@ -4,6 +4,9 @@ import {makePersistent} from '@app/features/platform/utils/MobXPersistence'; import type {SearchHints} from '@app/features/search/utils/SearchQueryParser'; import {action, makeAutoObservable} from 'mobx'; +export const SEARCH_HISTORY_LIMIT = 10; +export const SEARCH_HISTORY_DISPLAY_LIMIT = 5; + export interface SearchHistoryEntry { query: string; hints?: SearchHints; @@ -44,17 +47,42 @@ class SearchHistory { } const entries = this.entriesByChannel[channelId]; const ts = Date.now(); - const existingIdx = entries.findIndex((e) => e.query === q); const entry: SearchHistoryEntry = {query: q, hints, ts}; + const existingIdx = entries.findIndex((e) => e.query === q); if (existingIdx !== -1) { entries.splice(existingIdx, 1); } entries.unshift(entry); - if (entries.length > 10) { - this.entriesByChannel[channelId] = entries.slice(0, 10); + if (entries.length > SEARCH_HISTORY_LIMIT) { + this.entriesByChannel[channelId] = entries.slice(0, SEARCH_HISTORY_LIMIT); } } + @action + adoptLegacyEntries(targetKey: string | undefined, legacyKeys: ReadonlyArray): void { + if (!targetKey) return; + if (this.getEntries(targetKey).length > 0) return; + const merged: Array = []; + for (const legacyKey of legacyKeys) { + if (legacyKey === targetKey) continue; + const legacyEntries = this.entriesByChannel[legacyKey]; + if (legacyEntries == null) continue; + merged.push(...legacyEntries); + delete this.entriesByChannel[legacyKey]; + } + if (merged.length === 0) return; + merged.sort((left, right) => right.ts - left.ts); + const seen = new Set(); + const adopted: Array = []; + for (const entry of merged) { + if (seen.has(entry.query)) continue; + seen.add(entry.query); + adopted.push(entry); + if (adopted.length >= SEARCH_HISTORY_LIMIT) break; + } + this.entriesByChannel[targetKey] = adopted; + } + @action clear(channelId?: string): void { if (!channelId) return; diff --git a/fluxer_app/src/features/search/utils/SearchUtils.ts b/fluxer_app/src/features/search/utils/SearchUtils.ts index 546e7b8b6..eec5c0c31 100644 --- a/fluxer_app/src/features/search/utils/SearchUtils.ts +++ b/fluxer_app/src/features/search/utils/SearchUtils.ts @@ -5,7 +5,7 @@ import {Channel} from '@app/features/channel/models/Channel'; import {Message} from '@app/features/messaging/models/MessagingMessage'; import {http} from '@app/features/platform/transport/RestTransport'; import SearchHistory from '@app/features/search/state/SearchHistory'; -import {parseQuery} from '@app/features/search/utils/SearchQueryParser'; +import {addUniqueSearchParam as addUnique, parseQuery} from '@app/features/search/utils/SearchQueryParser'; import type {SearchSegment} from '@app/features/search/utils/SearchSegmentManager'; import type {Channel as WireChannel} from '@fluxer/schema/src/domains/channel/ChannelSchemas'; import type {Message as WireMessage} from '@fluxer/schema/src/domains/message/MessageResponseSchemas'; @@ -245,7 +245,7 @@ export interface SearchContext { } interface SearchQueryExecutionContext { - channelId?: string; + historyKey?: string; guildId?: string | null; } @@ -299,20 +299,11 @@ export interface SearchFilterOption { values?: Array; requiresValue?: boolean; requiresGuild?: boolean; + dmEligible?: boolean; } export function getSearchFilterOptions(i18n: I18n): Array { const hasContentValues: Array = [ - { - value: 'link', - label: i18n._(VALUE_LINK_LABEL_DESCRIPTOR), - description: i18n._(VALUE_LINK_DESCRIPTION_DESCRIPTOR), - }, - { - value: 'embed', - label: i18n._(VALUE_EMBED_LABEL_DESCRIPTOR), - description: i18n._(VALUE_EMBED_DESCRIPTION_DESCRIPTOR), - }, { value: 'image', label: i18n._(VALUE_IMAGE_LABEL_DESCRIPTOR), @@ -324,9 +315,9 @@ export function getSearchFilterOptions(i18n: I18n): Array { description: i18n._(VALUE_VIDEO_DESCRIPTION_DESCRIPTOR), }, { - value: 'sound', - label: i18n._(VALUE_SOUND_LABEL_DESCRIPTOR), - description: i18n._(VALUE_SOUND_DESCRIPTION_DESCRIPTOR), + value: 'link', + label: i18n._(VALUE_LINK_LABEL_DESCRIPTOR), + description: i18n._(VALUE_LINK_DESCRIPTION_DESCRIPTOR), }, { value: 'file', @@ -334,15 +325,25 @@ export function getSearchFilterOptions(i18n: I18n): Array { description: i18n._(VALUE_FILE_DESCRIPTION_DESCRIPTOR), }, { - value: 'sticker', - label: i18n._(VALUE_STICKER_LABEL_DESCRIPTOR), - description: i18n._(VALUE_STICKER_DESCRIPTION_DESCRIPTOR), + value: 'embed', + label: i18n._(VALUE_EMBED_LABEL_DESCRIPTOR), + description: i18n._(VALUE_EMBED_DESCRIPTION_DESCRIPTOR), + }, + { + value: 'sound', + label: i18n._(VALUE_SOUND_LABEL_DESCRIPTOR), + description: i18n._(VALUE_SOUND_DESCRIPTION_DESCRIPTOR), }, { value: 'poll', label: i18n._(VALUE_POLL_LABEL_DESCRIPTOR), description: i18n._(VALUE_POLL_DESCRIPTION_DESCRIPTOR), }, + { + value: 'sticker', + label: i18n._(VALUE_STICKER_LABEL_DESCRIPTOR), + description: i18n._(VALUE_STICKER_DESCRIPTION_DESCRIPTOR), + }, { value: 'forward', label: i18n._(VALUE_FORWARD_LABEL_DESCRIPTOR), @@ -427,6 +428,7 @@ export function getSearchFilterOptions(i18n: I18n): Array { syntax: 'in:', requiresValue: true, requiresGuild: true, + dmEligible: true, }, { key: '-in', @@ -435,6 +437,7 @@ export function getSearchFilterOptions(i18n: I18n): Array { syntax: '-in:', requiresValue: true, requiresGuild: true, + dmEligible: true, }, { key: 'pinned', @@ -603,10 +606,6 @@ function buildHintsFromSegments(segments: Array) { }; } -function addUnique(values: Array | undefined, value: T): Array { - return values?.includes(value) ? values : [...(values ?? []), value]; -} - function segmentMatchesQuery(query: string, segment: SearchSegment): boolean { return ( segment.start >= 0 && segment.end <= query.length && query.slice(segment.start, segment.end) === segment.displayText @@ -654,12 +653,21 @@ function applySegmentsToParams( return params; } +export function hasSearchableParams(params: MessageSearchParams): boolean { + for (const value of Object.values(params)) { + if (value !== undefined) { + return true; + } + } + return false; +} + export function parseSearchQueryWithSegments( query: string, segments: Array, ctx?: SearchQueryExecutionContext, ): MessageSearchParams { - const entry = SearchHistory.recent(ctx?.channelId).find((historyEntry) => historyEntry.query === query); + const entry = SearchHistory.recent(ctx?.historyKey).find((historyEntry) => historyEntry.query === query); const segmentHints = buildHintsFromSegments(segments); const segmentUserValues = new Set(); for (const segment of segments) {