feat(search): inline filters, and translate the search UI
The filters themselves already existed end to end — the server takes q, from, has, before and after, the API client passes them, and the popover has fields for each. What it lacked was discovery: the panel sits behind a button, so the capability was invisible. Typing 'de:fulano' or 'has:image' straight into the search box now applies the same filters. Keys are accepted in both languages, since the app is bilingual, and an unrecognised token falls back to being search text — otherwise a message containing a URL or 'algo:coisa' would become unfindable. Inline filters win over the panel's: whoever just typed one is expressing the more recent intent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useT } from '../../i18n';
|
||||
import { parseSearchQuery } from '../../utils/searchQuery';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useFloatingPosition } from '../../hooks/useFloatingPosition';
|
||||
import { isDmChannel, getChannelOrigin, getApiForOrigin } from '../../stores/spaceStore';
|
||||
@@ -103,6 +105,7 @@ export function SearchPopover({ open, onClose, anchorRef, channelId, isDm, onJum
|
||||
});
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const t = useT();
|
||||
const [fromFilter, setFromFilter] = useState('');
|
||||
const [hasFilter, setHasFilter] = useState('');
|
||||
const [beforeFilter, setBeforeFilter] = useState('');
|
||||
@@ -153,8 +156,16 @@ export function SearchPopover({ open, onClose, anchorRef, channelId, isDm, onJum
|
||||
}, [open, onClose]);
|
||||
|
||||
const doSearch = useCallback(async (searchOffset = 0) => {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed && !fromFilter && !hasFilter && !beforeFilter && !afterFilter) {
|
||||
// Filtros digitados na consulta (`de:fulano`) valem sobre os do painel:
|
||||
// quem acabou de escrever está expressando a intenção mais recente.
|
||||
const parsed = parseSearchQuery(query);
|
||||
const trimmed = parsed.text;
|
||||
const effFrom = parsed.from ?? fromFilter;
|
||||
const effHas = parsed.has ?? hasFilter;
|
||||
const effBefore = parsed.before ?? beforeFilter;
|
||||
const effAfter = parsed.after ?? afterFilter;
|
||||
|
||||
if (!trimmed && !effFrom && !effHas && !effBefore && !effAfter) {
|
||||
setResults([]);
|
||||
setTotalCount(0);
|
||||
return;
|
||||
@@ -166,10 +177,10 @@ export function SearchPopover({ open, onClose, anchorRef, channelId, isDm, onJum
|
||||
const client = getApiForOrigin(origin);
|
||||
const params = {
|
||||
q: trimmed || undefined,
|
||||
from: fromFilter || undefined,
|
||||
has: hasFilter || undefined,
|
||||
before: beforeFilter || undefined,
|
||||
after: afterFilter || undefined,
|
||||
from: effFrom || undefined,
|
||||
has: effHas || undefined,
|
||||
before: effBefore || undefined,
|
||||
after: effAfter || undefined,
|
||||
offset: searchOffset,
|
||||
limit: 25,
|
||||
};
|
||||
@@ -224,7 +235,7 @@ export function SearchPopover({ open, onClose, anchorRef, channelId, isDm, onJum
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search messages..."
|
||||
placeholder={t('search.placeholder')}
|
||||
className="input-embedded flex-1 text-[14px]"
|
||||
/>
|
||||
{query && (
|
||||
@@ -257,30 +268,30 @@ export function SearchPopover({ open, onClose, anchorRef, channelId, isDm, onJum
|
||||
{showFilters && (
|
||||
<div className="mt-2 grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-[11px] text-txt-tertiary font-medium mb-1 block">From</label>
|
||||
<label className="text-[11px] text-txt-tertiary font-medium mb-1 block">{t('search.from')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={fromFilter}
|
||||
onChange={(e) => setFromFilter(e.target.value)}
|
||||
placeholder="username"
|
||||
placeholder={t('search.fromPlaceholder')}
|
||||
className="input-search w-full px-2 py-1 text-[13px]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[11px] text-txt-tertiary font-medium mb-1 block">Has</label>
|
||||
<label className="text-[11px] text-txt-tertiary font-medium mb-1 block">{t('search.has')}</label>
|
||||
<select
|
||||
value={hasFilter}
|
||||
onChange={(e) => setHasFilter(e.target.value)}
|
||||
className="input-search w-full px-2 py-1 text-[13px] appearance-none cursor-pointer"
|
||||
>
|
||||
<option value="">Any</option>
|
||||
<option value="file">File</option>
|
||||
<option value="image">Image</option>
|
||||
<option value="link">Link</option>
|
||||
<option value="">{t('search.hasAny')}</option>
|
||||
<option value="file">{t('search.hasFile')}</option>
|
||||
<option value="image">{t('search.hasImage')}</option>
|
||||
<option value="link">{t('search.hasLink')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[11px] text-txt-tertiary font-medium mb-1 block">Before</label>
|
||||
<label className="text-[11px] text-txt-tertiary font-medium mb-1 block">{t('search.before')}</label>
|
||||
<input
|
||||
type="date"
|
||||
value={beforeFilter}
|
||||
@@ -289,7 +300,7 @@ export function SearchPopover({ open, onClose, anchorRef, channelId, isDm, onJum
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[11px] text-txt-tertiary font-medium mb-1 block">After</label>
|
||||
<label className="text-[11px] text-txt-tertiary font-medium mb-1 block">{t('search.after')}</label>
|
||||
<input
|
||||
type="date"
|
||||
value={afterFilter}
|
||||
|
||||
Reference in New Issue
Block a user