Browse Source

add a filter component to all feeds

imwald
Silberengel 1 month ago
parent
commit
1cc40d182b
  1. 16
      src/components/NormalFeed/index.tsx
  2. 194
      src/components/NoteList/index.tsx
  3. 210
      src/components/PostEditor/PostContent.tsx
  4. 1
      src/components/Relay/index.tsx
  5. 30
      src/i18n/locales/de.ts
  6. 30
      src/i18n/locales/en.ts
  7. 1
      src/pages/primary/NoteListPage/FollowingFeed.tsx
  8. 1
      src/pages/primary/NoteListPage/RelaysFeed.tsx
  9. 26
      src/pages/secondary/RelayReviewsPage/index.tsx

16
src/components/NormalFeed/index.tsx

@ -30,6 +30,10 @@ const NormalFeed = forwardRef<TNoteListRef, {
useFilterAsIs?: boolean useFilterAsIs?: boolean
clientSideKindFilter?: boolean clientSideKindFilter?: boolean
allowKindlessRelayExplore?: boolean allowKindlessRelayExplore?: boolean
/**
* Client-side 🔍 feed filter. When omitted: hidden on main following, shown on relay explore and non-main feeds.
*/
showFeedClientFilter?: boolean
}>(function NormalFeed( }>(function NormalFeed(
{ {
subRequests, subRequests,
@ -43,7 +47,8 @@ const NormalFeed = forwardRef<TNoteListRef, {
feedTimelineScopeKey, feedTimelineScopeKey,
useFilterAsIs = false, useFilterAsIs = false,
clientSideKindFilter = false, clientSideKindFilter = false,
allowKindlessRelayExplore = false allowKindlessRelayExplore = false,
showFeedClientFilter: showFeedClientFilterProp
}, },
ref ref
) { ) {
@ -91,6 +96,14 @@ const NormalFeed = forwardRef<TNoteListRef, {
const showKindsKey = useMemo(() => JSON.stringify(showKinds), [showKinds]) const showKindsKey = useMemo(() => JSON.stringify(showKinds), [showKinds])
/** Relay detail + kindless home chip use {@link useFilterAsIs}; include it so the 🔍 row is not dropped if only one flag is set. */
const showFeedClientFilter = useMemo(
() =>
showFeedClientFilterProp ??
(!isMainFeed || allowKindlessRelayExplore || useFilterAsIs),
[showFeedClientFilterProp, isMainFeed, allowKindlessRelayExplore, useFilterAsIs]
)
const subHeaderFilterDepsKey = allowKindlessRelayExplore const subHeaderFilterDepsKey = allowKindlessRelayExplore
? 'kindless-relay-explore' ? 'kindless-relay-explore'
: `${showKindsKey}|${feedKindFilterBypass}` : `${showKindsKey}|${feedKindFilterBypass}`
@ -158,6 +171,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
useFilterAsIs={useFilterAsIs} useFilterAsIs={useFilterAsIs}
clientSideKindFilter={clientSideKindFilter} clientSideKindFilter={clientSideKindFilter}
allowKindlessRelayExplore={allowKindlessRelayExplore} allowKindlessRelayExplore={allowKindlessRelayExplore}
showFeedClientFilter={showFeedClientFilter}
/> />
</div> </div>
</> </>

194
src/components/NoteList/index.tsx

@ -57,6 +57,16 @@ import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { NoteFeedProfileContext, type NoteFeedProfileContextValue } from '@/providers/NoteFeedProfileContext' import { NoteFeedProfileContext, type NoteFeedProfileContextValue } from '@/providers/NoteFeedProfileContext'
import type { TProfile } from '@/types' import type { TProfile } from '@/types'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard'
const LIMIT = 100 // Increased from 200 to load more events per request const LIMIT = 100 // Increased from 200 to load more events per request
@ -79,6 +89,9 @@ if (import.meta.env.DEV && import.meta.hot) {
const SHOW_COUNT = 20 // Increased from 10 to show more events at once, reducing scroll load frequency const SHOW_COUNT = 20 // Increased from 10 to show more events at once, reducing scroll load frequency
/** Hard cap after merging parallel one-shot fetches (e.g. interests = one REQ per topic). */ /** Hard cap after merging parallel one-shot fetches (e.g. interests = one REQ per topic). */
const ONE_SHOT_MERGED_CAP =100 const ONE_SHOT_MERGED_CAP =100
/** Client-side feed time window units (Day.js `.subtract` names). */
type TFeedClientTimeUnit = 'minute' | 'day' | 'week' | 'month' | 'year'
/** 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
/** Larger chunks + parallel fetches below — sequential 36-pubkey rounds made notification avatars lag. */ /** Larger chunks + parallel fetches below — sequential 36-pubkey rounds made notification avatars lag. */
@ -183,7 +196,12 @@ const NoteList = forwardRef(
/** Initial visible rows and each “reveal more” step when scrolling cached events (default first {@link SHOW_COUNT}, then 2× per step). */ /** Initial visible rows and each “reveal more” step when scrolling cached events (default first {@link SHOW_COUNT}, then 2× per step). */
revealBatchSize, revealBatchSize,
/** When set with {@link oneShotFetch}, logs fetch + filter diagnostics to the console (e.g. faux spells). */ /** When set with {@link oneShotFetch}, logs fetch + filter diagnostics to the console (e.g. faux spells). */
oneShotDebugLabel oneShotDebugLabel,
/**
* When true (default), show the 🔍 client-side filter bar (search / from me / time window).
* Set false on feeds where it should stay hidden (e.g. main following).
*/
showFeedClientFilter = true
}: { }: {
subRequests: TFeedSubRequest[] subRequests: TFeedSubRequest[]
showKinds: number[] showKinds: number[]
@ -222,6 +240,7 @@ const NoteList = forwardRef(
oneShotGlobalTimeoutMs?: number oneShotGlobalTimeoutMs?: number
oneShotEoseTimeoutMs?: number oneShotEoseTimeoutMs?: number
oneShotFirstRelayGraceMs?: number | false oneShotFirstRelayGraceMs?: number | false
showFeedClientFilter?: boolean
}, },
ref ref
) => { ) => {
@ -240,6 +259,11 @@ const NoteList = forwardRef(
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined) const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
const [refreshCount, setRefreshCount] = useState(0) const [refreshCount, setRefreshCount] = useState(0)
const [showCount, setShowCount] = useState(SHOW_COUNT) const [showCount, setShowCount] = useState(SHOW_COUNT)
const [feedClientFilterOpen, setFeedClientFilterOpen] = useState(false)
const [feedClientSearch, setFeedClientSearch] = useState('')
const [feedClientFromMeOnly, setFeedClientFromMeOnly] = useState(false)
const [feedClientTimeAmount, setFeedClientTimeAmount] = useState('')
const [feedClientTimeUnit, setFeedClientTimeUnit] = useState<TFeedClientTimeUnit>('day')
const supportTouch = useMemo(() => isTouchDevice(), []) const supportTouch = useMemo(() => isTouchDevice(), [])
const bottomRef = useRef<HTMLDivElement | null>(null) const bottomRef = useRef<HTMLDivElement | null>(null)
const topRef = useRef<HTMLDivElement | null>(null) const topRef = useRef<HTMLDivElement | null>(null)
@ -594,6 +618,63 @@ const NoteList = forwardRef(
allowKindlessRelayExplore allowKindlessRelayExplore
]) ])
const feedClientMinCreatedAt = useMemo(() => {
const raw = feedClientTimeAmount.trim()
const n = parseInt(raw, 10)
if (!Number.isFinite(n) || n < 1) return null
return dayjs().subtract(n, feedClientTimeUnit).unix()
}, [feedClientTimeAmount, feedClientTimeUnit])
const applyClientFeedFilter = useCallback(
(evts: Event[]) => {
let rows = evts
if (feedClientFromMeOnly && pubkey) {
const p = pubkey.toLowerCase()
rows = rows.filter((e) => e.pubkey.toLowerCase() === p)
}
if (feedClientMinCreatedAt !== null) {
rows = rows.filter((e) => e.created_at >= feedClientMinCreatedAt)
}
const q = feedClientSearch.trim().toLowerCase()
if (q) {
rows = rows.filter((e) => {
if (e.content?.toLowerCase().includes(q)) return true
for (const tag of e.tags) {
for (const cell of tag) {
if (typeof cell === 'string' && cell.toLowerCase().includes(q)) return true
}
}
return false
})
}
return rows
},
[feedClientFromMeOnly, pubkey, feedClientMinCreatedAt, feedClientSearch]
)
const clientFilteredEvents = useMemo(
() =>
showFeedClientFilter ? applyClientFeedFilter(filteredEvents) : filteredEvents,
[showFeedClientFilter, applyClientFeedFilter, filteredEvents]
)
const clientFilteredNewEvents = useMemo(
() =>
showFeedClientFilter ? applyClientFeedFilter(filteredNewEvents) : filteredNewEvents,
[showFeedClientFilter, applyClientFeedFilter, filteredNewEvents]
)
const feedClientFilterActive = useMemo(
() =>
!!(
showFeedClientFilter &&
(feedClientSearch.trim() ||
feedClientFromMeOnly ||
feedClientMinCreatedAt !== null)
),
[showFeedClientFilter, feedClientSearch, feedClientFromMeOnly, feedClientMinCreatedAt]
)
useLayoutEffect(() => { useLayoutEffect(() => {
if (!onSpellFeedFirstPaint || spellFeedInstrumentToken === undefined) return if (!onSpellFeedFirstPaint || spellFeedInstrumentToken === undefined) return
if (filteredEvents.length === 0) return if (filteredEvents.length === 0) return
@ -1658,7 +1739,7 @@ const NoteList = forwardRef(
// Debounce embedded event prefetching by 400ms to reduce frequency during rapid scrolling // Debounce embedded event prefetching by 400ms to reduce frequency during rapid scrolling
prefetchEmbeddedEventsTimeoutRef.current = setTimeout(() => { prefetchEmbeddedEventsTimeoutRef.current = setTimeout(() => {
const visibleTargets = mergePrefetchTargetsFromEvents(filteredEvents.slice(0, 40)) const visibleTargets = mergePrefetchTargetsFromEvents(clientFilteredEvents.slice(0, 40))
const upcomingTargets = mergePrefetchTargetsFromEvents(events.slice(0, 80)) const upcomingTargets = mergePrefetchTargetsFromEvents(events.slice(0, 80))
const hexIds = Array.from( const hexIds = Array.from(
new Set([...visibleTargets.hexIds, ...upcomingTargets.hexIds]) new Set([...visibleTargets.hexIds, ...upcomingTargets.hexIds])
@ -1702,7 +1783,7 @@ const NoteList = forwardRef(
prefetchEmbeddedEventsTimeoutRef.current = null prefetchEmbeddedEventsTimeoutRef.current = null
} }
} }
}, [filteredEvents, events, mergePrefetchTargetsFromEvents]) }, [clientFilteredEvents, events, mergePrefetchTargetsFromEvents])
// Also prefetch when loading more events (scrolling down) // Also prefetch when loading more events (scrolling down)
// Throttled to reduce frequency during rapid scrolling // Throttled to reduce frequency during rapid scrolling
@ -1765,9 +1846,100 @@ const NoteList = forwardRef(
}, 0) }, 0)
} }
const feedClientFilterBar = (
<div className="sticky top-0 z-20 border-b border-border/80 bg-background/95 px-1 py-1 backdrop-blur supports-[backdrop-filter]:bg-background/80">
<div className="flex items-center gap-1">
<Button
type="button"
variant="ghost"
size="icon"
className="shrink-0 text-lg leading-none"
aria-expanded={feedClientFilterOpen}
aria-controls="feed-client-filter-panel"
aria-label={t('Feed filter')}
title={t('Feed filter')}
onClick={() => setFeedClientFilterOpen((o) => !o)}
>
<span aria-hidden>🔍</span>
</Button>
</div>
{feedClientFilterOpen ? (
<div id="feed-client-filter-panel" className="space-y-3 border-t border-border/60 py-3">
<div className="space-y-2">
<Label htmlFor="feed-client-search" className="text-sm font-medium">
{t('Search loaded posts')}
</Label>
<Input
id="feed-client-search"
value={feedClientSearch}
onChange={(e) => setFeedClientSearch(e.target.value)}
placeholder={t('Filter loaded posts placeholder')}
autoComplete="off"
className="w-full"
/>
</div>
{pubkey ? (
<label className="flex cursor-pointer items-center gap-2 text-sm">
<Checkbox
checked={feedClientFromMeOnly}
onCheckedChange={(v) => setFeedClientFromMeOnly(v === true)}
/>
{t('From me only')}
</label>
) : null}
<div className="flex flex-wrap items-end gap-2">
<div className="grid min-w-0 flex-1 gap-1.5 sm:max-w-[10rem]">
<Label htmlFor="feed-client-time-n" className="text-sm font-medium">
{t('Within the last')}
</Label>
<Input
id="feed-client-time-n"
inputMode="numeric"
min={1}
value={feedClientTimeAmount}
onChange={(e) => {
const v = e.target.value
if (v === '' || /^\d+$/.test(v)) setFeedClientTimeAmount(v)
}}
placeholder="1"
className="w-full"
/>
</div>
<div className="grid min-w-0 gap-1.5 sm:w-40">
<Label htmlFor="feed-client-time-unit" className="text-sm font-medium">
{t('Time unit')}
</Label>
<Select
value={feedClientTimeUnit}
onValueChange={(v) => setFeedClientTimeUnit(v as TFeedClientTimeUnit)}
>
<SelectTrigger id="feed-client-time-unit" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="minute">{t('Minutes')}</SelectItem>
<SelectItem value="day">{t('Days')}</SelectItem>
<SelectItem value="week">{t('Weeks')}</SelectItem>
<SelectItem value="month">{t('Months')}</SelectItem>
<SelectItem value="year">{t('Years')}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<p className="text-xs text-muted-foreground">{t('Feed filter client-side hint')}</p>
</div>
) : null}
</div>
)
const list = ( const list = (
<div className="min-h-screen"> <div className="min-h-screen">
{filteredEvents.map((event) => ( {feedClientFilterActive && filteredEvents.length > 0 && clientFilteredEvents.length === 0 ? (
<div className="px-2 py-8 text-center text-sm text-muted-foreground">
{t('No loaded posts match your filters.')}
</div>
) : null}
{clientFilteredEvents.map((event) => (
<NoteCard <NoteCard
key={event.id} key={event.id}
className="w-full" className="w-full"
@ -1845,15 +2017,21 @@ const NoteList = forwardRef(
}} }}
pullingContent="" pullingContent=""
> >
{list} <div>
{showFeedClientFilter ? feedClientFilterBar : null}
{list}
</div>
</PullToRefresh> </PullToRefresh>
) : ( ) : (
list <div>
{showFeedClientFilter ? feedClientFilterBar : null}
{list}
</div>
)} )}
</NoteFeedProfileContext.Provider> </NoteFeedProfileContext.Provider>
<div className="h-40" /> <div className="h-40" />
{filteredNewEvents.length > 0 && ( {clientFilteredNewEvents.length > 0 && (
<NewNotesButton newEvents={filteredNewEvents} onClick={showNewEvents} /> <NewNotesButton newEvents={clientFilteredNewEvents} onClick={showNewEvents} />
)} )}
</div> </div>
) )

210
src/components/PostEditor/PostContent.tsx

@ -1688,6 +1688,8 @@ export default function PostContent({
return t('New Hardcopy Citation') return t('New Hardcopy Citation')
} else if (determinedKind === ExtendedKind.CITATION_PROMPT) { } else if (determinedKind === ExtendedKind.CITATION_PROMPT) {
return t('New Prompt Citation') return t('New Prompt Citation')
} else if (determinedKind === ExtendedKind.GIT_RELEASE) {
return t('New Repository Release')
} else { } else {
return t('New Note') return t('New Note')
} }
@ -1782,14 +1784,24 @@ export default function PostContent({
)} )}
{/* Citation metadata fields */} {/* Citation metadata fields */}
{(isCitationInternal || isCitationExternal || isCitationHardcopy || isCitationPrompt) && ( {(isCitationInternal ||
isCitationExternal ||
isCitationHardcopy ||
isCitationPrompt ||
isGitRelease) && (
<div className="p-4 border rounded-lg bg-muted/30"> <div className="p-4 border rounded-lg bg-muted/30">
<div className="text-sm font-medium mb-3"> <div className="text-sm font-medium mb-3">
{isCitationInternal && t('Internal Citation Settings')} {isGitRelease
{isCitationExternal && t('External Citation Settings')} ? t('Repository release')
{isCitationHardcopy && t('Hardcopy Citation Settings')} : isCitationInternal
{isCitationPrompt && t('Prompt Citation Settings')} ? t('Internal Citation Settings')
: isCitationExternal
? t('External Citation Settings')
: isCitationHardcopy
? t('Hardcopy Citation Settings')
: t('Prompt Citation Settings')}
</div> </div>
{(isCitationInternal || isCitationExternal || isCitationHardcopy || isCitationPrompt) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Prompt Citation specific fields - shown first if prompt */} {/* Prompt Citation specific fields - shown first if prompt */}
@ -2125,6 +2137,115 @@ export default function PostContent({
</> </>
)} )}
</div> </div>
)}
{isGitRelease && (
<div
className={cn(
'mt-4 grid grid-cols-1 gap-3 md:grid-cols-2',
(isCitationInternal ||
isCitationExternal ||
isCitationHardcopy ||
isCitationPrompt) &&
'border-t border-border pt-4'
)}
>
<p className="text-xs text-muted-foreground md:col-span-2">
{t('Release notes use the editor below (optional).')}
</p>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="release-repo-owner" className="text-sm font-medium">
{t('Repository owner (npub or hex)')} <span className="text-destructive">*</span>
</Label>
<Input
id="release-repo-owner"
value={releaseRepoOwnerInput}
onChange={(e) => setReleaseRepoOwnerInput(e.target.value)}
placeholder="npub1…"
className={
releaseRepoOwnerInput.trim() && !parseRepoOwnerPubkeyInput(releaseRepoOwnerInput)
? 'border-destructive'
: ''
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="release-repo-id" className="text-sm font-medium">
{t('Repository id (d-tag)')} <span className="text-destructive">*</span>
</Label>
<Input
id="release-repo-id"
value={releaseRepoId}
onChange={(e) => setReleaseRepoId(e.target.value)}
placeholder={t('e.g. my-repo')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="release-tag-name" className="text-sm font-medium">
{t('Git tag name')} <span className="text-destructive">*</span>
</Label>
<Input
id="release-tag-name"
value={releaseTagName}
onChange={(e) => setReleaseTagName(e.target.value)}
placeholder="v1.0.0"
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="release-tag-hash" className="text-sm font-medium">
{t('Tag target (40-char commit hash)')} <span className="text-destructive">*</span>
</Label>
<Input
id="release-tag-hash"
value={releaseTagHash}
onChange={(e) => setReleaseTagHash(e.target.value.trim())}
placeholder={t('40-character hex SHA-1')}
className={
releaseTagHash.trim() && !/^[0-9a-f]{40}$/i.test(releaseTagHash.trim())
? 'border-destructive'
: ''
}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="release-title" className="text-sm font-medium">
{t('Release title')}
</Label>
<Input
id="release-title"
value={releaseTitle}
onChange={(e) => setReleaseTitle(e.target.value)}
placeholder={t('Optional display title')}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor="release-download-url" className="text-sm font-medium">
{t('Download URL')}
</Label>
<Input
id="release-download-url"
value={releaseDownloadUrl}
onChange={(e) => setReleaseDownloadUrl(e.target.value)}
placeholder={t('https://…')}
/>
</div>
<div className="flex flex-wrap items-center gap-6 md:col-span-2">
<label className="flex cursor-pointer items-center gap-2 text-sm">
<Checkbox
checked={releaseDraft}
onCheckedChange={(v) => setReleaseDraft(v === true)}
/>
{t('Draft release')}
</label>
<label className="flex cursor-pointer items-center gap-2 text-sm">
<Checkbox
checked={releasePrerelease}
onCheckedChange={(v) => setReleasePrerelease(v === true)}
/>
{t('Pre-release')}
</label>
</div>
</div>
)}
</div> </div>
)} )}
@ -2242,39 +2363,52 @@ export default function PostContent({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
{/* Citation dropdown - only show if has private relays */} {/* Citations (private relays) + repository release */}
{hasPrivateRelaysAvailable && ( <DropdownMenu>
<DropdownMenu> <DropdownMenuTrigger asChild>
<DropdownMenuTrigger asChild> <Button
<Button variant="ghost"
variant="ghost" size="icon"
size="icon" title={t('Create Citation')}
title={t('Create Citation')} className={
className={ isCitationInternal ||
isCitationInternal || isCitationExternal || isCitationHardcopy || isCitationPrompt isCitationExternal ||
? 'bg-accent' isCitationHardcopy ||
: '' isCitationPrompt ||
} isGitRelease
> ? 'bg-accent'
<Quote className="h-4 w-4" /> : ''
</Button> }
</DropdownMenuTrigger> >
<DropdownMenuContent> <Quote className="h-4 w-4" />
<DropdownMenuItem onClick={() => handleCitationToggle('internal')}> </Button>
{t('Internal Citation')} </DropdownMenuTrigger>
</DropdownMenuItem> <DropdownMenuContent>
<DropdownMenuItem onClick={() => handleCitationToggle('external')}> {hasPrivateRelaysAvailable ? (
{t('External Citation')} <>
</DropdownMenuItem> <DropdownMenuItem onClick={() => handleCitationToggle('internal')}>
<DropdownMenuItem onClick={() => handleCitationToggle('hardcopy')}> {t('Internal Citation')}
{t('Hardcopy Citation')} </DropdownMenuItem>
</DropdownMenuItem> <DropdownMenuItem onClick={() => handleCitationToggle('external')}>
<DropdownMenuItem onClick={() => handleCitationToggle('prompt')}> {t('External Citation')}
{t('Prompt Citation')} </DropdownMenuItem>
</DropdownMenuItem> <DropdownMenuItem onClick={() => handleCitationToggle('hardcopy')}>
</DropdownMenuContent> {t('Hardcopy Citation')}
</DropdownMenu> </DropdownMenuItem>
)} <DropdownMenuItem onClick={() => handleCitationToggle('prompt')}>
{t('Prompt Citation')}
</DropdownMenuItem>
</>
) : (
<div className="px-2 py-1.5 text-xs text-muted-foreground max-w-[14rem]">
{t('Citations require private relays (NIP-65).')}
</div>
)}
<DropdownMenuItem onClick={handleGitReleaseFromMenu}>
{t('Repository release')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</> </>
)} )}
<GifPicker <GifPicker

1
src/components/Relay/index.tsx

@ -84,6 +84,7 @@ const Relay = forwardRef<TNoteListRef, { url?: string; className?: string }>(fun
]} ]}
useFilterAsIs useFilterAsIs
allowKindlessRelayExplore allowKindlessRelayExplore
showFeedClientFilter
/> />
</div> </div>
) )

30
src/i18n/locales/de.ts

@ -735,6 +735,22 @@ export default {
'Open in Git Republic': 'In Git Republic öffnen', 'Open in Git Republic': 'In Git Republic öffnen',
'Pre-release': 'Vorabversion', 'Pre-release': 'Vorabversion',
Draft: 'Entwurf', Draft: 'Entwurf',
'Repository release': 'Repository-Release',
'New Repository Release': 'Neues Repository-Release',
'Release notes use the editor below (optional).':
'Release-Notizen stehen im Editor unten (optional).',
'Repository owner (npub or hex)': 'Repository-Inhaber (npub oder Hex)',
'Repository id (d-tag)': 'Repository-ID (d-Tag)',
'Git tag name': 'Git-Tag-Name',
'Tag target (40-char commit hash)': 'Tag-Ziel (40 Zeichen Commit-Hash)',
'40-character hex SHA-1': '40 Zeichen Hex SHA-1',
'Release title': 'Release-Titel',
'Download URL': 'Download-URL',
'Draft release': 'Release-Entwurf',
'Fill repository release fields': 'Bitte alle Pflichtfelder für das Release ausfüllen.',
'Invalid repository owner pubkey': 'Ungültiger Inhaber (npub oder 64 Zeichen Hex).',
'Citations require private relays (NIP-65).':
'Zitate benötigen private Outbox-Relays (NIP-65).',
'Select All': 'Alle auswählen', 'Select All': 'Alle auswählen',
'Clear All': 'Alle löschen', 'Clear All': 'Alle löschen',
'Set as default filter': 'Als Standardfilter festlegen', 'Set as default filter': 'Als Standardfilter festlegen',
@ -754,6 +770,20 @@ export default {
'This note mentions a user you muted': 'This note mentions a user you muted':
'Diese Notiz erwähnt einen Benutzer, den Sie stumm geschaltet haben', 'Diese Notiz erwähnt einen Benutzer, den Sie stumm geschaltet haben',
Filter: 'Filter', Filter: 'Filter',
'Feed filter': 'Feed-Filter',
'Search loaded posts': 'Geladene Beiträge durchsuchen',
'Filter loaded posts placeholder': 'Nach Text in Inhalt oder Tags filtern…',
'From me only': 'Nur von mir',
'Within the last': 'Innerhalb der letzten',
'Time unit': 'Zeiteinheit',
Minutes: 'Minuten',
Days: 'Tage',
Weeks: 'Wochen',
Months: 'Monate',
Years: 'Jahre',
'Feed filter client-side hint':
'Filter gelten nur für bereits geladene Beiträge; Relays werden nicht erneut abgefragt.',
'No loaded posts match your filters.': 'Keine geladenen Beiträge entsprechen den Filtern.',
'mentioned you in a note': 'hat Sie in einer Notiz erwähnt', 'mentioned you in a note': 'hat Sie in einer Notiz erwähnt',
'quoted your note': 'hat Ihre Notiz zitiert', 'quoted your note': 'hat Ihre Notiz zitiert',
'quoted this note': 'Hat diese Notiz zitiert', 'quoted this note': 'Hat diese Notiz zitiert',

30
src/i18n/locales/en.ts

@ -780,6 +780,22 @@ export default {
'Open in Git Republic': 'Open in Git Republic', 'Open in Git Republic': 'Open in Git Republic',
'Pre-release': 'Pre-release', 'Pre-release': 'Pre-release',
Draft: 'Draft', Draft: 'Draft',
'Repository release': 'Repository release',
'New Repository Release': 'New Repository Release',
'Release notes use the editor below (optional).':
'Release notes use the editor below (optional).',
'Repository owner (npub or hex)': 'Repository owner (npub or hex)',
'Repository id (d-tag)': 'Repository id (d-tag)',
'Git tag name': 'Git tag name',
'Tag target (40-char commit hash)': 'Tag target (40-character commit hash)',
'40-character hex SHA-1': '40-character hex SHA-1',
'Release title': 'Release title',
'Download URL': 'Download URL',
'Draft release': 'Draft release',
'Fill repository release fields': 'Fill in all required repository release fields.',
'Invalid repository owner pubkey': 'Invalid repository owner (use npub or 64-char hex).',
'Citations require private relays (NIP-65).':
'Citations require private outbox relays (NIP-65).',
'Select All': 'Select All', 'Select All': 'Select All',
'Clear All': 'Clear All', 'Clear All': 'Clear All',
'Set as default filter': 'Set as default filter', 'Set as default filter': 'Set as default filter',
@ -798,6 +814,20 @@ export default {
'Hide content mentioning muted users': 'Hide content mentioning muted users', 'Hide content mentioning muted users': 'Hide content mentioning muted users',
'This note mentions a user you muted': 'This note mentions a user you muted', 'This note mentions a user you muted': 'This note mentions a user you muted',
Filter: 'Filter', Filter: 'Filter',
'Feed filter': 'Feed filter',
'Search loaded posts': 'Search loaded posts',
'Filter loaded posts placeholder': 'Filter by text in content or tags…',
'From me only': 'From me only',
'Within the last': 'Within the last',
'Time unit': 'Time unit',
Minutes: 'Minutes',
Days: 'Days',
Weeks: 'Weeks',
Months: 'Months',
Years: 'Years',
'Feed filter client-side hint':
'Filters only apply to posts already loaded; relays are not queried again.',
'No loaded posts match your filters.': 'No loaded posts match your filters.',
'mentioned you in a note': 'mentioned you in a note', 'mentioned you in a note': 'mentioned you in a note',
'quoted your note': 'quoted your note', 'quoted your note': 'quoted your note',
'quoted this note': 'Quoted this note', 'quoted this note': 'Quoted this note',

1
src/pages/primary/NoteListPage/FollowingFeed.tsx

@ -89,6 +89,7 @@ const FollowingFeed = forwardRef<
isMainFeed isMainFeed
setSubHeader={setSubHeader} setSubHeader={setSubHeader}
onSubHeaderRefresh={onSubHeaderRefresh} onSubHeaderRefresh={onSubHeaderRefresh}
showFeedClientFilter={false}
/> />
) )
}) })

1
src/pages/primary/NoteListPage/RelaysFeed.tsx

@ -132,6 +132,7 @@ const RelaysFeed = forwardRef<
feedTimelineScopeKey={feedTimelineScopeKey} feedTimelineScopeKey={feedTimelineScopeKey}
useFilterAsIs={singleRelayKindlessExplore} useFilterAsIs={singleRelayKindlessExplore}
allowKindlessRelayExplore={singleRelayKindlessExplore} allowKindlessRelayExplore={singleRelayKindlessExplore}
showFeedClientFilter
/> />
) )
}) })

26
src/pages/secondary/RelayReviewsPage/index.tsx

@ -5,6 +5,7 @@ import { FAST_READ_RELAY_URLS, ExtendedKind } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context' import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { normalizeUrl, simplifyUrl } from '@/lib/url' import { normalizeUrl, simplifyUrl } from '@/lib/url'
import type { TFeedSubRequest } from '@/types'
import { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react' import { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import NotFoundPage from '../NotFoundPage' import NotFoundPage from '../NotFoundPage'
@ -25,9 +26,24 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url
}, [hideTitlebar, registerPrimaryPanelRefresh, bumpFeed]) }, [hideTitlebar, registerPrimaryPanelRefresh, bumpFeed])
const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url]) const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url])
/** Stable identity for session feed snapshot (decoupled from FAST_READ_RELAY_URLS JSON churn). */
const relayReviewsFeedSubscriptionKey = useMemo(
() =>
normalizedUrl ? `relay-reviews:v1|${normalizedUrl}|k=${ExtendedKind.RELAY_REVIEW}` : '',
[normalizedUrl]
)
const reviewsSubRequests = useMemo<TFeedSubRequest[]>(() => {
if (!normalizedUrl) return []
return [
{
urls: [normalizedUrl, ...FAST_READ_RELAY_URLS],
filter: { '#d': [normalizedUrl] }
}
]
}, [normalizedUrl])
const title = useMemo( const title = useMemo(
() => (url ? t('Reviews for {{relay}}', { relay: simplifyUrl(url) }) : undefined), () => (url ? t('Reviews for {{relay}}', { relay: simplifyUrl(url) }) : undefined),
[url] [url, t]
) )
if (!normalizedUrl) { if (!normalizedUrl) {
@ -45,12 +61,8 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url
<NoteList <NoteList
ref={feedRef} ref={feedRef}
showKinds={[ExtendedKind.RELAY_REVIEW]} showKinds={[ExtendedKind.RELAY_REVIEW]}
subRequests={[ subRequests={reviewsSubRequests}
{ feedSubscriptionKey={relayReviewsFeedSubscriptionKey}
urls: [normalizedUrl, ...FAST_READ_RELAY_URLS],
filter: { '#d': [normalizedUrl] }
}
]}
/> />
</SecondaryPageLayout> </SecondaryPageLayout>
) )

Loading…
Cancel
Save