Browse Source

add kind to feed filter

pretty up the filter
imwald
Silberengel 4 weeks ago
parent
commit
de521e9802
  1. 94
      src/components/NoteList/index.tsx

94
src/components/NoteList/index.tsx

@ -109,6 +109,8 @@ type TFeedClientTimeUnit = 'minute' | 'day' | 'week' | 'month' | 'year'
/** Client-side “who wrote this” filter on already-loaded posts. */ /** Client-side “who wrote this” filter on already-loaded posts. */
type TFeedClientAuthorMode = 'everyone' | 'me' | 'npub' type TFeedClientAuthorMode = 'everyone' | 'me' | 'npub'
const FEED_FILTER_KIND_MIN = 0
const FEED_FILTER_KIND_MAX = 40_000
/** Short debounce: batch rapid timeline updates without delaying first paint on feeds like notifications. */ /** Short debounce: batch rapid timeline updates without delaying first paint on feeds like notifications. */
const FEED_PROFILE_BATCH_DEBOUNCE_MS = 50 const FEED_PROFILE_BATCH_DEBOUNCE_MS = 50
@ -420,6 +422,7 @@ const NoteList = forwardRef(
const [feedClientSearch, setFeedClientSearch] = useState('') const [feedClientSearch, setFeedClientSearch] = useState('')
const [feedClientAuthorMode, setFeedClientAuthorMode] = useState<TFeedClientAuthorMode>('everyone') const [feedClientAuthorMode, setFeedClientAuthorMode] = useState<TFeedClientAuthorMode>('everyone')
const [feedClientAuthorNpubInput, setFeedClientAuthorNpubInput] = useState('') const [feedClientAuthorNpubInput, setFeedClientAuthorNpubInput] = useState('')
const [feedClientKindInput, setFeedClientKindInput] = useState('')
const [feedClientTimeAmount, setFeedClientTimeAmount] = useState('') const [feedClientTimeAmount, setFeedClientTimeAmount] = useState('')
const [feedClientTimeUnit, setFeedClientTimeUnit] = useState<TFeedClientTimeUnit>('day') const [feedClientTimeUnit, setFeedClientTimeUnit] = useState<TFeedClientTimeUnit>('day')
const supportTouch = useMemo(() => isTouchDevice(), []) const supportTouch = useMemo(() => isTouchDevice(), [])
@ -518,6 +521,7 @@ const NoteList = forwardRef(
setFeedClientSearch('') setFeedClientSearch('')
setFeedClientAuthorMode('everyone') setFeedClientAuthorMode('everyone')
setFeedClientAuthorNpubInput('') setFeedClientAuthorNpubInput('')
setFeedClientKindInput('')
setFeedClientTimeAmount('') setFeedClientTimeAmount('')
setFeedClientTimeUnit('day') setFeedClientTimeUnit('day')
setFeedFullSearchEvents(null) setFeedFullSearchEvents(null)
@ -869,6 +873,19 @@ const NoteList = forwardRef(
return null return null
}, [feedClientAuthorMode, feedClientAuthorNpubInput, pubkey]) }, [feedClientAuthorMode, feedClientAuthorNpubInput, pubkey])
/**
* `null` => no kind constraint, `number` => valid kind, `undefined` => invalid non-empty input.
*/
const feedClientKindFilter = useMemo<number | null | undefined>(() => {
const raw = feedClientKindInput.trim()
if (raw.length === 0) return null
if (!/^\d+$/.test(raw)) return undefined
const parsed = Number(raw)
if (!Number.isInteger(parsed)) return undefined
if (parsed < FEED_FILTER_KIND_MIN || parsed > FEED_FILTER_KIND_MAX) return undefined
return parsed
}, [feedClientKindInput])
const applyClientFeedFilter = useCallback( const applyClientFeedFilter = useCallback(
(evts: Event[]) => { (evts: Event[]) => {
let rows = evts let rows = evts
@ -890,6 +907,11 @@ const NoteList = forwardRef(
if (feedClientMinCreatedAt !== null) { if (feedClientMinCreatedAt !== null) {
rows = rows.filter((e) => e.created_at >= feedClientMinCreatedAt) rows = rows.filter((e) => e.created_at >= feedClientMinCreatedAt)
} }
if (typeof feedClientKindFilter === 'number') {
rows = rows.filter((e) => e.kind === feedClientKindFilter)
} else if (feedClientKindFilter === undefined) {
rows = []
}
const q = feedClientSearch.trim().toLowerCase() const q = feedClientSearch.trim().toLowerCase()
if (q) { if (q) {
rows = rows.filter((e) => { rows = rows.filter((e) => {
@ -909,6 +931,7 @@ const NoteList = forwardRef(
feedClientAuthorNpubInput, feedClientAuthorNpubInput,
pubkey, pubkey,
feedClientMinCreatedAt, feedClientMinCreatedAt,
feedClientKindFilter,
feedClientSearch feedClientSearch
] ]
) )
@ -932,6 +955,7 @@ const NoteList = forwardRef(
(feedClientSearch.trim() || (feedClientSearch.trim() ||
(feedClientAuthorMode === 'me' && !!pubkey) || (feedClientAuthorMode === 'me' && !!pubkey) ||
(feedClientAuthorMode === 'npub' && feedClientAuthorNpubInput.trim() !== '') || (feedClientAuthorMode === 'npub' && feedClientAuthorNpubInput.trim() !== '') ||
feedClientKindInput.trim() !== '' ||
feedClientMinCreatedAt !== null) feedClientMinCreatedAt !== null)
), ),
[ [
@ -939,6 +963,7 @@ const NoteList = forwardRef(
feedClientSearch, feedClientSearch,
feedClientAuthorMode, feedClientAuthorMode,
feedClientAuthorNpubInput, feedClientAuthorNpubInput,
feedClientKindInput,
pubkey, pubkey,
feedClientMinCreatedAt feedClientMinCreatedAt
] ]
@ -1076,12 +1101,21 @@ const NoteList = forwardRef(
} }
const hasSearch = feedClientSearch.trim().length > 0 const hasSearch = feedClientSearch.trim().length > 0
const hasTime = feedClientMinCreatedAt !== null const hasTime = feedClientMinCreatedAt !== null
const hasKind = typeof feedClientKindFilter === 'number'
let hasAuthor = false let hasAuthor = false
if (feedClientAuthorMode === 'me' && pubkey) hasAuthor = true if (feedClientAuthorMode === 'me' && pubkey) hasAuthor = true
if (feedClientAuthorMode === 'npub' && inviteInputToHexPubkey(feedClientAuthorNpubInput)) { if (feedClientAuthorMode === 'npub' && inviteInputToHexPubkey(feedClientAuthorNpubInput)) {
hasAuthor = true hasAuthor = true
} }
if (!hasSearch && !hasTime && !hasAuthor) { if (feedClientKindFilter === undefined) {
toast.error(
t('Feed filter kind invalid', {
defaultValue: `Kind must be an integer between ${FEED_FILTER_KIND_MIN} and ${FEED_FILTER_KIND_MAX}.`
})
)
return
}
if (!hasSearch && !hasTime && !hasAuthor && !hasKind) {
toast.error(t('Feed full search need constraint')) toast.error(t('Feed full search need constraint'))
return return
} }
@ -1115,6 +1149,9 @@ const NoteList = forwardRef(
typeof finalFilter.since === 'number' ? finalFilter.since : 0 typeof finalFilter.since === 'number' ? finalFilter.since : 0
) )
} }
if (hasKind) {
finalFilter.kinds = [feedClientKindFilter]
}
const hasRelayScope = const hasRelayScope =
timelineFilterHasNonKindScope(finalFilter) || timelineFilterHasNonKindScope(finalFilter) ||
@ -1157,6 +1194,7 @@ const NoteList = forwardRef(
showFeedClientFilter, showFeedClientFilter,
feedClientSearch, feedClientSearch,
feedClientMinCreatedAt, feedClientMinCreatedAt,
feedClientKindFilter,
feedClientAuthorMode, feedClientAuthorMode,
feedClientAuthorNpubInput, feedClientAuthorNpubInput,
pubkey, pubkey,
@ -2296,8 +2334,9 @@ const NoteList = forwardRef(
const feedClientFilterPanelSurfaceClass = const feedClientFilterPanelSurfaceClass =
useFeedFilterTabRowPortal && feedClientFilterTabRowHost useFeedFilterTabRowPortal && feedClientFilterTabRowHost
? 'mt-1 space-y-3 w-full min-w-[min(100vw-2rem,22rem)] max-w-md rounded-md border border-border bg-background px-3 py-3 shadow-md' ? 'mt-1 w-[min(100vw-1rem,28rem)] max-w-[calc(100vw-1rem)] space-y-3 rounded-lg border border-border bg-background p-3 shadow-lg'
: 'space-y-3 border-t border-border/60 py-3' : 'space-y-3 border-t border-border/60 px-2 py-3'
const feedClientFilterSectionClass = 'space-y-2 rounded-md border border-border/60 bg-muted/25 p-2.5'
const feedClientFilterChrome = ( const feedClientFilterChrome = (
<> <>
@ -2318,7 +2357,7 @@ const NoteList = forwardRef(
</div> </div>
{feedClientFilterOpen ? ( {feedClientFilterOpen ? (
<div id="feed-client-filter-panel" className={feedClientFilterPanelSurfaceClass}> <div id="feed-client-filter-panel" className={feedClientFilterPanelSurfaceClass}>
<div className="space-y-2"> <div className={feedClientFilterSectionClass}>
<Label htmlFor="feed-client-search" className="text-sm font-medium"> <Label htmlFor="feed-client-search" className="text-sm font-medium">
{t('Search loaded posts')} {t('Search loaded posts')}
</Label> </Label>
@ -2331,7 +2370,31 @@ const NoteList = forwardRef(
className="w-full" className="w-full"
/> />
</div> </div>
<div className="space-y-2"> <div className={feedClientFilterSectionClass}>
<Label htmlFor="feed-client-kind" className="text-sm font-medium">
{t('Feed filter kind', { defaultValue: 'Event kind' })}
</Label>
<Input
id="feed-client-kind"
inputMode="numeric"
min={FEED_FILTER_KIND_MIN}
max={FEED_FILTER_KIND_MAX}
value={feedClientKindInput}
onChange={(e) => {
const v = e.target.value.trim()
if (v === '' || /^\d+$/.test(v)) setFeedClientKindInput(v)
}}
placeholder={t('Feed filter kind placeholder', { defaultValue: 'e.g. 30023' })}
className="w-full sm:max-w-[11rem]"
aria-invalid={feedClientKindFilter === undefined ? true : undefined}
/>
<p className="text-xs text-muted-foreground">
{t('Feed filter kind hint', {
defaultValue: `Integer ${FEED_FILTER_KIND_MIN}-${FEED_FILTER_KIND_MAX}.`
})}
</p>
</div>
<div className={feedClientFilterSectionClass}>
<Label className="text-sm font-medium">{t('Feed filter author')}</Label> <Label className="text-sm font-medium">{t('Feed filter author')}</Label>
<RadioGroup <RadioGroup
value={feedClientAuthorMode} value={feedClientAuthorMode}
@ -2355,7 +2418,7 @@ const NoteList = forwardRef(
<span>{t('Feed filter author npub')}</span> <span>{t('Feed filter author npub')}</span>
</label> </label>
{feedClientAuthorMode === 'npub' ? ( {feedClientAuthorMode === 'npub' ? (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1.5 pl-6"> <div className="grid gap-1.5 pl-6">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{t('Feed filter author npub from prefix')} {t('Feed filter author npub from prefix')}
</span> </span>
@ -2365,7 +2428,7 @@ const NoteList = forwardRef(
onChange={(e) => setFeedClientAuthorNpubInput(e.target.value)} onChange={(e) => setFeedClientAuthorNpubInput(e.target.value)}
placeholder={t('Feed filter author npub placeholder')} placeholder={t('Feed filter author npub placeholder')}
autoComplete="off" autoComplete="off"
className="min-w-[12rem] flex-1" className="w-full"
aria-invalid={ aria-invalid={
feedClientAuthorNpubInput.trim() !== '' && feedClientAuthorNpubInput.trim() !== '' &&
!inviteInputToHexPubkey(feedClientAuthorNpubInput) !inviteInputToHexPubkey(feedClientAuthorNpubInput)
@ -2378,8 +2441,9 @@ const NoteList = forwardRef(
</div> </div>
</RadioGroup> </RadioGroup>
</div> </div>
<div className="flex flex-wrap items-end gap-2"> <div className={feedClientFilterSectionClass}>
<div className="grid min-w-0 flex-1 gap-1.5 sm:max-w-[10rem]"> <div className="grid grid-cols-[minmax(0,8rem)_minmax(0,1fr)] items-end gap-2">
<div className="grid min-w-0 gap-1.5">
<Label htmlFor="feed-client-time-n" className="text-sm font-medium"> <Label htmlFor="feed-client-time-n" className="text-sm font-medium">
{t('Within the last')} {t('Within the last')}
</Label> </Label>
@ -2396,7 +2460,7 @@ const NoteList = forwardRef(
className="w-full" className="w-full"
/> />
</div> </div>
<div className="grid min-w-0 gap-1.5 sm:w-40"> <div className="grid min-w-0 gap-1.5">
<Label htmlFor="feed-client-time-unit" className="text-sm font-medium"> <Label htmlFor="feed-client-time-unit" className="text-sm font-medium">
{t('Time unit')} {t('Time unit')}
</Label> </Label>
@ -2417,19 +2481,23 @@ const NoteList = forwardRef(
</Select> </Select>
</div> </div>
</div> </div>
<p className="text-xs text-muted-foreground">{t('Feed filter client-side hint')}</p> </div>
<div className="flex flex-wrap items-center gap-2 pt-1"> <p className="px-0.5 text-xs leading-relaxed text-muted-foreground">
{t('Feed filter client-side hint')}
</p>
<div className="flex flex-wrap items-center gap-2 pt-0.5">
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
size="sm" size="sm"
className="h-8"
disabled={feedFullSearchLoading} disabled={feedFullSearchLoading}
onClick={() => void onPerformFeedFullSearch()} onClick={() => void onPerformFeedFullSearch()}
> >
{feedFullSearchLoading ? t('Feed full search running') : t('Feed full search')} {feedFullSearchLoading ? t('Feed full search running') : t('Feed full search')}
</Button> </Button>
{feedFullSearchEvents !== null ? ( {feedFullSearchEvents !== null ? (
<Button type="button" variant="outline" size="sm" onClick={onClearFeedFullSearch}> <Button type="button" variant="outline" size="sm" className="h-8" onClick={onClearFeedFullSearch}>
{t('Feed full search clear')} {t('Feed full search clear')}
</Button> </Button>
) : null} ) : null}

Loading…
Cancel
Save