diff --git a/src/components/KindFilter/index.tsx b/src/components/KindFilter/index.tsx index b853928a..53487758 100644 --- a/src/components/KindFilter/index.tsx +++ b/src/components/KindFilter/index.tsx @@ -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 = [ { 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({ }) { 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(() => { - setTemporaryShowKinds(showKinds) - setIsPersistent(false) - }, [open]) + if (open) { + setTemporaryShowKinds(showKinds) + setTemporaryShowKind1OPs(savedShowKind1OPs) + setTemporaryShowRepliesAndComments(savedShowRepliesAndComments) + setIsPersistent(false) + } + }, [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({ const content = (
+ {/* Posts (OPs) - kind 1 top-level only */} +
setTemporaryShowKind1OPs((prev) => !prev)} + > +

{t('Posts (OPs)')}

+

kind {KIND_1}

+
+ {/* Replies & comments - kind 1 replies + kind 1111 */} +
setTemporaryShowRepliesAndComments((prev) => !prev)} + > +

{t('Replies & comments')}

+

kind {KIND_1}, {KIND_1111}

+
{KIND_FILTER_OPTIONS.map(({ kindGroup, label }) => { const checked = kindGroup.every((k) => temporaryShowKinds.includes(k)) return ( @@ -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({ diff --git a/src/components/NormalFeed/index.tsx b/src/components/NormalFeed/index.tsx index a4d80647..276b34df 100644 --- a/src/components/NormalFeed/index.tsx +++ b/src/components/NormalFeed/index.tsx @@ -30,7 +30,7 @@ const NormalFeed = forwardRef(() => { // Get stored mode preference @@ -267,6 +267,8 @@ const NormalFeed = forwardRef() 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( idSet.add(id) return true }) - }, [events, showCount, shouldHideEvent, showKinds]) + }, [events, showCount, shouldHideEvent, showKinds, showKind1OPs, showRepliesAndComments]) const filteredNewEvents = useMemo(() => { const idSet = new Set() 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( 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( } }, 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( return () => { promise.then((closer) => closer()) } - }, [subRequestsKey, refreshCount, showKinds]) + }, [subRequestsKey, refreshCount, showKinds, showKind1OPs, showRepliesAndComments]) useEffect(() => { const options = { diff --git a/src/constants.ts b/src/constants.ts index b8955d87..9e0e5273 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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', diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 21178bd9..b91e5c00 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -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', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 4d709dbf..e6042238 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -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', diff --git a/src/providers/KindFilterProvider.tsx b/src/providers/KindFilterProvider.tsx index ff5acf8f..038113ba 100644 --- a/src/providers/KindFilterProvider.tsx +++ b/src/providers/KindFilterProvider.tsx @@ -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(undefined) @@ -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 !== ExtendedKind.PUBLICATION && - kind !== ExtendedKind.PUBLICATION_CONTENT + (kind) => + kind !== kinds.Repost && + kind !== ExtendedKind.PUBLICATION && + kind !== ExtendedKind.PUBLICATION_CONTENT ) const storedShowKinds = storage.getShowKinds() - const [showKinds, setShowKinds] = useState( + const storedShowKind1OPs = storage.getShowKind1OPs() + const storedShowRepliesAndComments = storage.getShowRepliesAndComments() + + const [showKinds, setShowKindsState] = useState( 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 ( - - {children} - + 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 {children} } diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts index 70dc83a8..af00221e 100644 --- a/src/services/local-storage.service.ts +++ b/src/services/local-storage.service.ts @@ -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 { 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 { 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() {