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()