diff --git a/package.json b/package.json
index 512f467..e712708 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "jumble-imwald",
- "version": "14.8",
+ "version": "15.0.0",
"description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble",
"private": true,
"type": "module",
diff --git a/src/components/ContentPreview/FollowPackPreview.tsx b/src/components/ContentPreview/FollowPackPreview.tsx
new file mode 100644
index 0000000..444c3ff
--- /dev/null
+++ b/src/components/ContentPreview/FollowPackPreview.tsx
@@ -0,0 +1,92 @@
+import { getPubkeysFromPTags } from '@/lib/tag'
+import { cn } from '@/lib/utils'
+import { Event } from 'nostr-tools'
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import { SimpleUserAvatar } from '../UserAvatar'
+import { Users, ExternalLink } from 'lucide-react'
+import { Button } from '../ui/button'
+import { toFollowPacks } from '@/lib/link'
+import { useSecondaryPage } from '@/PageManager'
+
+export default function FollowPackPreview({
+ event,
+ className
+}: {
+ event: Event
+ className?: string
+}) {
+ const { t } = useTranslation()
+ const { push } = useSecondaryPage()
+
+ const packPubkeys = useMemo(() => getPubkeysFromPTags(event.tags), [event.tags])
+
+ const getPackTitle = (pack: Event): string => {
+ const titleTag = pack.tags.find(tag => tag[0] === 'title' || tag[0] === 'name')
+ return titleTag?.[1] || t('Follow Pack')
+ }
+
+ const getPackDescription = (pack: Event): string => {
+ const descTag = pack.tags.find(tag => tag[0] === 'description' || tag[0] === 'd')
+ return descTag?.[1] || ''
+ }
+
+ const title = getPackTitle(event)
+ const description = getPackDescription(event)
+
+ const handleOpenInViewer = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ push(toFollowPacks())
+ }
+
+ return (
+
+
+ [{t('Follow Pack')}]
+ {title}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+
+
+ {t('{{count}} profiles', { count: packPubkeys.length })}
+
+
+ {packPubkeys.length > 0 && (
+
+ {packPubkeys.slice(0, 5).map((pubkey) => (
+
+ ))}
+ {packPubkeys.length > 5 && (
+
+ +{packPubkeys.length - 5}
+
+ )}
+
+ )}
+
+
+
+
+ )
+}
+
diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx
index 8772acc..2639400 100644
--- a/src/components/ContentPreview/index.tsx
+++ b/src/components/ContentPreview/index.tsx
@@ -19,6 +19,7 @@ import ZapPreview from './ZapPreview'
import DiscussionNote from '../DiscussionNote'
import ApplicationHandlerInfo from '../ApplicationHandlerInfo'
import ApplicationHandlerRecommendation from '../ApplicationHandlerRecommendation'
+import FollowPackPreview from './FollowPackPreview'
export default function ContentPreview({
event,
@@ -121,5 +122,9 @@ export default function ContentPreview({
return
}
+ if (event.kind === ExtendedKind.FOLLOW_PACK) {
+ return
+ }
+
return [{t('Cannot handle event of kind k', { k: event.kind })}]
}
diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx
index 5d40f7d..68ba780 100644
--- a/src/components/Note/index.tsx
+++ b/src/components/Note/index.tsx
@@ -39,6 +39,7 @@ import VideoNote from './VideoNote'
import RelayReview from './RelayReview'
import Zap from './Zap'
import CitationCard from '@/components/CitationCard'
+import FollowPackPreview from '../ContentPreview/FollowPackPreview'
export default function Note({
event,
@@ -78,7 +79,8 @@ export default function Note({
ExtendedKind.PUBLIC_MESSAGE,
ExtendedKind.ZAP_REQUEST,
ExtendedKind.ZAP_RECEIPT,
- ExtendedKind.PUBLICATION_CONTENT // Only for rendering embedded content, not in feeds
+ ExtendedKind.PUBLICATION_CONTENT, // Only for rendering embedded content, not in feeds
+ ExtendedKind.FOLLOW_PACK // Only for rendering embedded content, not in feeds
]
@@ -173,6 +175,8 @@ export default function Note({
content =
} else if (event.kind === ExtendedKind.ZAP_REQUEST || event.kind === ExtendedKind.ZAP_RECEIPT) {
content =
+ } else if (event.kind === ExtendedKind.FOLLOW_PACK) {
+ content =
} else if (event.kind === kinds.ShortTextNote || event.kind === ExtendedKind.COMMENT || event.kind === ExtendedKind.VOICE_COMMENT) {
// Plain text notes use MarkdownArticle for proper markdown rendering
content =
diff --git a/src/constants.ts b/src/constants.ts
index ec87b3b..5d2ef09 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -163,7 +163,8 @@ export const ExtendedKind = {
// NIP-89 Application Handlers
APPLICATION_HANDLER_RECOMMENDATION: 31989,
APPLICATION_HANDLER_INFO: 31990,
- PAYMENT_INFO: 10133
+ PAYMENT_INFO: 10133,
+ FOLLOW_PACK: 39089
}
export const SUPPORTED_KINDS = [
diff --git a/src/providers/KindFilterProvider.tsx b/src/providers/KindFilterProvider.tsx
index 27e8784..ff5acf8 100644
--- a/src/providers/KindFilterProvider.tsx
+++ b/src/providers/KindFilterProvider.tsx
@@ -1,6 +1,6 @@
import { createContext, useContext, useState } from 'react'
import storage from '@/services/local-storage.service'
-import { SUPPORTED_KINDS } from '@/constants'
+import { SUPPORTED_KINDS, ExtendedKind } from '@/constants'
import { kinds } from 'nostr-tools'
type TKindFilterContext = {
@@ -19,8 +19,13 @@ export const useKindFilter = () => {
}
export function KindFilterProvider({ children }: { children: React.ReactNode }) {
- // Ensure we always have a default value - show all supported kinds except reposts
- const defaultShowKinds = SUPPORTED_KINDS.filter(kind => kind !== kinds.Repost)
+ // 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
+ )
const storedShowKinds = storage.getShowKinds()
const [showKinds, setShowKinds] = useState(
storedShowKinds.length > 0 ? storedShowKinds : defaultShowKinds
diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts
index 3c8eb8a..b52cf7b 100644
--- a/src/services/local-storage.service.ts
+++ b/src/services/local-storage.service.ts
@@ -181,8 +181,13 @@ class LocalStorageService {
const showKindsStr = window.localStorage.getItem(StorageKey.SHOW_KINDS)
if (!showKindsStr) {
- // Default: show all supported kinds except reposts
- this.showKinds = SUPPORTED_KINDS.filter(kind => kind !== kinds.Repost)
+ // Default: show all supported kinds except reposts, publications, and publication content
+ // Publications (30040) and Publication Content (30041) should only be embedded, not shown in feeds
+ this.showKinds = SUPPORTED_KINDS.filter(
+ kind => kind !== kinds.Repost &&
+ kind !== ExtendedKind.PUBLICATION &&
+ kind !== ExtendedKind.PUBLICATION_CONTENT
+ )
} else {
const showKindsVersionStr = window.localStorage.getItem(StorageKey.SHOW_KINDS_VERSION)
const showKindsVersion = showKindsVersionStr ? parseInt(showKindsVersionStr) : 0
@@ -219,10 +224,21 @@ class LocalStorageService {
showKinds.splice(pubContentIndex, 1)
}
}
+ if (showKindsVersion < 6) {
+ // Remove publications and publication content from existing users' filters (should only be embedded, not in feeds)
+ const pubIndex = showKinds.indexOf(ExtendedKind.PUBLICATION)
+ if (pubIndex !== -1) {
+ showKinds.splice(pubIndex, 1)
+ }
+ const pubContentIndex = showKinds.indexOf(ExtendedKind.PUBLICATION_CONTENT)
+ if (pubContentIndex !== -1) {
+ showKinds.splice(pubContentIndex, 1)
+ }
+ }
this.showKinds = showKinds
}
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(this.showKinds))
- window.localStorage.setItem(StorageKey.SHOW_KINDS_VERSION, '5')
+ window.localStorage.setItem(StorageKey.SHOW_KINDS_VERSION, '6')
this.hideContentMentioningMutedUsers =
window.localStorage.getItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS) === 'true'