mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(app): keep search history per conversation and allow in: in DMs (#1886)
This commit is contained in:
1 parent
406340fa65
commit
202856c0f7
4 files changed
+68
-31
No files matched your search
@@ -254,7 +254,7 @@ export const ChannelSearchResults = observer(
|
|||||||
(page: number, scopeOverride?: MessageSearchScope | null): MessageSearchParams => {
|
(page: number, scopeOverride?: MessageSearchScope | null): MessageSearchParams => {
|
||||||
const params: MessageSearchParams = {
|
const params: MessageSearchParams = {
|
||||||
...parseSearchQueryWithSegments(currentSearchQuery.current, currentSearchSegments.current, {
|
...parseSearchQueryWithSegments(currentSearchQuery.current, currentSearchSegments.current, {
|
||||||
channelId: channelId.current,
|
historyKey: contextId ?? undefined,
|
||||||
guildId: channelGuildId.current,
|
guildId: channelGuildId.current,
|
||||||
}),
|
}),
|
||||||
page,
|
page,
|
||||||
@@ -265,7 +265,7 @@ export const ChannelSearchResults = observer(
|
|||||||
applyMatureContentToParamsIfNeeded(params, channelId.current);
|
applyMatureContentToParamsIfNeeded(params, channelId.current);
|
||||||
return params;
|
return params;
|
||||||
},
|
},
|
||||||
[applyScopeToParams],
|
[applyScopeToParams, contextId],
|
||||||
);
|
);
|
||||||
const performSearch = useCallback(
|
const performSearch = useCallback(
|
||||||
async (page = 1, sortOverride?: ChannelSearchSortMode, scopeOverride?: MessageSearchScope | null) => {
|
async (page = 1, sortOverride?: ChannelSearchSortMode, scopeOverride?: MessageSearchScope | null) => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
transitionSearchMachineSnapshot,
|
transitionSearchMachineSnapshot,
|
||||||
} from '@app/features/channel/components/SearchStateMachine';
|
} from '@app/features/channel/components/SearchStateMachine';
|
||||||
import type {Channel} from '@app/features/channel/models/Channel';
|
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 Channels from '@app/features/channel/state/Channels';
|
||||||
import {getChannelSearchIndexingPollInterval} from '@app/features/channel/utils/ChannelSearchPolling';
|
import {getChannelSearchIndexingPollInterval} from '@app/features/channel/utils/ChannelSearchPolling';
|
||||||
import GuildMatureContentAgree from '@app/features/guild/state/GuildMatureContentAgree';
|
import GuildMatureContentAgree from '@app/features/guild/state/GuildMatureContentAgree';
|
||||||
@@ -269,7 +270,7 @@ export const useChannelSearch = ({
|
|||||||
currentSegmentsRef.current = segments;
|
currentSegmentsRef.current = segments;
|
||||||
currentFiltersRef.current = null;
|
currentFiltersRef.current = null;
|
||||||
const params = parseSearchQueryWithSegments(query, segments, {
|
const params = parseSearchQueryWithSegments(query, segments, {
|
||||||
channelId: channel.id,
|
historyKey: getChannelSearchContextId(channel) ?? undefined,
|
||||||
guildId: channel.guildId ?? null,
|
guildId: channel.guildId ?? null,
|
||||||
});
|
});
|
||||||
await executeSearch(params, page, overrides);
|
await executeSearch(params, page, overrides);
|
||||||
@@ -293,7 +294,7 @@ export const useChannelSearch = ({
|
|||||||
executeSearch(params, page);
|
executeSearch(params, page);
|
||||||
} else if (currentQueryRef.current) {
|
} else if (currentQueryRef.current) {
|
||||||
const params = parseSearchQueryWithSegments(currentQueryRef.current, currentSegmentsRef.current, {
|
const params = parseSearchQueryWithSegments(currentQueryRef.current, currentSegmentsRef.current, {
|
||||||
channelId: channel.id,
|
historyKey: getChannelSearchContextId(channel) ?? undefined,
|
||||||
guildId: channel.guildId ?? null,
|
guildId: channel.guildId ?? null,
|
||||||
});
|
});
|
||||||
executeSearch(params, page);
|
executeSearch(params, page);
|
||||||
@@ -364,7 +365,7 @@ export const useChannelSearch = ({
|
|||||||
params = filtersToParams(currentFiltersRef.current);
|
params = filtersToParams(currentFiltersRef.current);
|
||||||
} else {
|
} else {
|
||||||
params = parseSearchQueryWithSegments(currentQueryRef.current, currentSegmentsRef.current, {
|
params = parseSearchQueryWithSegments(currentQueryRef.current, currentSegmentsRef.current, {
|
||||||
channelId: channel.id,
|
historyKey: getChannelSearchContextId(channel) ?? undefined,
|
||||||
guildId: channel.guildId ?? null,
|
guildId: channel.guildId ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import {makePersistent} from '@app/features/platform/utils/MobXPersistence';
|
|||||||
import type {SearchHints} from '@app/features/search/utils/SearchQueryParser';
|
import type {SearchHints} from '@app/features/search/utils/SearchQueryParser';
|
||||||
import {action, makeAutoObservable} from 'mobx';
|
import {action, makeAutoObservable} from 'mobx';
|
||||||
|
|
||||||
|
export const SEARCH_HISTORY_LIMIT = 10;
|
||||||
|
export const SEARCH_HISTORY_DISPLAY_LIMIT = 5;
|
||||||
|
|
||||||
export interface SearchHistoryEntry {
|
export interface SearchHistoryEntry {
|
||||||
query: string;
|
query: string;
|
||||||
hints?: SearchHints;
|
hints?: SearchHints;
|
||||||
@@ -44,17 +47,42 @@ class SearchHistory {
|
|||||||
}
|
}
|
||||||
const entries = this.entriesByChannel[channelId];
|
const entries = this.entriesByChannel[channelId];
|
||||||
const ts = Date.now();
|
const ts = Date.now();
|
||||||
const existingIdx = entries.findIndex((e) => e.query === q);
|
|
||||||
const entry: SearchHistoryEntry = {query: q, hints, ts};
|
const entry: SearchHistoryEntry = {query: q, hints, ts};
|
||||||
|
const existingIdx = entries.findIndex((e) => e.query === q);
|
||||||
if (existingIdx !== -1) {
|
if (existingIdx !== -1) {
|
||||||
entries.splice(existingIdx, 1);
|
entries.splice(existingIdx, 1);
|
||||||
}
|
}
|
||||||
entries.unshift(entry);
|
entries.unshift(entry);
|
||||||
if (entries.length > 10) {
|
if (entries.length > SEARCH_HISTORY_LIMIT) {
|
||||||
this.entriesByChannel[channelId] = entries.slice(0, 10);
|
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
|
@action
|
||||||
clear(channelId?: string): void {
|
clear(channelId?: string): void {
|
||||||
if (!channelId) return;
|
if (!channelId) return;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {Channel} from '@app/features/channel/models/Channel';
|
|||||||
import {Message} from '@app/features/messaging/models/MessagingMessage';
|
import {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||||
import {http} from '@app/features/platform/transport/RestTransport';
|
import {http} from '@app/features/platform/transport/RestTransport';
|
||||||
import SearchHistory from '@app/features/search/state/SearchHistory';
|
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 {SearchSegment} from '@app/features/search/utils/SearchSegmentManager';
|
||||||
import type {Channel as WireChannel} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
import type {Channel as WireChannel} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||||
import type {Message as WireMessage} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
import type {Message as WireMessage} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||||
@@ -245,7 +245,7 @@ export interface SearchContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SearchQueryExecutionContext {
|
interface SearchQueryExecutionContext {
|
||||||
channelId?: string;
|
historyKey?: string;
|
||||||
guildId?: string | null;
|
guildId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,20 +299,11 @@ export interface SearchFilterOption {
|
|||||||
values?: Array<SearchValueOption>;
|
values?: Array<SearchValueOption>;
|
||||||
requiresValue?: boolean;
|
requiresValue?: boolean;
|
||||||
requiresGuild?: boolean;
|
requiresGuild?: boolean;
|
||||||
|
dmEligible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSearchFilterOptions(i18n: I18n): Array<SearchFilterOption> {
|
export function getSearchFilterOptions(i18n: I18n): Array<SearchFilterOption> {
|
||||||
const hasContentValues: Array<SearchValueOption> = [
|
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',
|
value: 'image',
|
||||||
label: i18n._(VALUE_IMAGE_LABEL_DESCRIPTOR),
|
label: i18n._(VALUE_IMAGE_LABEL_DESCRIPTOR),
|
||||||
@@ -324,9 +315,9 @@ export function getSearchFilterOptions(i18n: I18n): Array<SearchFilterOption> {
|
|||||||
description: i18n._(VALUE_VIDEO_DESCRIPTION_DESCRIPTOR),
|
description: i18n._(VALUE_VIDEO_DESCRIPTION_DESCRIPTOR),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'sound',
|
value: 'link',
|
||||||
label: i18n._(VALUE_SOUND_LABEL_DESCRIPTOR),
|
label: i18n._(VALUE_LINK_LABEL_DESCRIPTOR),
|
||||||
description: i18n._(VALUE_SOUND_DESCRIPTION_DESCRIPTOR),
|
description: i18n._(VALUE_LINK_DESCRIPTION_DESCRIPTOR),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'file',
|
value: 'file',
|
||||||
@@ -334,15 +325,25 @@ export function getSearchFilterOptions(i18n: I18n): Array<SearchFilterOption> {
|
|||||||
description: i18n._(VALUE_FILE_DESCRIPTION_DESCRIPTOR),
|
description: i18n._(VALUE_FILE_DESCRIPTION_DESCRIPTOR),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'sticker',
|
value: 'embed',
|
||||||
label: i18n._(VALUE_STICKER_LABEL_DESCRIPTOR),
|
label: i18n._(VALUE_EMBED_LABEL_DESCRIPTOR),
|
||||||
description: i18n._(VALUE_STICKER_DESCRIPTION_DESCRIPTOR),
|
description: i18n._(VALUE_EMBED_DESCRIPTION_DESCRIPTOR),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'sound',
|
||||||
|
label: i18n._(VALUE_SOUND_LABEL_DESCRIPTOR),
|
||||||
|
description: i18n._(VALUE_SOUND_DESCRIPTION_DESCRIPTOR),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'poll',
|
value: 'poll',
|
||||||
label: i18n._(VALUE_POLL_LABEL_DESCRIPTOR),
|
label: i18n._(VALUE_POLL_LABEL_DESCRIPTOR),
|
||||||
description: i18n._(VALUE_POLL_DESCRIPTION_DESCRIPTOR),
|
description: i18n._(VALUE_POLL_DESCRIPTION_DESCRIPTOR),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'sticker',
|
||||||
|
label: i18n._(VALUE_STICKER_LABEL_DESCRIPTOR),
|
||||||
|
description: i18n._(VALUE_STICKER_DESCRIPTION_DESCRIPTOR),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: 'forward',
|
value: 'forward',
|
||||||
label: i18n._(VALUE_FORWARD_LABEL_DESCRIPTOR),
|
label: i18n._(VALUE_FORWARD_LABEL_DESCRIPTOR),
|
||||||
@@ -427,6 +428,7 @@ export function getSearchFilterOptions(i18n: I18n): Array<SearchFilterOption> {
|
|||||||
syntax: 'in:',
|
syntax: 'in:',
|
||||||
requiresValue: true,
|
requiresValue: true,
|
||||||
requiresGuild: true,
|
requiresGuild: true,
|
||||||
|
dmEligible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '-in',
|
key: '-in',
|
||||||
@@ -435,6 +437,7 @@ export function getSearchFilterOptions(i18n: I18n): Array<SearchFilterOption> {
|
|||||||
syntax: '-in:',
|
syntax: '-in:',
|
||||||
requiresValue: true,
|
requiresValue: true,
|
||||||
requiresGuild: true,
|
requiresGuild: true,
|
||||||
|
dmEligible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'pinned',
|
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 {
|
function segmentMatchesQuery(query: string, segment: SearchSegment): boolean {
|
||||||
return (
|
return (
|
||||||
segment.start >= 0 && segment.end <= query.length && query.slice(segment.start, segment.end) === segment.displayText
|
segment.start >= 0 && segment.end <= query.length && query.slice(segment.start, segment.end) === segment.displayText
|
||||||
@@ -654,12 +653,21 @@ function applySegmentsToParams(
|
|||||||
return params;
|
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(
|
export function parseSearchQueryWithSegments(
|
||||||
query: string,
|
query: string,
|
||||||
segments: Array<SearchSegment>,
|
segments: Array<SearchSegment>,
|
||||||
ctx?: SearchQueryExecutionContext,
|
ctx?: SearchQueryExecutionContext,
|
||||||
): MessageSearchParams {
|
): 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 segmentHints = buildHintsFromSegments(segments);
|
||||||
const segmentUserValues = new Set<string>();
|
const segmentUserValues = new Set<string>();
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
|
|||||||
Reference in new issue
Block a user