feat(app): keep search history per conversation and allow in: in DMs (#1886)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:39:34 +02:00
1 parent 406340fa65
commit 202856c0f7
4 files changed
+68 -31

No files matched your search

@@ -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<string>): void {
if (!targetKey) return;
if (this.getEntries(targetKey).length > 0) return;
const merged: Array<SearchHistoryEntry> = [];
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<string>();
const adopted: Array<SearchHistoryEntry> = [];
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;
@@ -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<SearchValueOption>;
requiresValue?: boolean;
requiresGuild?: boolean;
dmEligible?: boolean;
}
export function getSearchFilterOptions(i18n: I18n): Array<SearchFilterOption> {
const hasContentValues: Array<SearchValueOption> = [
{
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<SearchFilterOption> {
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<SearchFilterOption> {
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<SearchFilterOption> {
syntax: 'in:',
requiresValue: true,
requiresGuild: true,
dmEligible: true,
},
{
key: '-in',
@@ -435,6 +437,7 @@ export function getSearchFilterOptions(i18n: I18n): Array<SearchFilterOption> {
syntax: '-in:',
requiresValue: true,
requiresGuild: true,
dmEligible: true,
},
{
key: 'pinned',
@@ -603,10 +606,6 @@ function buildHintsFromSegments(segments: Array<SearchSegment>) {
};
}
function addUnique<T>(values: Array<T> | undefined, value: T): Array<T> {
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<SearchSegment>,
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<string>();
for (const segment of segments) {