Browse Source

separate out kind 1 OPs in the filter feed from kind 1 replies and kind 1111 comments

imwald
Silberengel 3 days ago
parent
commit
715792076d
  1. 118
      src/components/KindFilter/index.tsx
  2. 4
      src/components/NormalFeed/index.tsx
  3. 37
      src/components/NoteList/index.tsx
  4. 2
      src/constants.ts
  5. 2
      src/i18n/locales/de.ts
  6. 2
      src/i18n/locales/en.ts
  7. 92
      src/providers/KindFilterProvider.tsx
  8. 40
      src/services/local-storage.service.ts

118
src/components/KindFilter/index.tsx

@ -12,8 +12,10 @@ import { kinds } from 'nostr-tools' @@ -12,8 +12,10 @@ import { kinds } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
const KIND_1 = kinds.ShortTextNote
const KIND_1111 = ExtendedKind.COMMENT
const KIND_FILTER_OPTIONS = [
{ kindGroup: [kinds.ShortTextNote, ExtendedKind.COMMENT], label: 'Posts' },
{ kindGroup: [kinds.Repost], label: 'Reposts' },
{ kindGroup: [kinds.LongFormArticle], label: 'Articles' },
{ kindGroup: [ExtendedKind.PUBLICATION], label: 'Publications' },
@ -27,6 +29,18 @@ const KIND_FILTER_OPTIONS = [ @@ -27,6 +29,18 @@ const KIND_FILTER_OPTIONS = [
{ kindGroup: [ExtendedKind.ZAP_RECEIPT], label: 'Zaps' }
]
function buildShowKindsFromOptions(
baseKinds: number[],
showKind1OPs: boolean,
showRepliesAndComments: boolean
): number[] {
const rest = baseKinds.filter((k) => k !== KIND_1 && k !== KIND_1111)
const out = [...rest]
if (showKind1OPs || showRepliesAndComments) out.push(KIND_1)
if (showRepliesAndComments) out.push(KIND_1111)
return out.sort((a, b) => a - b)
}
export default function KindFilter({
showKinds,
onShowKindsChange
@ -36,40 +50,71 @@ export default function KindFilter({ @@ -36,40 +50,71 @@ export default function KindFilter({
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { showKinds: savedShowKinds } = useKindFilter()
const {
showKinds: savedShowKinds,
showKind1OPs: savedShowKind1OPs,
showRepliesAndComments: savedShowRepliesAndComments,
updateShowKinds,
updateShowKind1OPs,
updateShowRepliesAndComments
} = useKindFilter()
const [open, setOpen] = useState(false)
const { updateShowKinds } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [temporaryShowKind1OPs, setTemporaryShowKind1OPs] = useState(savedShowKind1OPs)
const [temporaryShowRepliesAndComments, setTemporaryShowRepliesAndComments] = useState(
savedShowRepliesAndComments
)
const [isPersistent, setIsPersistent] = useState(false)
const isDifferentFromSaved = useMemo(
() => !isSameKindFilter(showKinds, savedShowKinds),
[showKinds, savedShowKinds]
)
const isTemporaryDifferentFromSaved = useMemo(
() => !isSameKindFilter(temporaryShowKinds, savedShowKinds),
[temporaryShowKinds, savedShowKinds]
() =>
!isSameKindFilter(temporaryShowKinds, savedShowKinds) ||
temporaryShowKind1OPs !== savedShowKind1OPs ||
temporaryShowRepliesAndComments !== savedShowRepliesAndComments,
[
temporaryShowKinds,
savedShowKinds,
temporaryShowKind1OPs,
temporaryShowRepliesAndComments,
savedShowKind1OPs,
savedShowRepliesAndComments
]
)
useEffect(() => {
if (open) {
setTemporaryShowKinds(showKinds)
setTemporaryShowKind1OPs(savedShowKind1OPs)
setTemporaryShowRepliesAndComments(savedShowRepliesAndComments)
setIsPersistent(false)
}, [open])
}
}, [open, showKinds, savedShowKind1OPs, savedShowRepliesAndComments])
const appliedShowKinds = useMemo(
() =>
buildShowKindsFromOptions(
temporaryShowKinds,
temporaryShowKind1OPs,
temporaryShowRepliesAndComments
),
[temporaryShowKinds, temporaryShowKind1OPs, temporaryShowRepliesAndComments]
)
const canApply = appliedShowKinds.length > 0
const handleApply = () => {
if (temporaryShowKinds.length === 0) {
// must select at least one kind
return
}
if (!canApply) return
const newShowKinds = [...temporaryShowKinds].sort()
const newShowKinds = appliedShowKinds
if (!isSameKindFilter(newShowKinds, showKinds)) {
onShowKindsChange(newShowKinds)
}
if (isPersistent) {
updateShowKinds(newShowKinds)
}
updateShowKinds(newShowKinds, {
showKind1OPs: temporaryShowKind1OPs,
showRepliesAndComments: temporaryShowRepliesAndComments
})
setIsPersistent(false)
setOpen(false)
}
@ -99,6 +144,28 @@ export default function KindFilter({ @@ -99,6 +144,28 @@ export default function KindFilter({
const content = (
<div>
<div className="grid grid-cols-2 gap-2">
{/* Posts (OPs) - kind 1 top-level only */}
<div
className={cn(
'cursor-pointer grid gap-1.5 rounded-lg border px-4 py-3',
temporaryShowKind1OPs ? 'border-primary/60 bg-primary/5' : 'clickable'
)}
onClick={() => setTemporaryShowKind1OPs((prev) => !prev)}
>
<p className="leading-none font-medium">{t('Posts (OPs)')}</p>
<p className="text-muted-foreground text-xs">kind {KIND_1}</p>
</div>
{/* Replies & comments - kind 1 replies + kind 1111 */}
<div
className={cn(
'cursor-pointer grid gap-1.5 rounded-lg border px-4 py-3',
temporaryShowRepliesAndComments ? 'border-primary/60 bg-primary/5' : 'clickable'
)}
onClick={() => setTemporaryShowRepliesAndComments((prev) => !prev)}
>
<p className="leading-none font-medium">{t('Replies & comments')}</p>
<p className="text-muted-foreground text-xs">kind {KIND_1}, {KIND_1111}</p>
</div>
{KIND_FILTER_OPTIONS.map(({ kindGroup, label }) => {
const checked = kindGroup.every((k) => temporaryShowKinds.includes(k))
return (
@ -110,10 +177,8 @@ export default function KindFilter({ @@ -110,10 +177,8 @@ export default function KindFilter({
)}
onClick={() => {
if (!checked) {
// add all kinds in this group
setTemporaryShowKinds((prev) => Array.from(new Set([...prev, ...kindGroup])))
} else {
// remove all kinds in this group
setTemporaryShowKinds((prev) => prev.filter((k) => !kindGroup.includes(k)))
}
}}
@ -129,8 +194,11 @@ export default function KindFilter({ @@ -129,8 +194,11 @@ export default function KindFilter({
<Button
variant="secondary"
onClick={() => {
// Select all supported kinds except reposts (matching default behavior)
setTemporaryShowKinds(SUPPORTED_KINDS.filter(kind => kind !== kinds.Repost))
setTemporaryShowKinds(
SUPPORTED_KINDS.filter((k) => k !== kinds.Repost && k !== KIND_1 && k !== KIND_1111)
)
setTemporaryShowKind1OPs(true)
setTemporaryShowRepliesAndComments(true)
}}
>
{t('Select All')}
@ -139,13 +207,19 @@ export default function KindFilter({ @@ -139,13 +207,19 @@ export default function KindFilter({
variant="secondary"
onClick={() => {
setTemporaryShowKinds([])
setTemporaryShowKind1OPs(false)
setTemporaryShowRepliesAndComments(false)
}}
>
{t('Clear All')}
</Button>
<Button
variant="secondary"
onClick={() => setTemporaryShowKinds(savedShowKinds)}
onClick={() => {
setTemporaryShowKinds(savedShowKinds)
setTemporaryShowKind1OPs(savedShowKind1OPs)
setTemporaryShowRepliesAndComments(savedShowRepliesAndComments)
}}
disabled={!isTemporaryDifferentFromSaved}
>
{t('Reset')}
@ -164,7 +238,7 @@ export default function KindFilter({ @@ -164,7 +238,7 @@ export default function KindFilter({
<Button
onClick={handleApply}
className="mt-4 w-full"
disabled={temporaryShowKinds.length === 0}
disabled={!canApply}
>
{t('Apply')}
</Button>

4
src/components/NormalFeed/index.tsx

@ -30,7 +30,7 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -30,7 +30,7 @@ const NormalFeed = forwardRef<TNoteListRef, {
logger.debug('NormalFeed component rendering with:', { subRequests, areAlgoRelays, isMainFeed })
const { t } = useTranslation()
const { hideUntrustedNotes } = useUserTrust()
const { showKinds } = useKindFilter()
const { showKinds, showKind1OPs, showRepliesAndComments } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [listMode, setListMode] = useState<TNoteListMode>(() => {
// Get stored mode preference
@ -267,6 +267,8 @@ const NormalFeed = forwardRef<TNoteListRef, { @@ -267,6 +267,8 @@ const NormalFeed = forwardRef<TNoteListRef, {
<NoteList
ref={noteListRef}
showKinds={temporaryShowKinds}
showKind1OPs={showKind1OPs}
showRepliesAndComments={showRepliesAndComments}
subRequests={subRequests}
hideReplies={listMode === 'posts'}
hideUntrustedNotes={hideUntrustedNotes}

37
src/components/NoteList/index.tsx

@ -19,7 +19,7 @@ import { useZap } from '@/providers/ZapProvider' @@ -19,7 +19,7 @@ import { useZap } from '@/providers/ZapProvider'
import client from '@/services/client.service'
import { TFeedSubRequest } from '@/types'
import dayjs from 'dayjs'
import { Event } from 'nostr-tools'
import { Event, kinds } from 'nostr-tools'
import { decode } from 'nostr-tools/nip19'
import {
forwardRef,
@ -44,6 +44,8 @@ const NoteList = forwardRef( @@ -44,6 +44,8 @@ const NoteList = forwardRef(
{
subRequests,
showKinds,
showKind1OPs = true,
showRepliesAndComments = true,
filterMutedNotes = true,
hideReplies = false,
hideUntrustedNotes = false,
@ -53,6 +55,8 @@ const NoteList = forwardRef( @@ -53,6 +55,8 @@ const NoteList = forwardRef(
}: {
subRequests: TFeedSubRequest[]
showKinds: number[]
showKind1OPs?: boolean
showRepliesAndComments?: boolean
filterMutedNotes?: boolean
hideReplies?: boolean
hideUntrustedNotes?: boolean
@ -136,8 +140,15 @@ const NoteList = forwardRef( @@ -136,8 +140,15 @@ const NoteList = forwardRef(
const idSet = new Set<string>()
return events.slice(0, showCount).filter((evt) => {
// Filter out events that aren't in the whitelisted kinds
if (!showKinds.includes(evt.kind)) return false
// Kind 1: show only OPs if showKind1OPs, only replies if showRepliesAndComments
if (evt.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(evt)
if (isReply && !showRepliesAndComments) return false
if (!isReply && !showKind1OPs) return false
}
// Kind 1111 (comments): show only if showRepliesAndComments
if (evt.kind === ExtendedKind.COMMENT && !showRepliesAndComments) return false
if (shouldHideEvent(evt)) return false
const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id
@ -147,14 +158,19 @@ const NoteList = forwardRef( @@ -147,14 +158,19 @@ const NoteList = forwardRef(
idSet.add(id)
return true
})
}, [events, showCount, shouldHideEvent, showKinds])
}, [events, showCount, shouldHideEvent, showKinds, showKind1OPs, showRepliesAndComments])
const filteredNewEvents = useMemo(() => {
const idSet = new Set<string>()
return newEvents.filter((event: Event) => {
// Filter out events that aren't in the whitelisted kinds
if (!showKinds.includes(event.kind)) return false
if (event.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(event)
if (isReply && !showRepliesAndComments) return false
if (!isReply && !showKind1OPs) return false
}
if (event.kind === ExtendedKind.COMMENT && !showRepliesAndComments) return false
if (shouldHideEvent(event)) return false
const id = isReplaceableEvent(event.kind)
@ -166,7 +182,7 @@ const NoteList = forwardRef( @@ -166,7 +182,7 @@ const NoteList = forwardRef(
idSet.add(id)
return true
})
}, [newEvents, shouldHideEvent, showKinds])
}, [newEvents, shouldHideEvent, showKinds, showKind1OPs, showRepliesAndComments])
const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
setTimeout(() => {
@ -221,10 +237,13 @@ const NoteList = forwardRef( @@ -221,10 +237,13 @@ const NoteList = forwardRef(
}
},
onNew: (event) => {
// Filter out events that aren't in the whitelisted kinds
if (!showKinds.includes(event.kind)) {
return
if (!showKinds.includes(event.kind)) return
if (event.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(event)
if (isReply && !showRepliesAndComments) return
if (!isReply && !showKind1OPs) return
}
if (event.kind === ExtendedKind.COMMENT && !showRepliesAndComments) return
if (pubkey && event.pubkey === pubkey) {
// If the new event is from the current user, insert it directly into the feed
setEvents((oldEvents) =>
@ -268,7 +287,7 @@ const NoteList = forwardRef( @@ -268,7 +287,7 @@ const NoteList = forwardRef(
return () => {
promise.then((closer) => closer())
}
}, [subRequestsKey, refreshCount, showKinds])
}, [subRequestsKey, refreshCount, showKinds, showKind1OPs, showRepliesAndComments])
useEffect(() => {
const options = {

2
src/constants.ts

@ -42,6 +42,8 @@ export const StorageKey = { @@ -42,6 +42,8 @@ export const StorageKey = {
DISMISSED_TOO_MANY_RELAYS_ALERT: 'dismissedTooManyRelaysAlert',
SHOW_KINDS: 'showKinds',
SHOW_KINDS_VERSION: 'showKindsVersion',
SHOW_KIND_1_OPs: 'showKind1OPs',
SHOW_REPLIES_AND_COMMENTS: 'showRepliesAndComments',
HIDE_CONTENT_MENTIONING_MUTED_USERS: 'hideContentMentioningMutedUsers',
NOTIFICATION_LIST_STYLE: 'notificationListStyle',
MEDIA_AUTO_LOAD_POLICY: 'mediaAutoLoadPolicy',

2
src/i18n/locales/de.ts

@ -367,6 +367,8 @@ export default { @@ -367,6 +367,8 @@ export default {
'Maybe Later': 'Vielleicht später',
"Don't remind me again": 'Nicht mehr erinnern',
Posts: 'Beiträge',
'Posts (OPs)': 'Beiträge (OPs)',
'Replies & comments': 'Antworten & Kommentare',
Articles: 'Artikel',
Highlights: 'Highlights',
Polls: 'Umfragen',

2
src/i18n/locales/en.ts

@ -374,6 +374,8 @@ export default { @@ -374,6 +374,8 @@ export default {
'Maybe Later': 'Maybe Later',
"Don't remind me again": "Don't remind me again",
Posts: 'Posts',
'Posts (OPs)': 'Posts (OPs)',
'Replies & comments': 'Replies & comments',
Articles: 'Articles',
Highlights: 'Highlights',
'A note from': 'A note from',

92
src/providers/KindFilterProvider.tsx

@ -1,11 +1,31 @@ @@ -1,11 +1,31 @@
import { createContext, useContext, useState } from 'react'
import { createContext, useContext, useState, useCallback, useMemo } from 'react'
import storage from '@/services/local-storage.service'
import { SUPPORTED_KINDS, ExtendedKind } from '@/constants'
import { kinds } from 'nostr-tools'
const KIND_1 = kinds.ShortTextNote
const KIND_1111 = ExtendedKind.COMMENT
/** Build showKinds array from base kinds (excluding 1 and 1111) plus the two post/reply flags */
function buildShowKinds(
baseKinds: number[],
showKind1OPs: boolean,
showRepliesAndComments: boolean
): number[] {
const rest = baseKinds.filter((k) => k !== KIND_1 && k !== KIND_1111)
const out = [...rest]
if (showKind1OPs || showRepliesAndComments) out.push(KIND_1)
if (showRepliesAndComments) out.push(KIND_1111)
return out.sort((a, b) => a - b)
}
type TKindFilterContext = {
showKinds: number[]
updateShowKinds: (kinds: number[]) => void
showKind1OPs: boolean
showRepliesAndComments: boolean
updateShowKinds: (kinds: number[], options?: { showKind1OPs?: boolean; showRepliesAndComments?: boolean }) => void
updateShowKind1OPs: (value: boolean) => void
updateShowRepliesAndComments: (value: boolean) => void
}
const KindFilterContext = createContext<TKindFilterContext | undefined>(undefined)
@ -20,33 +40,63 @@ export const useKindFilter = () => { @@ -20,33 +40,63 @@ export const useKindFilter = () => {
export function KindFilterProvider({ children }: { children: React.ReactNode }) {
// Ensure we always have a default value - show all supported kinds except reposts, publications, and publication content
// Publications (30040) and Publication Content (30041) should only be embedded, not shown in feeds
const defaultShowKinds = SUPPORTED_KINDS.filter(
kind => kind !== kinds.Repost &&
(kind) =>
kind !== kinds.Repost &&
kind !== ExtendedKind.PUBLICATION &&
kind !== ExtendedKind.PUBLICATION_CONTENT
)
const storedShowKinds = storage.getShowKinds()
const [showKinds, setShowKinds] = useState<number[]>(
const storedShowKind1OPs = storage.getShowKind1OPs()
const storedShowRepliesAndComments = storage.getShowRepliesAndComments()
const [showKinds, setShowKindsState] = useState<number[]>(
storedShowKinds.length > 0 ? storedShowKinds : defaultShowKinds
)
const [showKind1OPs, setShowKind1OPsState] = useState(storedShowKind1OPs)
const [showRepliesAndComments, setShowRepliesAndCommentsState] = useState(storedShowRepliesAndComments)
// Debug logging
// console.log('KindFilterProvider initialized:', {
// defaultShowKinds,
// storedShowKinds,
// finalShowKinds: showKinds,
// showKindsLength: showKinds.length
// })
const updateShowKinds = (kinds: number[]) => {
storage.setShowKinds(kinds)
setShowKinds(kinds)
}
const updateShowKinds = useCallback(
(newKinds: number[], options?: { showKind1OPs?: boolean; showRepliesAndComments?: boolean }) => {
const op = options?.showKind1OPs ?? newKinds.includes(KIND_1)
const replies = options?.showRepliesAndComments ?? newKinds.includes(KIND_1111)
storage.setShowKind1OPs(op)
storage.setShowRepliesAndComments(replies)
setShowKind1OPsState(op)
setShowRepliesAndCommentsState(replies)
storage.setShowKinds(newKinds)
setShowKindsState(newKinds)
},
[]
)
const updateShowKind1OPs = useCallback((value: boolean) => {
storage.setShowKind1OPs(value)
setShowKind1OPsState(value)
const next = buildShowKinds(showKinds, value, showRepliesAndComments)
storage.setShowKinds(next)
setShowKindsState(next)
}, [showKinds, showRepliesAndComments])
return (
<KindFilterContext.Provider value={{ showKinds, updateShowKinds }}>
{children}
</KindFilterContext.Provider>
const updateShowRepliesAndComments = useCallback((value: boolean) => {
storage.setShowRepliesAndComments(value)
setShowRepliesAndCommentsState(value)
const next = buildShowKinds(showKinds, showKind1OPs, value)
storage.setShowKinds(next)
setShowKindsState(next)
}, [showKinds, showKind1OPs])
const value = useMemo(
() => ({
showKinds,
showKind1OPs,
showRepliesAndComments,
updateShowKinds,
updateShowKind1OPs,
updateShowRepliesAndComments
}),
[showKinds, showKind1OPs, showRepliesAndComments, updateShowKinds, updateShowKind1OPs, updateShowRepliesAndComments]
)
return <KindFilterContext.Provider value={value}>{children}</KindFilterContext.Provider>
}

40
src/services/local-storage.service.ts

@ -48,6 +48,8 @@ class LocalStorageService { @@ -48,6 +48,8 @@ class LocalStorageService {
private defaultShowNsfw: boolean = false
private dismissedTooManyRelaysAlert: boolean = false
private showKinds: number[] = []
private showKind1OPs: boolean = true
private showRepliesAndComments: boolean = true
private hideContentMentioningMutedUsers: boolean = false
private notificationListStyle: TNotificationStyle = NOTIFICATION_LIST_STYLE.DETAILED
private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS
@ -241,6 +243,20 @@ class LocalStorageService { @@ -241,6 +243,20 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(this.showKinds))
window.localStorage.setItem(StorageKey.SHOW_KINDS_VERSION, '6')
// Feed filter: kind 1 OPs vs replies+comments (migrate from showKinds if not set)
const showKind1OPsStr = window.localStorage.getItem(StorageKey.SHOW_KIND_1_OPs)
const showRepliesStr = window.localStorage.getItem(StorageKey.SHOW_REPLIES_AND_COMMENTS)
if (showKind1OPsStr !== null) {
this.showKind1OPs = showKind1OPsStr === 'true'
} else {
this.showKind1OPs = this.showKinds.includes(kinds.ShortTextNote)
}
if (showRepliesStr !== null) {
this.showRepliesAndComments = showRepliesStr === 'true'
} else {
this.showRepliesAndComments = this.showKinds.includes(ExtendedKind.COMMENT)
}
this.hideContentMentioningMutedUsers =
window.localStorage.getItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS) === 'true'
@ -564,9 +580,27 @@ class LocalStorageService { @@ -564,9 +580,27 @@ class LocalStorageService {
return this.showKinds
}
setShowKinds(kinds: number[]) {
this.showKinds = kinds
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(kinds))
setShowKinds(newKinds: number[]) {
this.showKinds = newKinds
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(newKinds))
}
getShowKind1OPs(): boolean {
return this.showKind1OPs
}
setShowKind1OPs(value: boolean) {
this.showKind1OPs = value
window.localStorage.setItem(StorageKey.SHOW_KIND_1_OPs, value.toString())
}
getShowRepliesAndComments(): boolean {
return this.showRepliesAndComments
}
setShowRepliesAndComments(value: boolean) {
this.showRepliesAndComments = value
window.localStorage.setItem(StorageKey.SHOW_REPLIES_AND_COMMENTS, value.toString())
}
getHideContentMentioningMutedUsers() {

Loading…
Cancel
Save