feat: kind filter
This commit is contained in:
199
package-lock.json
generated
199
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@noble/hashes": "^1.6.1",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.4",
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||||
"@radix-ui/react-hover-card": "^1.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": {
|
"node_modules/@radix-ui/react-collection": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"@noble/hashes": "^1.6.1",
|
"@noble/hashes": "^1.6.1",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.4",
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||||
"@radix-ui/react-hover-card": "^1.1.4",
|
"@radix-ui/react-hover-card": "^1.1.4",
|
||||||
|
|||||||
31
src/App.tsx
31
src/App.tsx
@@ -1,22 +1,23 @@
|
|||||||
import 'yet-another-react-lightbox/styles.css'
|
import 'yet-another-react-lightbox/styles.css'
|
||||||
import './index.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 { 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 { 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 {
|
export default function App(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
@@ -34,8 +35,10 @@ export default function App(): JSX.Element {
|
|||||||
<FeedProvider>
|
<FeedProvider>
|
||||||
<ReplyProvider>
|
<ReplyProvider>
|
||||||
<MediaUploadServiceProvider>
|
<MediaUploadServiceProvider>
|
||||||
|
<KindFilterProvider>
|
||||||
<PageManager />
|
<PageManager />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
</KindFilterProvider>
|
||||||
</MediaUploadServiceProvider>
|
</MediaUploadServiceProvider>
|
||||||
</ReplyProvider>
|
</ReplyProvider>
|
||||||
</FeedProvider>
|
</FeedProvider>
|
||||||
|
|||||||
179
src/components/KindFilter/index.tsx
Normal file
179
src/components/KindFilter/index.tsx
Normal file
@@ -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 = (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="titlebar-icon"
|
||||||
|
className={cn('mr-1', !isFilterApplied && 'text-muted-foreground')}
|
||||||
|
onClick={() => {
|
||||||
|
if (isSmallScreen) {
|
||||||
|
setOpen(true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListFilter />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{SUPPORTED_KINDS.map(({ kindGroup, label }) => (
|
||||||
|
<Label
|
||||||
|
key={label}
|
||||||
|
className="focus:bg-accent/50 cursor-pointer flex items-start gap-3 rounded-lg border px-4 py-3 has-[[aria-checked=true]]:border-primary has-[[aria-checked=true]]:bg-primary/20"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id="toggle-2"
|
||||||
|
checked={kindGroup.every((k) => temporaryShowKinds.includes(k))}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
<p className="leading-none font-medium">{label}</p>
|
||||||
|
<p className="text-muted-foreground text-xs">kind {kindGroup.join(', ')}</p>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 mt-4">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setTemporaryShowKinds(DEFAULT_SHOW_KINDS)
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{t('Select All')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setTemporaryShowKinds([])
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{t('Clear All')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Label className="flex items-center gap-2 cursor-pointer mt-4">
|
||||||
|
<Checkbox
|
||||||
|
id="persistent-filter"
|
||||||
|
checked={isPersistent}
|
||||||
|
onCheckedChange={(checked) => setIsPersistent(!!checked)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm">{t('Remember my choice')}</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleApply}
|
||||||
|
className="mt-4 w-full"
|
||||||
|
disabled={temporaryShowKinds.length === 0}
|
||||||
|
>
|
||||||
|
{t('Apply')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{trigger}
|
||||||
|
<Drawer open={open} onOpenChange={setOpen}>
|
||||||
|
<DrawerTrigger asChild></DrawerTrigger>
|
||||||
|
<DrawerContent className="px-4">
|
||||||
|
<DrawerHeader />
|
||||||
|
{content}
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-96" collisionPadding={16}>
|
||||||
|
{content}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import NoteList, { TNoteListRef } from '@/components/NoteList'
|
import NoteList, { TNoteListRef } from '@/components/NoteList'
|
||||||
import Tabs from '@/components/Tabs'
|
import Tabs from '@/components/Tabs'
|
||||||
|
import { useKindFilter } from '@/providers/KindFilterProvider'
|
||||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||||
import storage from '@/services/local-storage.service'
|
import storage from '@/services/local-storage.service'
|
||||||
import { TFeedSubRequest, TNoteListMode } from '@/types'
|
import { TFeedSubRequest, TNoteListMode } from '@/types'
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
|
import KindFilter from '../KindFilter'
|
||||||
|
|
||||||
export default function NormalFeed({
|
export default function NormalFeed({
|
||||||
subRequests,
|
subRequests,
|
||||||
@@ -15,6 +17,8 @@ export default function NormalFeed({
|
|||||||
isMainFeed?: boolean
|
isMainFeed?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { hideUntrustedNotes } = useUserTrust()
|
const { hideUntrustedNotes } = useUserTrust()
|
||||||
|
const { showKinds } = useKindFilter()
|
||||||
|
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
|
||||||
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
|
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
|
||||||
const noteListRef = useRef<TNoteListRef>(null)
|
const noteListRef = useRef<TNoteListRef>(null)
|
||||||
|
|
||||||
@@ -23,9 +27,12 @@ export default function NormalFeed({
|
|||||||
if (isMainFeed) {
|
if (isMainFeed) {
|
||||||
storage.setNoteListMode(mode)
|
storage.setNoteListMode(mode)
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
noteListRef.current?.scrollToTop('smooth')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShowKindsChange = (newShowKinds: number[]) => {
|
||||||
|
setTemporaryShowKinds(newShowKinds)
|
||||||
noteListRef.current?.scrollToTop()
|
noteListRef.current?.scrollToTop()
|
||||||
}, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -39,9 +46,13 @@ export default function NormalFeed({
|
|||||||
onTabChange={(listMode) => {
|
onTabChange={(listMode) => {
|
||||||
handleListModeChange(listMode as TNoteListMode)
|
handleListModeChange(listMode as TNoteListMode)
|
||||||
}}
|
}}
|
||||||
|
options={
|
||||||
|
<KindFilter showKinds={temporaryShowKinds} onShowKindsChange={handleShowKindsChange} />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<NoteList
|
<NoteList
|
||||||
ref={noteListRef}
|
ref={noteListRef}
|
||||||
|
showKinds={temporaryShowKinds}
|
||||||
subRequests={subRequests}
|
subRequests={subRequests}
|
||||||
hideReplies={listMode === 'posts'}
|
hideReplies={listMode === 'posts'}
|
||||||
hideUntrustedNotes={hideUntrustedNotes}
|
hideUntrustedNotes={hideUntrustedNotes}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import NewNotesButton from '@/components/NewNotesButton'
|
import NewNotesButton from '@/components/NewNotesButton'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { ExtendedKind } from '@/constants'
|
|
||||||
import {
|
import {
|
||||||
getReplaceableCoordinateFromEvent,
|
getReplaceableCoordinateFromEvent,
|
||||||
isReplaceableEvent,
|
isReplaceableEvent,
|
||||||
@@ -12,7 +11,7 @@ import { useUserTrust } from '@/providers/UserTrustProvider'
|
|||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import { TFeedSubRequest } from '@/types'
|
import { TFeedSubRequest } from '@/types'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import PullToRefresh from 'react-simple-pull-to-refresh'
|
import PullToRefresh from 'react-simple-pull-to-refresh'
|
||||||
@@ -20,30 +19,20 @@ import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard'
|
|||||||
|
|
||||||
const LIMIT = 100
|
const LIMIT = 100
|
||||||
const ALGO_LIMIT = 500
|
const ALGO_LIMIT = 500
|
||||||
const KINDS = [
|
|
||||||
kinds.ShortTextNote,
|
|
||||||
kinds.Repost,
|
|
||||||
kinds.Highlights,
|
|
||||||
kinds.LongFormArticle,
|
|
||||||
ExtendedKind.COMMENT,
|
|
||||||
ExtendedKind.POLL,
|
|
||||||
ExtendedKind.VOICE,
|
|
||||||
ExtendedKind.VOICE_COMMENT,
|
|
||||||
ExtendedKind.PICTURE
|
|
||||||
]
|
|
||||||
|
|
||||||
const SHOW_COUNT = 10
|
const SHOW_COUNT = 10
|
||||||
|
|
||||||
const NoteList = forwardRef(
|
const NoteList = forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
subRequests,
|
subRequests,
|
||||||
|
showKinds,
|
||||||
filterMutedNotes = true,
|
filterMutedNotes = true,
|
||||||
hideReplies = false,
|
hideReplies = false,
|
||||||
hideUntrustedNotes = false,
|
hideUntrustedNotes = false,
|
||||||
areAlgoRelays = false
|
areAlgoRelays = false
|
||||||
}: {
|
}: {
|
||||||
subRequests: TFeedSubRequest[]
|
subRequests: TFeedSubRequest[]
|
||||||
|
showKinds: number[]
|
||||||
filterMutedNotes?: boolean
|
filterMutedNotes?: boolean
|
||||||
hideReplies?: boolean
|
hideReplies?: boolean
|
||||||
hideUntrustedNotes?: boolean
|
hideUntrustedNotes?: boolean
|
||||||
@@ -100,8 +89,10 @@ const NoteList = forwardRef(
|
|||||||
})
|
})
|
||||||
}, [newEvents, hideReplies, hideUntrustedNotes, filterMutedNotes, mutePubkeys])
|
}, [newEvents, hideReplies, hideUntrustedNotes, filterMutedNotes, mutePubkeys])
|
||||||
|
|
||||||
const scrollToTop = () => {
|
const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
|
||||||
topRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
setTimeout(() => {
|
||||||
|
topRef.current?.scrollIntoView({ behavior, block: 'start' })
|
||||||
|
}, 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({ scrollToTop }), [])
|
useImperativeHandle(ref, () => ({ scrollToTop }), [])
|
||||||
@@ -115,11 +106,17 @@ const NoteList = forwardRef(
|
|||||||
setNewEvents([])
|
setNewEvents([])
|
||||||
setHasMore(true)
|
setHasMore(true)
|
||||||
|
|
||||||
|
if (showKinds.length === 0) {
|
||||||
|
setLoading(false)
|
||||||
|
setHasMore(false)
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
|
||||||
const { closer, timelineKey } = await client.subscribeTimeline(
|
const { closer, timelineKey } = await client.subscribeTimeline(
|
||||||
subRequests.map(({ urls, filter }) => ({
|
subRequests.map(({ urls, filter }) => ({
|
||||||
urls,
|
urls,
|
||||||
filter: {
|
filter: {
|
||||||
kinds: KINDS,
|
kinds: showKinds,
|
||||||
...filter,
|
...filter,
|
||||||
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
|
limit: areAlgoRelays ? ALGO_LIMIT : LIMIT
|
||||||
}
|
}
|
||||||
@@ -156,7 +153,7 @@ const NoteList = forwardRef(
|
|||||||
return () => {
|
return () => {
|
||||||
promise.then((closer) => closer())
|
promise.then((closer) => closer())
|
||||||
}
|
}
|
||||||
}, [JSON.stringify(subRequests), refreshCount])
|
}, [JSON.stringify(subRequests), refreshCount, showKinds])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const options = {
|
const options = {
|
||||||
@@ -264,5 +261,5 @@ NoteList.displayName = 'NoteList'
|
|||||||
export default NoteList
|
export default NoteList
|
||||||
|
|
||||||
export type TNoteListRef = {
|
export type TNoteListRef = {
|
||||||
scrollToTop: () => void
|
scrollToTop: (behavior?: ScrollBehavior) => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider'
|
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider'
|
||||||
import { useMemo } from 'react'
|
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { ScrollArea, ScrollBar } from '../ui/scroll-area'
|
||||||
|
|
||||||
type TabDefinition = {
|
type TabDefinition = {
|
||||||
value: string
|
value: string
|
||||||
@@ -12,29 +13,72 @@ export default function Tabs({
|
|||||||
tabs,
|
tabs,
|
||||||
value,
|
value,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
threshold = 800
|
threshold = 800,
|
||||||
|
options = null
|
||||||
}: {
|
}: {
|
||||||
tabs: TabDefinition[]
|
tabs: TabDefinition[]
|
||||||
value: string
|
value: string
|
||||||
onTabChange?: (tab: string) => void
|
onTabChange?: (tab: string) => void
|
||||||
threshold?: number
|
threshold?: number
|
||||||
|
options?: ReactNode
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { deepBrowsing, lastScrollTop } = useDeepBrowsing()
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'sticky flex top-12 py-1 bg-background z-30 w-full transition-transform',
|
'sticky flex justify-between top-12 bg-background z-30 w-full transition-transform',
|
||||||
deepBrowsing && lastScrollTop > threshold ? '-translate-y-[calc(100%+12rem)]' : ''
|
deepBrowsing && lastScrollTop > threshold ? '-translate-y-[calc(100%+12rem)]' : ''
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tabs.map((tab) => (
|
<ScrollArea className="flex-1 w-0">
|
||||||
|
<div className="flex w-fit relative">
|
||||||
|
{tabs.map((tab, index) => (
|
||||||
<div
|
<div
|
||||||
key={tab.value}
|
key={tab.value}
|
||||||
|
ref={(el) => (tabRefs.current[index] = el)}
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex-1 text-center py-2 font-semibold clickable cursor-pointer rounded-lg`,
|
`w-fit text-center py-2 px-6 my-1 font-semibold clickable cursor-pointer rounded-lg`,
|
||||||
value === tab.value ? '' : 'text-muted-foreground'
|
value === tab.value ? '' : 'text-muted-foreground'
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -45,16 +89,16 @@ export default function Tabs({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-0 left-0 transition-all duration-500"
|
className="absolute bottom-0 h-1 bg-primary rounded-full transition-all duration-500"
|
||||||
style={{
|
style={{
|
||||||
width: `${100 / tabs.length}%`,
|
width: `${indicatorStyle.width}px`,
|
||||||
left: `${activeIndex >= 0 ? activeIndex * (100 / tabs.length) : 0}%`
|
left: `${indicatorStyle.left}px`
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<div className="px-4">
|
|
||||||
<div className="w-full h-1 bg-primary rounded-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ScrollBar orientation="horizontal" className="opacity-0 pointer-events-none" />
|
||||||
|
</ScrollArea>
|
||||||
|
{options && <div className="py-1 flex items-center">{options}</div>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/components/ui/checkbox.tsx
Normal file
26
src/components/ui/checkbox.tsx
Normal file
@@ -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<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'peer h-4 w-4 shrink-0 rounded-sm border border-accent shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
))
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
@@ -96,7 +96,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { kinds } from 'nostr-tools'
|
||||||
|
|
||||||
export const JUMBLE_API_BASE_URL = 'https://api.jumble.social'
|
export const JUMBLE_API_BASE_URL = 'https://api.jumble.social'
|
||||||
|
|
||||||
export const DEFAULT_FAVORITE_RELAYS = [
|
export const DEFAULT_FAVORITE_RELAYS = [
|
||||||
@@ -36,6 +38,7 @@ export const StorageKey = {
|
|||||||
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes',
|
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes',
|
||||||
DEFAULT_SHOW_NSFW: 'defaultShowNsfw',
|
DEFAULT_SHOW_NSFW: 'defaultShowNsfw',
|
||||||
DISMISSED_TOO_MANY_RELAYS_ALERT: 'dismissedTooManyRelaysAlert',
|
DISMISSED_TOO_MANY_RELAYS_ALERT: 'dismissedTooManyRelaysAlert',
|
||||||
|
SHOW_KINDS: 'showKinds',
|
||||||
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
|
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
|
||||||
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
|
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
|
||||||
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
|
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
|
||||||
@@ -74,6 +77,18 @@ export const ExtendedKind = {
|
|||||||
GROUP_METADATA: 39000
|
GROUP_METADATA: 39000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SHOW_KINDS = [
|
||||||
|
kinds.ShortTextNote,
|
||||||
|
kinds.Repost,
|
||||||
|
ExtendedKind.PICTURE,
|
||||||
|
ExtendedKind.POLL,
|
||||||
|
ExtendedKind.COMMENT,
|
||||||
|
ExtendedKind.VOICE,
|
||||||
|
ExtendedKind.VOICE_COMMENT,
|
||||||
|
kinds.Highlights,
|
||||||
|
kinds.LongFormArticle
|
||||||
|
]
|
||||||
|
|
||||||
export const URL_REGEX =
|
export const URL_REGEX =
|
||||||
/https?:\/\/[\w\p{L}\p{N}\p{M}&.-/?=#\-@%+_:!~*]+(?<![.,;:'")\]}!?,。;:""''!?】)])/gu
|
/https?:\/\/[\w\p{L}\p{N}\p{M}&.-/?=#\-@%+_:!~*]+(?<![.,;:'")\]}!?,。;:""''!?】)])/gu
|
||||||
export const WS_URL_REGEX =
|
export const WS_URL_REGEX =
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import KindFilter from '@/components/KindFilter'
|
||||||
import NoteList, { TNoteListRef } from '@/components/NoteList'
|
import NoteList, { TNoteListRef } from '@/components/NoteList'
|
||||||
import Tabs from '@/components/Tabs'
|
import Tabs from '@/components/Tabs'
|
||||||
import { BIG_RELAY_URLS } from '@/constants'
|
import { BIG_RELAY_URLS } from '@/constants'
|
||||||
|
import { useKindFilter } from '@/providers/KindFilterProvider'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import storage from '@/services/local-storage.service'
|
import storage from '@/services/local-storage.service'
|
||||||
@@ -15,6 +17,8 @@ export default function ProfileFeed({
|
|||||||
topSpace?: number
|
topSpace?: number
|
||||||
}) {
|
}) {
|
||||||
const { pubkey: myPubkey } = useNostr()
|
const { pubkey: myPubkey } = useNostr()
|
||||||
|
const { showKinds } = useKindFilter()
|
||||||
|
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
|
||||||
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
|
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
|
||||||
const noteListRef = useRef<TNoteListRef>(null)
|
const noteListRef = useRef<TNoteListRef>(null)
|
||||||
const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([])
|
const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([])
|
||||||
@@ -78,9 +82,12 @@ export default function ProfileFeed({
|
|||||||
|
|
||||||
const handleListModeChange = (mode: TNoteListMode) => {
|
const handleListModeChange = (mode: TNoteListMode) => {
|
||||||
setListMode(mode)
|
setListMode(mode)
|
||||||
setTimeout(() => {
|
noteListRef.current?.scrollToTop('smooth')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShowKindsChange = (newShowKinds: number[]) => {
|
||||||
|
setTemporaryShowKinds(newShowKinds)
|
||||||
noteListRef.current?.scrollToTop()
|
noteListRef.current?.scrollToTop()
|
||||||
}, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -92,8 +99,16 @@ export default function ProfileFeed({
|
|||||||
handleListModeChange(listMode as TNoteListMode)
|
handleListModeChange(listMode as TNoteListMode)
|
||||||
}}
|
}}
|
||||||
threshold={Math.max(800, topSpace)}
|
threshold={Math.max(800, topSpace)}
|
||||||
|
options={
|
||||||
|
<KindFilter showKinds={temporaryShowKinds} onShowKindsChange={handleShowKindsChange} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<NoteList
|
||||||
|
ref={noteListRef}
|
||||||
|
subRequests={subRequests}
|
||||||
|
showKinds={temporaryShowKinds}
|
||||||
|
hideReplies={listMode === 'posts'}
|
||||||
/>
|
/>
|
||||||
<NoteList ref={noteListRef} subRequests={subRequests} hideReplies={listMode === 'posts'} />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/providers/KindFilterProvider.tsx
Normal file
32
src/providers/KindFilterProvider.tsx
Normal file
@@ -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<TKindFilterContext | undefined>(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<number[]>(storage.getShowKinds())
|
||||||
|
|
||||||
|
const updateShowKinds = (kinds: number[]) => {
|
||||||
|
storage.setShowKinds(kinds)
|
||||||
|
setShowKinds(kinds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<KindFilterContext.Provider value={{ showKinds, updateShowKinds }}>
|
||||||
|
{children}
|
||||||
|
</KindFilterContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 { isSameAccount } from '@/lib/account'
|
||||||
import { randomString } from '@/lib/random'
|
import { randomString } from '@/lib/random'
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +34,7 @@ class LocalStorageService {
|
|||||||
private mediaUploadServiceConfigMap: Record<string, TMediaUploadServiceConfig> = {}
|
private mediaUploadServiceConfigMap: Record<string, TMediaUploadServiceConfig> = {}
|
||||||
private defaultShowNsfw: boolean = false
|
private defaultShowNsfw: boolean = false
|
||||||
private dismissedTooManyRelaysAlert: boolean = false
|
private dismissedTooManyRelaysAlert: boolean = false
|
||||||
|
private showKinds: number[] = []
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!LocalStorageService.instance) {
|
if (!LocalStorageService.instance) {
|
||||||
@@ -140,6 +141,9 @@ class LocalStorageService {
|
|||||||
this.dismissedTooManyRelaysAlert =
|
this.dismissedTooManyRelaysAlert =
|
||||||
window.localStorage.getItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT) === 'true'
|
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
|
// Clean up deprecated data
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
|
||||||
@@ -372,6 +376,15 @@ class LocalStorageService {
|
|||||||
this.dismissedTooManyRelaysAlert = dismissed
|
this.dismissedTooManyRelaysAlert = dismissed
|
||||||
window.localStorage.setItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT, dismissed.toString())
|
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()
|
const instance = new LocalStorageService()
|
||||||
|
|||||||
Reference in New Issue
Block a user