diff --git a/package-lock.json b/package-lock.json
index 780f3a96..b5f63ca8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"@noble/hashes": "^1.6.1",
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.2",
+ "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-hover-card": "^1.1.4",
@@ -2652,6 +2653,204 @@
}
}
},
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
+ "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-collection": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
diff --git a/package.json b/package.json
index 0d942310..f616ee1d 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"@noble/hashes": "^1.6.1",
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.2",
+ "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-hover-card": "^1.1.4",
diff --git a/src/App.tsx b/src/App.tsx
index 5a06345f..d88f1b5e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,22 +1,23 @@
import 'yet-another-react-lightbox/styles.css'
import './index.css'
+import { Toaster } from '@/components/ui/sonner'
+import { BookmarksProvider } from '@/providers/BookmarksProvider'
+import { ContentPolicyProvider } from '@/providers/ContentPolicyProvider'
+import { FavoriteRelaysProvider } from '@/providers/FavoriteRelaysProvider'
+import { FeedProvider } from '@/providers/FeedProvider'
+import { FollowListProvider } from '@/providers/FollowListProvider'
+import { KindFilterProvider } from '@/providers/KindFilterProvider'
+import { MediaUploadServiceProvider } from '@/providers/MediaUploadServiceProvider'
+import { MuteListProvider } from '@/providers/MuteListProvider'
+import { NostrProvider } from '@/providers/NostrProvider'
+import { ReplyProvider } from '@/providers/ReplyProvider'
+import { ScreenSizeProvider } from '@/providers/ScreenSizeProvider'
import { ThemeProvider } from '@/providers/ThemeProvider'
-import { Toaster } from './components/ui/sonner'
+import { TranslationServiceProvider } from '@/providers/TranslationServiceProvider'
+import { UserTrustProvider } from '@/providers/UserTrustProvider'
+import { ZapProvider } from '@/providers/ZapProvider'
import { PageManager } from './PageManager'
-import { BookmarksProvider } from './providers/BookmarksProvider'
-import { ContentPolicyProvider } from './providers/ContentPolicyProvider'
-import { FavoriteRelaysProvider } from './providers/FavoriteRelaysProvider'
-import { FeedProvider } from './providers/FeedProvider'
-import { FollowListProvider } from './providers/FollowListProvider'
-import { MediaUploadServiceProvider } from './providers/MediaUploadServiceProvider'
-import { MuteListProvider } from './providers/MuteListProvider'
-import { NostrProvider } from './providers/NostrProvider'
-import { ReplyProvider } from './providers/ReplyProvider'
-import { ScreenSizeProvider } from './providers/ScreenSizeProvider'
-import { TranslationServiceProvider } from './providers/TranslationServiceProvider'
-import { UserTrustProvider } from './providers/UserTrustProvider'
-import { ZapProvider } from './providers/ZapProvider'
export default function App(): JSX.Element {
return (
@@ -34,8 +35,10 @@ export default function App(): JSX.Element {
-
-
+
+
+
+
diff --git a/src/components/KindFilter/index.tsx b/src/components/KindFilter/index.tsx
new file mode 100644
index 00000000..3418e436
--- /dev/null
+++ b/src/components/KindFilter/index.tsx
@@ -0,0 +1,179 @@
+import { Button } from '@/components/ui/button'
+import { Checkbox } from '@/components/ui/checkbox'
+import { Drawer, DrawerContent, DrawerHeader, DrawerTrigger } from '@/components/ui/drawer'
+import { Label } from '@/components/ui/label'
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
+import { DEFAULT_SHOW_KINDS, ExtendedKind } from '@/constants'
+import { cn } from '@/lib/utils'
+import { useKindFilter } from '@/providers/KindFilterProvider'
+import { useScreenSize } from '@/providers/ScreenSizeProvider'
+import { ListFilter } from 'lucide-react'
+import { kinds } from 'nostr-tools'
+import { useEffect, useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+
+const SUPPORTED_KINDS = [
+ { kindGroup: [kinds.ShortTextNote, ExtendedKind.COMMENT], label: 'Posts' },
+ { kindGroup: [kinds.Repost], label: 'Reposts' },
+ { kindGroup: [kinds.LongFormArticle], label: 'Articles' },
+ { kindGroup: [kinds.Highlights], label: 'Highlights' },
+ { kindGroup: [ExtendedKind.POLL], label: 'Polls' },
+ { kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' },
+ { kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' }
+]
+
+export default function KindFilter({
+ showKinds,
+ onShowKindsChange
+}: {
+ showKinds: number[]
+ onShowKindsChange: (kinds: number[]) => void
+}) {
+ const { t } = useTranslation()
+ const { isSmallScreen } = useScreenSize()
+ const [open, setOpen] = useState(false)
+ const { updateShowKinds } = useKindFilter()
+ const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
+ const [isPersistent, setIsPersistent] = useState(false)
+ const isFilterApplied = useMemo(() => {
+ return showKinds.length !== DEFAULT_SHOW_KINDS.length
+ }, [showKinds])
+
+ useEffect(() => {
+ setTemporaryShowKinds(showKinds)
+ }, [open])
+
+ const handleApply = () => {
+ if (temporaryShowKinds.length === 0) {
+ // must select at least one kind
+ return
+ }
+
+ const newShowKinds = [...temporaryShowKinds].sort()
+ let isSame = true
+ for (let index = 0; index < newShowKinds.length; index++) {
+ if (showKinds[index] !== newShowKinds[index]) {
+ isSame = false
+ break
+ }
+ }
+ if (!isSame) {
+ onShowKindsChange(newShowKinds)
+ }
+
+ if (isPersistent) {
+ updateShowKinds(newShowKinds)
+ }
+
+ setIsPersistent(false)
+ setOpen(false)
+ }
+
+ const trigger = (
+
+ )
+
+ const content = (
+
+
+ {SUPPORTED_KINDS.map(({ kindGroup, label }) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ )
+
+ if (isSmallScreen) {
+ return (
+ <>
+ {trigger}
+
+
+
+
+ {content}
+
+
+ >
+ )
+ }
+
+ return (
+
+ {trigger}
+
+ {content}
+
+
+ )
+}
diff --git a/src/components/NormalFeed/index.tsx b/src/components/NormalFeed/index.tsx
index 8c528b0c..6c9f9fc3 100644
--- a/src/components/NormalFeed/index.tsx
+++ b/src/components/NormalFeed/index.tsx
@@ -1,9 +1,11 @@
import NoteList, { TNoteListRef } from '@/components/NoteList'
import Tabs from '@/components/Tabs'
+import { useKindFilter } from '@/providers/KindFilterProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import storage from '@/services/local-storage.service'
import { TFeedSubRequest, TNoteListMode } from '@/types'
import { useRef, useState } from 'react'
+import KindFilter from '../KindFilter'
export default function NormalFeed({
subRequests,
@@ -15,6 +17,8 @@ export default function NormalFeed({
isMainFeed?: boolean
}) {
const { hideUntrustedNotes } = useUserTrust()
+ const { showKinds } = useKindFilter()
+ const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [listMode, setListMode] = useState(() => storage.getNoteListMode())
const noteListRef = useRef(null)
@@ -23,9 +27,12 @@ export default function NormalFeed({
if (isMainFeed) {
storage.setNoteListMode(mode)
}
- setTimeout(() => {
- noteListRef.current?.scrollToTop()
- }, 0)
+ noteListRef.current?.scrollToTop('smooth')
+ }
+
+ const handleShowKindsChange = (newShowKinds: number[]) => {
+ setTemporaryShowKinds(newShowKinds)
+ noteListRef.current?.scrollToTop()
}
return (
@@ -39,9 +46,13 @@ export default function NormalFeed({
onTabChange={(listMode) => {
handleListModeChange(listMode as TNoteListMode)
}}
+ options={
+
+ }
/>
{
- topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
+ const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
+ setTimeout(() => {
+ topRef.current?.scrollIntoView({ behavior, block: 'start' })
+ }, 20)
}
useImperativeHandle(ref, () => ({ scrollToTop }), [])
@@ -115,11 +106,17 @@ const NoteList = forwardRef(
setNewEvents([])
setHasMore(true)
+ if (showKinds.length === 0) {
+ setLoading(false)
+ setHasMore(false)
+ return () => {}
+ }
+
const { closer, timelineKey } = await client.subscribeTimeline(
subRequests.map(({ urls, filter }) => ({
urls,
filter: {
- kinds: KINDS,
+ kinds: showKinds,
...filter,
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
}
@@ -156,7 +153,7 @@ const NoteList = forwardRef(
return () => {
promise.then((closer) => closer())
}
- }, [JSON.stringify(subRequests), refreshCount])
+ }, [JSON.stringify(subRequests), refreshCount, showKinds])
useEffect(() => {
const options = {
@@ -264,5 +261,5 @@ NoteList.displayName = 'NoteList'
export default NoteList
export type TNoteListRef = {
- scrollToTop: () => void
+ scrollToTop: (behavior?: ScrollBehavior) => void
}
diff --git a/src/components/Tabs/index.tsx b/src/components/Tabs/index.tsx
index 67524f8f..ec3038b9 100644
--- a/src/components/Tabs/index.tsx
+++ b/src/components/Tabs/index.tsx
@@ -1,7 +1,8 @@
import { cn } from '@/lib/utils'
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider'
-import { useMemo } from 'react'
+import { ReactNode, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
+import { ScrollArea, ScrollBar } from '../ui/scroll-area'
type TabDefinition = {
value: string
@@ -12,49 +13,92 @@ export default function Tabs({
tabs,
value,
onTabChange,
- threshold = 800
+ threshold = 800,
+ options = null
}: {
tabs: TabDefinition[]
value: string
onTabChange?: (tab: string) => void
threshold?: number
+ options?: ReactNode
}) {
const { t } = useTranslation()
const { deepBrowsing, lastScrollTop } = useDeepBrowsing()
- const activeIndex = useMemo(() => tabs.findIndex((tab) => tab.value === value), [value, tabs])
+ const tabRefs = useRef<(HTMLDivElement | null)[]>([])
+ const [indicatorStyle, setIndicatorStyle] = useState({ width: 0, left: 0 })
+
+ const updateIndicatorPosition = () => {
+ const activeIndex = tabs.findIndex((tab) => tab.value === value)
+ if (activeIndex >= 0 && tabRefs.current[activeIndex]) {
+ const activeTab = tabRefs.current[activeIndex]
+ const { offsetWidth, offsetLeft } = activeTab
+ const padding = 48 // 24px padding on each side
+ setIndicatorStyle({
+ width: offsetWidth - padding,
+ left: offsetLeft + padding / 2
+ })
+ }
+ }
+
+ useEffect(() => {
+ const animationId = requestAnimationFrame(() => {
+ updateIndicatorPosition()
+ })
+
+ return () => {
+ cancelAnimationFrame(animationId)
+ }
+ }, [tabs, value])
+
+ useEffect(() => {
+ const resizeObserver = new ResizeObserver(() => {
+ updateIndicatorPosition()
+ })
+
+ tabRefs.current.forEach((tab) => {
+ if (tab) resizeObserver.observe(tab)
+ })
+
+ return () => {
+ resizeObserver.disconnect()
+ }
+ }, [tabs])
return (
threshold ? '-translate-y-[calc(100%+12rem)]' : ''
)}
>
- {tabs.map((tab) => (
-
{
- onTabChange?.(tab.value)
- }}
- >
- {t(tab.label)}
+
+
+ {tabs.map((tab, index) => (
+
(tabRefs.current[index] = el)}
+ className={cn(
+ `w-fit text-center py-2 px-6 my-1 font-semibold clickable cursor-pointer rounded-lg`,
+ value === tab.value ? '' : 'text-muted-foreground'
+ )}
+ onClick={() => {
+ onTabChange?.(tab.value)
+ }}
+ >
+ {t(tab.label)}
+
+ ))}
+
- ))}
- = 0 ? activeIndex * (100 / tabs.length) : 0}%`
- }}
- >
-
-
+
+
+ {options &&
{options}
}
)
}
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
new file mode 100644
index 00000000..2a37e3d8
--- /dev/null
+++ b/src/components/ui/checkbox.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react'
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
+import { Check } from 'lucide-react'
+
+import { cn } from '@/lib/utils'
+
+const Checkbox = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
index 37cd1f53..ee8abd12 100644
--- a/src/components/ui/dropdown-menu.tsx
+++ b/src/components/ui/dropdown-menu.tsx
@@ -96,7 +96,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
(() => storage.getNoteListMode())
const noteListRef = useRef(null)
const [subRequests, setSubRequests] = useState([])
@@ -78,9 +82,12 @@ export default function ProfileFeed({
const handleListModeChange = (mode: TNoteListMode) => {
setListMode(mode)
- setTimeout(() => {
- noteListRef.current?.scrollToTop()
- }, 0)
+ noteListRef.current?.scrollToTop('smooth')
+ }
+
+ const handleShowKindsChange = (newShowKinds: number[]) => {
+ setTemporaryShowKinds(newShowKinds)
+ noteListRef.current?.scrollToTop()
}
return (
@@ -92,8 +99,16 @@ export default function ProfileFeed({
handleListModeChange(listMode as TNoteListMode)
}}
threshold={Math.max(800, topSpace)}
+ options={
+
+ }
+ />
+
-
>
)
}
diff --git a/src/providers/KindFilterProvider.tsx b/src/providers/KindFilterProvider.tsx
new file mode 100644
index 00000000..09867a40
--- /dev/null
+++ b/src/providers/KindFilterProvider.tsx
@@ -0,0 +1,32 @@
+import { createContext, useContext, useState } from 'react'
+import storage from '@/services/local-storage.service'
+
+type TKindFilterContext = {
+ showKinds: number[]
+ updateShowKinds: (kinds: number[]) => void
+}
+
+const KindFilterContext = createContext(undefined)
+
+export const useKindFilter = () => {
+ const context = useContext(KindFilterContext)
+ if (!context) {
+ throw new Error('useKindFilter must be used within a KindFilterProvider')
+ }
+ return context
+}
+
+export function KindFilterProvider({ children }: { children: React.ReactNode }) {
+ const [showKinds, setShowKinds] = useState(storage.getShowKinds())
+
+ const updateShowKinds = (kinds: number[]) => {
+ storage.setShowKinds(kinds)
+ setShowKinds(kinds)
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts
index e718d844..a0429703 100644
--- a/src/services/local-storage.service.ts
+++ b/src/services/local-storage.service.ts
@@ -1,4 +1,4 @@
-import { DEFAULT_NIP_96_SERVICE, StorageKey } from '@/constants'
+import { DEFAULT_NIP_96_SERVICE, DEFAULT_SHOW_KINDS, StorageKey } from '@/constants'
import { isSameAccount } from '@/lib/account'
import { randomString } from '@/lib/random'
import {
@@ -34,6 +34,7 @@ class LocalStorageService {
private mediaUploadServiceConfigMap: Record = {}
private defaultShowNsfw: boolean = false
private dismissedTooManyRelaysAlert: boolean = false
+ private showKinds: number[] = []
constructor() {
if (!LocalStorageService.instance) {
@@ -140,6 +141,9 @@ class LocalStorageService {
this.dismissedTooManyRelaysAlert =
window.localStorage.getItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT) === 'true'
+ const showKindsStr = window.localStorage.getItem(StorageKey.SHOW_KINDS)
+ this.showKinds = showKindsStr ? JSON.parse(showKindsStr) : DEFAULT_SHOW_KINDS
+
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@@ -372,6 +376,15 @@ class LocalStorageService {
this.dismissedTooManyRelaysAlert = dismissed
window.localStorage.setItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT, dismissed.toString())
}
+
+ getShowKinds() {
+ return this.showKinds
+ }
+
+ setShowKinds(kinds: number[]) {
+ this.showKinds = kinds
+ window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(kinds))
+ }
}
const instance = new LocalStorageService()