From 1cbb7b7428194f2673f0e38ed315b5ce4e16ed3b Mon Sep 17 00:00:00 2001 From: Derek Ross Date: Mon, 2 Mar 2026 16:51:58 -0500 Subject: [PATCH 1/3] Add Development feed with NIP-34 git repos, patches, PRs, custom NIPs, and app submissions New 'Development' section under Other Stuff in Content Settings that aggregates developer-focused Nostr content: git repository announcements (kind 30617), patches (kind 1617), pull requests (kind 1618), custom NIP proposals (kind 30817), and app submissions (kind 31733). Adds /dev route using KindFeedPage with external links to Gitworkshop and NostrHub. Five new card components render each kind in the feed. --- src/App.tsx | 2 + src/AppRouter.tsx | 2 + src/components/AppSubmissionCard.tsx | 74 ++++++++++++++++++++++++++ src/components/CustomNipCard.tsx | 47 +++++++++++++++++ src/components/GitRepoCard.tsx | 78 ++++++++++++++++++++++++++++ src/components/InitialSyncGate.tsx | 2 + src/components/NoteCard.tsx | 24 ++++++++- src/components/PatchCard.tsx | 39 ++++++++++++++ src/components/PullRequestCard.tsx | 47 +++++++++++++++++ src/contexts/AppContext.ts | 4 ++ src/lib/extraKinds.ts | 20 ++++++- src/lib/schemas.ts | 2 + src/lib/sidebarItems.tsx | 7 +-- src/test/TestApp.tsx | 2 + 14 files changed, 344 insertions(+), 6 deletions(-) create mode 100644 src/components/AppSubmissionCard.tsx create mode 100644 src/components/CustomNipCard.tsx create mode 100644 src/components/GitRepoCard.tsx create mode 100644 src/components/PatchCard.tsx create mode 100644 src/components/PullRequestCard.tsx diff --git a/src/App.tsx b/src/App.tsx index 46de047b..9f745346 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -95,6 +95,8 @@ const hardcodedConfig: AppConfig = { showPodcasts: false, feedIncludePodcastEpisodes: false, feedIncludePodcastTrailers: false, + showDevelopment: false, + feedIncludeDevelopment: false, followsFeedShowReplies: true, }, sidebarOrder: ['feed', 'notifications', 'search', 'bookmarks', 'profile', 'photos', 'videos', 'themes', 'theme', 'settings'], diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 5f69bba1..ac11b11c 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -50,6 +50,7 @@ const packsDef = getExtraKindDef('packs')!; const articlesDef = getExtraKindDef('articles')!; const decksDef = getExtraKindDef('decks')!; const emojiPacksDef = getExtraKindDef('emoji-packs')!; +const developmentDef = getExtraKindDef('development')!; /** Redirects /profile to the user's canonical profile URL (nip05 or npub). */ function ProfileRedirect() { @@ -103,6 +104,7 @@ export function AppRouter() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/AppSubmissionCard.tsx b/src/components/AppSubmissionCard.tsx new file mode 100644 index 00000000..7a2891c2 --- /dev/null +++ b/src/components/AppSubmissionCard.tsx @@ -0,0 +1,74 @@ +import { Rocket, ExternalLink } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import type { NostrEvent } from '@nostrify/nostrify'; + +interface AppSubmissionCardProps { + event: NostrEvent; +} + +/** Renders a Shakespeare kind 31733 app submission as a compact card. */ +export function AppSubmissionCard({ event }: AppSubmissionCardProps) { + const title = event.tags.find(([n]) => n === 'title')?.[1] + ?? event.tags.find(([n]) => n === 'name')?.[1]; + const website = event.tags.find(([n]) => n === 'website')?.[1] + ?? event.tags.find(([n]) => n === 'r')?.[1]; + const icon = event.tags.find(([n]) => n === 'icon')?.[1] + ?? event.tags.find(([n]) => n === 'image')?.[1]; + const hashtags = event.tags.filter(([n]) => n === 't').map(([, v]) => v) + .filter((t) => t !== 'soapbox-app-submission'); + const description = event.content.trim().slice(0, 200); + + const displayTitle = title || 'App Submission'; + + return ( +
+ {/* Header */} +
+ {icon ? ( + { e.currentTarget.style.display = 'none'; }} + /> + ) : ( +
+ +
+ )} +
+
+ {displayTitle} +
+ {description && ( +

{description}{event.content.length > 200 ? '…' : ''}

+ )} +
+
+ + {/* Tags */} + {hashtags.length > 0 && ( +
+ {hashtags.slice(0, 6).map((tag) => ( + {tag} + ))} +
+ )} + + {/* Website link */} + {website && ( + e.stopPropagation()} + > + + {new URL(website).hostname} + + )} +
+ ); +} diff --git a/src/components/CustomNipCard.tsx b/src/components/CustomNipCard.tsx new file mode 100644 index 00000000..c02c5ae6 --- /dev/null +++ b/src/components/CustomNipCard.tsx @@ -0,0 +1,47 @@ +import { FileCode } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import type { NostrEvent } from '@nostrify/nostrify'; + +interface CustomNipCardProps { + event: NostrEvent; +} + +/** Renders a NostrHub kind 30817 custom NIP proposal as a compact card. */ +export function CustomNipCard({ event }: CustomNipCardProps) { + const title = event.tags.find(([n]) => n === 'title')?.[1]; + const dTag = event.tags.find(([n]) => n === 'd')?.[1] ?? ''; + const relatedKinds = event.tags.filter(([n]) => n === 'k').map(([, v]) => v); + + // Extract first ~200 chars of content as preview, stripping markdown headings + const contentPreview = event.content + .replace(/^#{1,6}\s+.*/gm, '') // strip headings + .replace(/\n{2,}/g, '\n') // collapse blank lines + .trim() + .slice(0, 200); + + const displayTitle = title || `NIP: ${dTag}`; + + return ( +
+ {/* Header */} +
+ +
+ {displayTitle} + {contentPreview && ( +

{contentPreview}{event.content.length > 200 ? '…' : ''}

+ )} +
+
+ + {/* Related kinds */} + {relatedKinds.length > 0 && ( +
+ {relatedKinds.slice(0, 8).map((k) => ( + kind:{k} + ))} +
+ )} +
+ ); +} diff --git a/src/components/GitRepoCard.tsx b/src/components/GitRepoCard.tsx new file mode 100644 index 00000000..3a499d47 --- /dev/null +++ b/src/components/GitRepoCard.tsx @@ -0,0 +1,78 @@ +import { GitBranch, ExternalLink, Globe, Copy } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import type { NostrEvent } from '@nostrify/nostrify'; + +interface GitRepoCardProps { + event: NostrEvent; +} + +/** Renders a NIP-34 kind 30617 repository announcement as a compact card. */ +export function GitRepoCard({ event }: GitRepoCardProps) { + const name = event.tags.find(([n]) => n === 'name')?.[1]; + const description = event.tags.find(([n]) => n === 'description')?.[1]; + const webUrls = event.tags.filter(([n]) => n === 'web').map(([, v]) => v); + const cloneUrls = event.tags.filter(([n]) => n === 'clone').map(([, v]) => v); + const hashtags = event.tags.filter(([n]) => n === 't').map(([, v]) => v).filter((t) => t !== 'personal-fork'); + const isPersonalFork = event.tags.some(([n, v]) => n === 't' && v === 'personal-fork'); + const dTag = event.tags.find(([n]) => n === 'd')?.[1] ?? ''; + + const displayName = name || dTag; + + return ( +
+ {/* Header */} +
+ +
+
+ {displayName} + {isPersonalFork && ( + fork + )} +
+ {description && ( +

{description}

+ )} +
+
+ + {/* Tags */} + {hashtags.length > 0 && ( +
+ {hashtags.slice(0, 6).map((tag) => ( + {tag} + ))} +
+ )} + + {/* Links */} +
+ {webUrls[0] && ( + e.stopPropagation()} + > + + Web + + + )} + {cloneUrls[0] && ( + + )} +
+
+ ); +} diff --git a/src/components/InitialSyncGate.tsx b/src/components/InitialSyncGate.tsx index f8c3f0c4..007c9cc4 100644 --- a/src/components/InitialSyncGate.tsx +++ b/src/components/InitialSyncGate.tsx @@ -368,6 +368,8 @@ function SetupQuestionnaire({ onComplete, onPreload, isSignup = false }: { showPodcasts: false, feedIncludePodcastEpisodes: false, feedIncludePodcastTrailers: false, + showDevelopment: false, + feedIncludeDevelopment: false, followsFeedShowReplies: true, }; diff --git a/src/components/NoteCard.tsx b/src/components/NoteCard.tsx index 831588bd..8d7b8bd2 100644 --- a/src/components/NoteCard.tsx +++ b/src/components/NoteCard.tsx @@ -16,8 +16,14 @@ import { GeocacheContent } from '@/components/GeocacheContent'; import { FoundLogContent } from '@/components/FoundLogContent'; import { ColorMomentContent, ColorMomentEyeButton } from '@/components/ColorMomentContent'; import { FollowPackContent } from '@/components/FollowPackContent'; +import { AppSubmissionCard } from '@/components/AppSubmissionCard'; import { ArticleContent } from '@/components/ArticleContent'; import { type ImetaEntry, parseImetaMap } from '@/lib/imeta'; +import { CustomNipCard } from '@/components/CustomNipCard'; +import { GitRepoCard } from '@/components/GitRepoCard'; +import { PatchCard } from '@/components/PatchCard'; +import { PullRequestCard } from '@/components/PullRequestCard'; +import { WebxdcEmbed } from '@/components/WebxdcEmbed'; import { MagicDeckContent } from '@/components/MagicDeckContent'; import { EmojiPackContent } from '@/components/EmojiPackContent'; import { FileMetadataContent } from '@/components/FileMetadataContent'; @@ -193,7 +199,13 @@ export function NoteCard({ event, className, repostedBy, compact, threaded, thre const isPodcastEpisode = event.kind === 30054; const isPodcastTrailer = event.kind === 30055; const isAudioKind = isMusicTrack || isMusicPlaylist || isPodcastEpisode || isPodcastTrailer; - const isTextNote = !isVine && !isPoll && !isGeocache && !isFoundLog && !isColor && !isFollowPack && !isArticle && !isMagicDeck && !isStream && !isFileMetadata && !isTheme && !isVoiceMessage && !isCalendarEvent && !isEmojiPack && !isReaction && !isPhoto && !isVideo && !isAudioKind; + const isGitRepo = event.kind === 30617; + const isPatch = event.kind === 1617; + const isPullRequest = event.kind === 1618; + const isCustomNip = event.kind === 30817; + const isAppSubmission = event.kind === 31733; + const isDevKind = isGitRepo || isPatch || isPullRequest || isCustomNip || isAppSubmission; + const isTextNote = !isVine && !isPoll && !isGeocache && !isFoundLog && !isColor && !isFollowPack && !isArticle && !isMagicDeck && !isStream && !isFileMetadata && !isTheme && !isVoiceMessage && !isCalendarEvent && !isEmojiPack && !isReaction && !isPhoto && !isVideo && !isAudioKind && !isDevKind; // Kind 1 specific — images now render inline in NoteContent, only videos go to NoteMedia const videos = useMemo(() => isTextNote ? extractVideoUrls(event.content) : [], [event.content, isTextNote]); @@ -325,6 +337,16 @@ export function NoteCard({ event, className, repostedBy, compact, threaded, thre ) : isPodcastTrailer ? ( + ) : isGitRepo ? ( + + ) : isPatch ? ( + + ) : isPullRequest ? ( + + ) : isCustomNip ? ( + + ) : isAppSubmission ? ( + ) : ( )} diff --git a/src/components/PatchCard.tsx b/src/components/PatchCard.tsx new file mode 100644 index 00000000..855c99cc --- /dev/null +++ b/src/components/PatchCard.tsx @@ -0,0 +1,39 @@ +import { FileText } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import type { NostrEvent } from '@nostrify/nostrify'; + +interface PatchCardProps { + event: NostrEvent; +} + +/** Renders a NIP-34 kind 1617 patch event as a compact card. */ +export function PatchCard({ event }: PatchCardProps) { + // Subject is typically the first line of the patch content (git format-patch) + const firstLine = event.content.split('\n')[0]?.trim() ?? ''; + const subject = firstLine.startsWith('Subject:') + ? firstLine.replace(/^Subject:\s*(\[PATCH[^\]]*\])?\s*/, '') + : firstLine; + + const isRoot = event.tags.some(([n, v]) => n === 't' && v === 'root'); + const isRevision = event.tags.some(([n, v]) => n === 't' && v === 'root-revision'); + const repoTag = event.tags.find(([n]) => n === 'a')?.[1]; + const repoName = repoTag?.split(':')[2] ?? ''; + + return ( +
+
+ +
+
+ {subject || 'Patch'} + {isRoot && root} + {isRevision && revision} +
+ {repoName && ( +

{repoName}

+ )} +
+
+
+ ); +} diff --git a/src/components/PullRequestCard.tsx b/src/components/PullRequestCard.tsx new file mode 100644 index 00000000..0b88e31f --- /dev/null +++ b/src/components/PullRequestCard.tsx @@ -0,0 +1,47 @@ +import { GitPullRequest } from 'lucide-react'; +import { Badge } from '@/components/ui/badge'; +import type { NostrEvent } from '@nostrify/nostrify'; + +interface PullRequestCardProps { + event: NostrEvent; +} + +/** Renders a NIP-34 kind 1618 pull request event as a compact card. */ +export function PullRequestCard({ event }: PullRequestCardProps) { + const subject = event.tags.find(([n]) => n === 'subject')?.[1]; + const branchName = event.tags.find(([n]) => n === 'branch-name')?.[1]; + const repoTag = event.tags.find(([n]) => n === 'a')?.[1]; + const repoName = repoTag?.split(':')[2] ?? ''; + const labels = event.tags.filter(([n]) => n === 't').map(([, v]) => v); + + const title = subject || event.content.split('\n')[0]?.trim() || 'Pull Request'; + + return ( +
+
+ +
+
+ {title} +
+
+ {repoName && {repoName}} + {repoName && branchName && ·} + {branchName && ( + {branchName} + )} +
+
+
+ + {/* Labels */} + {labels.length > 0 && ( +
+ {labels.slice(0, 6).map((label) => ( + {label} + ))} +
+ )} +
+ ); +} diff --git a/src/contexts/AppContext.ts b/src/contexts/AppContext.ts index 3c0f0f38..04f1ffd1 100644 --- a/src/contexts/AppContext.ts +++ b/src/contexts/AppContext.ts @@ -120,6 +120,10 @@ export interface FeedSettings { feedIncludePodcastEpisodes: boolean; /** Include podcast trailers (kind 30055) in the follows/global feed */ feedIncludePodcastTrailers: boolean; + /** Show Development (NIP-34 repos, patches, PRs, custom NIPs, app submissions) link in sidebar */ + showDevelopment: boolean; + /** Include Development content in the follows/global feed */ + feedIncludeDevelopment: boolean; /** Include replies in the follows feed (default: true) */ followsFeedShowReplies: boolean; } diff --git a/src/lib/extraKinds.ts b/src/lib/extraKinds.ts index 8d2fb975..13ac8e58 100644 --- a/src/lib/extraKinds.ts +++ b/src/lib/extraKinds.ts @@ -24,18 +24,19 @@ export interface ExtraKindSite { } /** Section labels for grouping extra kinds in settings UI. */ -export type ExtraKindSection = 'feed' | 'media' | 'social' | 'whimsy'; +export type ExtraKindSection = 'feed' | 'media' | 'social' | 'development' | 'whimsy'; /** Display labels for each section. */ export const SECTION_LABELS: Record = { feed: 'Feed', media: 'Media', social: 'Social', + development: 'Development', whimsy: 'Whimsy', }; /** Ordered list of sections for the "Other Stuff" settings UI. */ -export const SECTION_ORDER: ExtraKindSection[] = ['media', 'social', 'whimsy']; +export const SECTION_ORDER: ExtraKindSection[] = ['media', 'social', 'development', 'whimsy']; /** Metadata for an extra (non-kind-1) content type. */ export interface ExtraKindDef { @@ -415,6 +416,21 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ }, ], }, + // Development + { + kind: 30617, + id: 'development', + showKey: 'showDevelopment', + feedKey: 'feedIncludeDevelopment', + extraFeedKinds: [1617, 1618, 30817, 31733], + label: 'Development', + description: 'Git repos, patches, PRs, NIPs, and apps', + route: 'dev', + addressable: true, + section: 'development', + blurb: 'Nostr-native git repositories, patches, pull requests, custom NIPs, and published applications.', + sites: [{ url: 'https://gitworkshop.dev', name: 'Gitworkshop' }, { url: 'https://nostrhub.io', name: 'NostrHub' }], + }, ]; /** Lookup an ExtraKindDef by its `id` field. */ diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 7fdff747..bb639986 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -171,6 +171,8 @@ export const FeedSettingsSchema = z.looseObject({ showPodcasts: z.boolean().optional(), feedIncludePodcastEpisodes: z.boolean().optional(), feedIncludePodcastTrailers: z.boolean().optional(), + showDevelopment: z.boolean().optional(), + feedIncludeDevelopment: z.boolean().optional(), }); // ─── AppConfigSchema ───────────────────────────────────────────────── diff --git a/src/lib/sidebarItems.tsx b/src/lib/sidebarItems.tsx index 6176010a..c838dea4 100644 --- a/src/lib/sidebarItems.tsx +++ b/src/lib/sidebarItems.tsx @@ -1,8 +1,8 @@ import { Bell, Search, TrendingUp, User, Bookmark, Settings, SwatchBook, Palette, Clapperboard, BarChart3, PartyPopper, BookOpen, BookMarked, Sparkles, Blocks, - MessageSquare, Repeat2, MessageSquareMore, Mic, Smile, Bot, SmilePlus, Camera, Film, Earth, Calendar, - Music, Podcast, + MessageSquare, Repeat2, MessageSquareMore, Mic, Smile, Bot, SmilePlus, Camera, Film, Earth, CalendarDays, + Music, Podcast, Code, } from 'lucide-react'; import { PlanetIcon } from '@/components/icons/PlanetIcon'; import { ChestIcon } from '@/components/icons/ChestIcon'; @@ -55,7 +55,7 @@ export const SIDEBAR_ITEMS: SidebarItemDef[] = [ { id: 'theme', label: 'Vibe', path: '/settings/theme', icon: SwatchBook }, { id: 'ai-chat', label: 'AI Chat', path: '/ai-chat', icon: Bot, requiresAuth: true }, // Content types - { id: 'events', label: 'Events', path: '/events', icon: Calendar }, + { id: 'events', label: 'Events', path: '/events', icon: CalendarDays }, { id: 'photos', label: 'Photos', path: '/photos', icon: Camera }, { id: 'videos', label: 'Videos', path: '/videos', icon: Film }, { id: 'articles', label: 'Articles', path: '/articles', icon: BookOpen }, @@ -72,6 +72,7 @@ export const SIDEBAR_ITEMS: SidebarItemDef[] = [ { id: 'decks', label: 'Magic Decks', path: '/decks', icon: CardsIcon }, { id: 'treasures', label: 'Treasures', path: '/treasures', icon: ChestIcon }, { id: 'emoji-packs', label: 'Emoji Packs', path: '/emoji-packs', icon: SmilePlus }, + { id: 'development', label: 'Development', path: '/dev', icon: Code }, { id: 'world', label: 'World', path: '/world', icon: Earth }, ]; diff --git a/src/test/TestApp.tsx b/src/test/TestApp.tsx index 5e64f71b..904ebad3 100644 --- a/src/test/TestApp.tsx +++ b/src/test/TestApp.tsx @@ -80,6 +80,8 @@ export function TestApp({ children }: TestAppProps) { showPodcasts: false, feedIncludePodcastEpisodes: false, feedIncludePodcastTrailers: false, + showDevelopment: false, + feedIncludeDevelopment: false, followsFeedShowReplies: true, }, sidebarOrder: [], From e6b0ccf9f1a6c6d2c964d5e2f952b35a74d1991a Mon Sep 17 00:00:00 2001 From: Derek Ross Date: Tue, 3 Mar 2026 14:39:12 -0500 Subject: [PATCH 2/3] Redesign development feed cards with NostrHub-style layout and Shakespeare integration - Remove kind 31733 (AppSubmissionCard) from development feed - Redesign GitRepoCard, PatchCard, PullRequestCard, CustomNipCard with labeled sections (Tags, Clone, Kinds, Labels), pill-shaped action buttons - Detect Shakespeare apps (t=shakespeare + web tag) and show favicon, 'Open App' button, and 'Edit with Shakespeare' clone link - Add full detail views: markdown rendering for custom NIPs and PRs, syntax-colored diffs for patches, commit/branch metadata display - Fix development feed to query all dev kinds (30617, 1617, 1618, 30817) - Route /dev -> /development, remove FAB, add per-kind page titles --- src/AppRouter.tsx | 2 +- src/components/AppSubmissionCard.tsx | 74 ----------- src/components/CustomNipCard.tsx | 109 ++++++++++++---- src/components/GitRepoCard.tsx | 152 +++++++++++++++++------ src/components/NoteCard.tsx | 6 +- src/components/PatchCard.tsx | 178 ++++++++++++++++++++++++--- src/components/PullRequestCard.tsx | 147 +++++++++++++++++++--- src/lib/extraKinds.ts | 6 +- src/lib/sidebarItems.tsx | 2 +- src/pages/PostDetailPage.tsx | 27 +++- 10 files changed, 526 insertions(+), 177 deletions(-) delete mode 100644 src/components/AppSubmissionCard.tsx diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index ac11b11c..4e7c72d6 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -104,7 +104,7 @@ export function AppRouter() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/components/AppSubmissionCard.tsx b/src/components/AppSubmissionCard.tsx deleted file mode 100644 index 7a2891c2..00000000 --- a/src/components/AppSubmissionCard.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Rocket, ExternalLink } from 'lucide-react'; -import { Badge } from '@/components/ui/badge'; -import type { NostrEvent } from '@nostrify/nostrify'; - -interface AppSubmissionCardProps { - event: NostrEvent; -} - -/** Renders a Shakespeare kind 31733 app submission as a compact card. */ -export function AppSubmissionCard({ event }: AppSubmissionCardProps) { - const title = event.tags.find(([n]) => n === 'title')?.[1] - ?? event.tags.find(([n]) => n === 'name')?.[1]; - const website = event.tags.find(([n]) => n === 'website')?.[1] - ?? event.tags.find(([n]) => n === 'r')?.[1]; - const icon = event.tags.find(([n]) => n === 'icon')?.[1] - ?? event.tags.find(([n]) => n === 'image')?.[1]; - const hashtags = event.tags.filter(([n]) => n === 't').map(([, v]) => v) - .filter((t) => t !== 'soapbox-app-submission'); - const description = event.content.trim().slice(0, 200); - - const displayTitle = title || 'App Submission'; - - return ( -
- {/* Header */} -
- {icon ? ( - { e.currentTarget.style.display = 'none'; }} - /> - ) : ( -
- -
- )} -
-
- {displayTitle} -
- {description && ( -

{description}{event.content.length > 200 ? '…' : ''}

- )} -
-
- - {/* Tags */} - {hashtags.length > 0 && ( -
- {hashtags.slice(0, 6).map((tag) => ( - {tag} - ))} -
- )} - - {/* Website link */} - {website && ( - e.stopPropagation()} - > - - {new URL(website).hostname} - - )} -
- ); -} diff --git a/src/components/CustomNipCard.tsx b/src/components/CustomNipCard.tsx index c02c5ae6..352b7554 100644 --- a/src/components/CustomNipCard.tsx +++ b/src/components/CustomNipCard.tsx @@ -1,45 +1,112 @@ -import { FileCode } from 'lucide-react'; +import Markdown from 'react-markdown'; +import rehypeSanitize from 'rehype-sanitize'; +import { FileCode, Wand2 } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import type { NostrEvent } from '@nostrify/nostrify'; interface CustomNipCardProps { event: NostrEvent; + /** If true, show a truncated preview instead of the full NIP content. Defaults to true. */ + preview?: boolean; } -/** Renders a NostrHub kind 30817 custom NIP proposal as a compact card. */ -export function CustomNipCard({ event }: CustomNipCardProps) { +/** Extracts the first meaningful paragraph from markdown content. */ +function extractFirstParagraph(content: string, maxLength: number = 200): string { + if (!content) return ''; + + const lines = content.split('\n').map((l) => l.trim()).filter(Boolean); + + for (const line of lines) { + // Skip markdown headers, rules, code fences, lists, blockquotes + if (line.startsWith('#')) continue; + if (line.match(/^[-*_]{3,}$/)) continue; + if (line.startsWith('```')) continue; + if (line.match(/^[-*+]\s/)) continue; + if (line.match(/^\d+\.\s/)) continue; + if (line.startsWith('>')) continue; + + if (line.length > 10) { + const cleaned = line + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/`(.*?)`/g, '$1') + .replace(/\[(.*?)\]\(.*?\)/g, '$1') + .trim(); + + if (cleaned.length > maxLength) { + const truncated = cleaned.slice(0, maxLength); + const lastSpace = truncated.lastIndexOf(' '); + return (lastSpace > maxLength * 0.7 ? truncated.slice(0, lastSpace) : truncated) + '...'; + } + return cleaned; + } + } + + const fallback = content.replace(/\n/g, ' ').trim(); + return fallback.length > maxLength ? fallback.slice(0, maxLength).trim() + '...' : fallback; +} + +/** Renders a NostrHub kind 30817 custom NIP proposal card (NostrHub-style). */ +export function CustomNipCard({ event, preview = true }: CustomNipCardProps) { const title = event.tags.find(([n]) => n === 'title')?.[1]; const dTag = event.tags.find(([n]) => n === 'd')?.[1] ?? ''; const relatedKinds = event.tags.filter(([n]) => n === 'k').map(([, v]) => v); - - // Extract first ~200 chars of content as preview, stripping markdown headings - const contentPreview = event.content - .replace(/^#{1,6}\s+.*/gm, '') // strip headings - .replace(/\n{2,}/g, '\n') // collapse blank lines - .trim() - .slice(0, 200); + const hasShakespeare = event.tags.some(([n, v]) => n === 't' && v === 'shakespeare'); + const contentPreview = preview ? extractFirstParagraph(event.content, 200) : ''; const displayTitle = title || `NIP: ${dTag}`; return ( -
- {/* Header */} +
+ {/* Title */}
- +
- {displayTitle} - {contentPreview && ( -

{contentPreview}{event.content.length > 200 ? '…' : ''}

+ {displayTitle} + {preview && contentPreview && ( +

{contentPreview}

)}
- {/* Related kinds */} + {/* Full markdown content — detail view only */} + {!preview && event.content && ( +
+ + {event.content} + +
+ )} + + {/* Related Kinds section */} {relatedKinds.length > 0 && ( -
- {relatedKinds.slice(0, 8).map((k) => ( - kind:{k} - ))} +
+

Kinds

+
+ {relatedKinds.slice(0, preview ? 6 : relatedKinds.length).map((k) => ( + + Kind {k} + + ))} + {preview && relatedKinds.length > 6 && ( + + +{relatedKinds.length - 6} more + + )} +
+
+ )} + + {/* Action buttons */} + {hasShakespeare && ( +
+
)}
diff --git a/src/components/GitRepoCard.tsx b/src/components/GitRepoCard.tsx index 3a499d47..f4d2da92 100644 --- a/src/components/GitRepoCard.tsx +++ b/src/components/GitRepoCard.tsx @@ -1,77 +1,153 @@ -import { GitBranch, ExternalLink, Globe, Copy } from 'lucide-react'; +import { useState } from 'react'; +import { GitBranch, Globe, Copy, ExternalLink, Wand2 } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; import type { NostrEvent } from '@nostrify/nostrify'; interface GitRepoCardProps { event: NostrEvent; } -/** Renders a NIP-34 kind 30617 repository announcement as a compact card. */ +/** Derive a favicon URL from a website URL. */ +function getFaviconUrl(webUrl: string): string | undefined { + try { + const origin = new URL(webUrl).origin; + return `${origin}/favicon.ico`; + } catch { + return undefined; + } +} + +/** Renders a NIP-34 kind 30617 event. Shakespeare apps show as app cards; others as repo cards. */ export function GitRepoCard({ event }: GitRepoCardProps) { const name = event.tags.find(([n]) => n === 'name')?.[1]; const description = event.tags.find(([n]) => n === 'description')?.[1]; const webUrls = event.tags.filter(([n]) => n === 'web').map(([, v]) => v); const cloneUrls = event.tags.filter(([n]) => n === 'clone').map(([, v]) => v); - const hashtags = event.tags.filter(([n]) => n === 't').map(([, v]) => v).filter((t) => t !== 'personal-fork'); + const hashtags = event.tags.filter(([n]) => n === 't').map(([, v]) => v).filter((t) => t !== 'personal-fork' && t !== 'shakespeare'); const isPersonalFork = event.tags.some(([n, v]) => n === 't' && v === 'personal-fork'); + const hasShakespeare = event.tags.some(([n, v]) => n === 't' && v === 'shakespeare'); const dTag = event.tags.find(([n]) => n === 'd')?.[1] ?? ''; + // Shakespeare + web URL = this is a deployed application, not a repo + const isApp = hasShakespeare && !!webUrls[0]; + const faviconUrl = isApp ? getFaviconUrl(webUrls[0]) : undefined; + const displayName = name || dTag; + const [faviconError, setFaviconError] = useState(false); + + const handleCopy = (url: string) => { + navigator.clipboard.writeText(url); + }; + + const shakespeareUrl = cloneUrls[0] + ? `https://shakespeare.diy/clone?url=${encodeURIComponent(cloneUrls[0])}` + : 'https://shakespeare.diy'; + return ( -
- {/* Header */} -
- +
+ {/* Header: icon/favicon + title */} +
+ {isApp && faviconUrl && !faviconError ? ( + setFaviconError(true)} + /> + ) : ( + + )}
- {displayName} + {displayName} {isPersonalFork && ( - fork + Fork )}
{description && ( -

{description}

+

{description}

)}
- {/* Tags */} + {/* Tags section */} {hashtags.length > 0 && ( -
- {hashtags.slice(0, 6).map((tag) => ( - {tag} - ))} +
+

Tags

+
+ {hashtags.slice(0, 6).map((tag) => ( + + {tag} + + ))} + {hashtags.length > 6 && ( + + +{hashtags.length - 6} more + + )} +
)} - {/* Links */} -
- {webUrls[0] && ( - e.stopPropagation()} - > - - Web - - - )} - {cloneUrls[0] && ( + {/* Clone URL section — hidden for apps */} + {!isApp && cloneUrls[0] && ( +
+

Clone

+
+ + {cloneUrls[0]} + + +
+
+ )} + + {/* Action buttons */} +
+ {hasShakespeare && ( )} + {isApp ? ( + + ) : webUrls[0] ? ( + + ) : !hasShakespeare && cloneUrls[0] ? ( + + ) : null}
); diff --git a/src/components/NoteCard.tsx b/src/components/NoteCard.tsx index 8d7b8bd2..61b53bee 100644 --- a/src/components/NoteCard.tsx +++ b/src/components/NoteCard.tsx @@ -16,7 +16,6 @@ import { GeocacheContent } from '@/components/GeocacheContent'; import { FoundLogContent } from '@/components/FoundLogContent'; import { ColorMomentContent, ColorMomentEyeButton } from '@/components/ColorMomentContent'; import { FollowPackContent } from '@/components/FollowPackContent'; -import { AppSubmissionCard } from '@/components/AppSubmissionCard'; import { ArticleContent } from '@/components/ArticleContent'; import { type ImetaEntry, parseImetaMap } from '@/lib/imeta'; import { CustomNipCard } from '@/components/CustomNipCard'; @@ -203,8 +202,7 @@ export function NoteCard({ event, className, repostedBy, compact, threaded, thre const isPatch = event.kind === 1617; const isPullRequest = event.kind === 1618; const isCustomNip = event.kind === 30817; - const isAppSubmission = event.kind === 31733; - const isDevKind = isGitRepo || isPatch || isPullRequest || isCustomNip || isAppSubmission; + const isDevKind = isGitRepo || isPatch || isPullRequest || isCustomNip; const isTextNote = !isVine && !isPoll && !isGeocache && !isFoundLog && !isColor && !isFollowPack && !isArticle && !isMagicDeck && !isStream && !isFileMetadata && !isTheme && !isVoiceMessage && !isCalendarEvent && !isEmojiPack && !isReaction && !isPhoto && !isVideo && !isAudioKind && !isDevKind; // Kind 1 specific — images now render inline in NoteContent, only videos go to NoteMedia @@ -345,8 +343,6 @@ export function NoteCard({ event, className, repostedBy, compact, threaded, thre ) : isCustomNip ? ( - ) : isAppSubmission ? ( - ) : ( )} diff --git a/src/components/PatchCard.tsx b/src/components/PatchCard.tsx index 855c99cc..fbb1950b 100644 --- a/src/components/PatchCard.tsx +++ b/src/components/PatchCard.tsx @@ -1,39 +1,189 @@ -import { FileText } from 'lucide-react'; +import { FileText, GitCommit, User, Wand2 } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import type { NostrEvent } from '@nostrify/nostrify'; interface PatchCardProps { event: NostrEvent; + /** If true, show a compact preview. If false, show the full patch content. Defaults to true. */ + preview?: boolean; } -/** Renders a NIP-34 kind 1617 patch event as a compact card. */ -export function PatchCard({ event }: PatchCardProps) { - // Subject is typically the first line of the patch content (git format-patch) - const firstLine = event.content.split('\n')[0]?.trim() ?? ''; - const subject = firstLine.startsWith('Subject:') - ? firstLine.replace(/^Subject:\s*(\[PATCH[^\]]*\])?\s*/, '') - : firstLine; +/** Parse the git format-patch content into structured parts. */ +function parsePatchContent(content: string) { + const lines = content.split('\n'); + let subject = ''; + let commitMessage = ''; + let diff = ''; + + // Extract subject from first line or Subject: header + const firstLine = lines[0]?.trim() ?? ''; + if (firstLine.startsWith('Subject:')) { + subject = firstLine.replace(/^Subject:\s*(\[PATCH[^\]]*\])?\s*/, ''); + } else { + subject = firstLine; + } + + // Find the diff start (lines starting with "---" followed by diff content, or "diff --git") + let diffStartIdx = -1; + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('diff --git ')) { + diffStartIdx = i; + break; + } + } + + if (diffStartIdx > 0) { + // Everything between subject and diff is the commit message + // Skip blank lines and email-style headers + const messageLines: string[] = []; + for (let i = 1; i < diffStartIdx; i++) { + const line = lines[i]; + // Skip email-style headers (From:, Date:, Subject:, etc.) + if (/^[A-Z][a-z-]+:/.test(line) && messageLines.length === 0) continue; + // Skip the "---" separator before diff stats + if (line === '---') continue; + // Skip diff stat lines (e.g. " file.ts | 5 ++---") + if (/^\s+\S+.*\|.*\d+/.test(line)) continue; + // Skip the summary line (e.g. "2 files changed, 10 insertions(+)") + if (/^\s*\d+ files? changed/.test(line)) continue; + messageLines.push(line); + } + commitMessage = messageLines.join('\n').trim(); + diff = lines.slice(diffStartIdx).join('\n'); + } else { + // No diff found — treat everything after the first line as the message + commitMessage = lines.slice(1).join('\n').trim(); + } + + return { subject, commitMessage, diff }; +} + +/** Renders a NIP-34 kind 1617 patch event card. */ +export function PatchCard({ event, preview = true }: PatchCardProps) { + const { subject, commitMessage, diff } = parsePatchContent(event.content); const isRoot = event.tags.some(([n, v]) => n === 't' && v === 'root'); const isRevision = event.tags.some(([n, v]) => n === 't' && v === 'root-revision'); + const hasShakespeare = event.tags.some(([n, v]) => n === 't' && v === 'shakespeare'); const repoTag = event.tags.find(([n]) => n === 'a')?.[1]; const repoName = repoTag?.split(':')[2] ?? ''; + const commitId = event.tags.find(([n]) => n === 'commit')?.[1]; + const parentCommit = event.tags.find(([n]) => n === 'parent-commit')?.[1]; + const committerTag = event.tags.find(([n]) => n === 'committer'); + const hashtags = event.tags + .filter(([n]) => n === 't') + .map(([, v]) => v) + .filter((t) => t !== 'root' && t !== 'root-revision' && t !== 'shakespeare'); return ( -
+
+ {/* Title + status badges */}
- +
- {subject || 'Patch'} - {isRoot && root} - {isRevision && revision} + {subject || 'Patch'} + {isRoot && ( + root + )} + {isRevision && ( + revision + )}
{repoName && ( -

{repoName}

+

{repoName}

)}
+ + {/* Commit metadata — detail view only */} + {!preview && (commitId || parentCommit || committerTag) && ( +
+ {commitId && ( +
+ + Commit + {commitId.slice(0, 12)} +
+ )} + {parentCommit && ( +
+ + Parent + {parentCommit.slice(0, 12)} +
+ )} + {committerTag && ( +
+ + Committer + {committerTag[1]} + {committerTag[2] && <{committerTag[2]}>} +
+ )} +
+ )} + + {/* Commit message — detail view only */} + {!preview && commitMessage && ( +
+

Message

+
+ {commitMessage} +
+
+ )} + + {/* Diff — detail view only */} + {!preview && diff && ( +
+

Diff

+
+
+              {diff.split('\n').map((line, i) => {
+                let lineClass = 'text-foreground';
+                if (line.startsWith('+') && !line.startsWith('+++')) lineClass = 'text-green-600 dark:text-green-400';
+                else if (line.startsWith('-') && !line.startsWith('---')) lineClass = 'text-red-600 dark:text-red-400';
+                else if (line.startsWith('@@')) lineClass = 'text-blue-600 dark:text-blue-400';
+                else if (line.startsWith('diff --git')) lineClass = 'text-muted-foreground font-semibold';
+                return 
{line}
; + })} +
+
+
+ )} + + {/* Tags section */} + {hashtags.length > 0 && ( +
+

Tags

+
+ {hashtags.slice(0, preview ? 6 : hashtags.length).map((tag) => ( + + {tag} + + ))} + {preview && hashtags.length > 6 && ( + + +{hashtags.length - 6} more + + )} +
+
+ )} + + {/* Action buttons */} + {hasShakespeare && ( +
+ +
+ )}
); } diff --git a/src/components/PullRequestCard.tsx b/src/components/PullRequestCard.tsx index 0b88e31f..6228b0f0 100644 --- a/src/components/PullRequestCard.tsx +++ b/src/components/PullRequestCard.tsx @@ -1,47 +1,158 @@ -import { GitPullRequest } from 'lucide-react'; +import Markdown from 'react-markdown'; +import rehypeSanitize from 'rehype-sanitize'; +import { GitPullRequest, GitCommit, GitBranch, Copy, Wand2 } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; import type { NostrEvent } from '@nostrify/nostrify'; interface PullRequestCardProps { event: NostrEvent; + /** If true, show a compact preview. If false, show the full PR content. Defaults to true. */ + preview?: boolean; } -/** Renders a NIP-34 kind 1618 pull request event as a compact card. */ -export function PullRequestCard({ event }: PullRequestCardProps) { +/** Renders a NIP-34 kind 1618 pull request event card. */ +export function PullRequestCard({ event, preview = true }: PullRequestCardProps) { const subject = event.tags.find(([n]) => n === 'subject')?.[1]; const branchName = event.tags.find(([n]) => n === 'branch-name')?.[1]; const repoTag = event.tags.find(([n]) => n === 'a')?.[1]; const repoName = repoTag?.split(':')[2] ?? ''; - const labels = event.tags.filter(([n]) => n === 't').map(([, v]) => v); + const cloneUrls = event.tags.filter(([n]) => n === 'clone').map(([, v]) => v); + const hasShakespeare = event.tags.some(([n, v]) => n === 't' && v === 'shakespeare'); + const commitTip = event.tags.find(([n]) => n === 'c')?.[1]; + const mergeBase = event.tags.find(([n]) => n === 'merge-base')?.[1]; + const labels = event.tags + .filter(([n]) => n === 't') + .map(([, v]) => v) + .filter((t) => t !== 'shakespeare'); const title = subject || event.content.split('\n')[0]?.trim() || 'Pull Request'; + const hasDescription = event.content.trim().length > 0; + + const handleCopy = (url: string) => { + navigator.clipboard.writeText(url); + }; + + const shakespeareUrl = cloneUrls[0] + ? `https://shakespeare.diy/clone?url=${encodeURIComponent(cloneUrls[0])}` + : 'https://shakespeare.diy'; return ( -
+
+ {/* Title + branch info */}
- +
-
- {title} -
-
+ {title} +
{repoName && {repoName}} - {repoName && branchName && ·} + {repoName && branchName && /} {branchName && ( - {branchName} + {branchName} )}
- {/* Labels */} - {labels.length > 0 && ( -
- {labels.slice(0, 6).map((label) => ( - {label} - ))} + {/* Branch & commit metadata — detail view only */} + {!preview && (commitTip || mergeBase || branchName) && ( +
+ {branchName && ( +
+ + Branch + {branchName} +
+ )} + {commitTip && ( +
+ + Tip + {commitTip.slice(0, 12)} +
+ )} + {mergeBase && ( +
+ + Base + {mergeBase.slice(0, 12)} +
+ )}
)} + + {/* PR description — detail view only, rendered as markdown */} + {!preview && hasDescription && ( +
+

Description

+
+ + {event.content} + +
+
+ )} + + {/* Labels section */} + {labels.length > 0 && ( +
+

Labels

+
+ {labels.slice(0, preview ? 6 : labels.length).map((label) => ( + + {label} + + ))} + {preview && labels.length > 6 && ( + + +{labels.length - 6} more + + )} +
+
+ )} + + {/* Clone URL section */} + {cloneUrls[0] && ( +
+

Clone

+
+ + {cloneUrls[0]} + + +
+
+ )} + + {/* Action buttons */} +
+ {hasShakespeare && ( + + )} + {!hasShakespeare && cloneUrls[0] && ( + + )} +
); } diff --git a/src/lib/extraKinds.ts b/src/lib/extraKinds.ts index 13ac8e58..61987b1f 100644 --- a/src/lib/extraKinds.ts +++ b/src/lib/extraKinds.ts @@ -422,10 +422,10 @@ export const EXTRA_KINDS: ExtraKindDef[] = [ id: 'development', showKey: 'showDevelopment', feedKey: 'feedIncludeDevelopment', - extraFeedKinds: [1617, 1618, 30817, 31733], + extraFeedKinds: [1617, 1618, 30817], label: 'Development', - description: 'Git repos, patches, PRs, NIPs, and apps', - route: 'dev', + description: 'Git repos, patches, PRs, and custom NIPs', + route: 'development', addressable: true, section: 'development', blurb: 'Nostr-native git repositories, patches, pull requests, custom NIPs, and published applications.', diff --git a/src/lib/sidebarItems.tsx b/src/lib/sidebarItems.tsx index c838dea4..27fc1fca 100644 --- a/src/lib/sidebarItems.tsx +++ b/src/lib/sidebarItems.tsx @@ -72,7 +72,7 @@ export const SIDEBAR_ITEMS: SidebarItemDef[] = [ { id: 'decks', label: 'Magic Decks', path: '/decks', icon: CardsIcon }, { id: 'treasures', label: 'Treasures', path: '/treasures', icon: ChestIcon }, { id: 'emoji-packs', label: 'Emoji Packs', path: '/emoji-packs', icon: SmilePlus }, - { id: 'development', label: 'Development', path: '/dev', icon: Code }, + { id: 'development', label: 'Development', path: '/development', icon: Code }, { id: 'world', label: 'World', path: '/world', icon: Earth }, ]; diff --git a/src/pages/PostDetailPage.tsx b/src/pages/PostDetailPage.tsx index 63986845..e3f39b93 100644 --- a/src/pages/PostDetailPage.tsx +++ b/src/pages/PostDetailPage.tsx @@ -39,6 +39,10 @@ import { LiveStreamPage } from '@/components/LiveStreamPage'; import { MusicDetailContent } from '@/components/MusicDetailContent'; import { PodcastDetailContent } from '@/components/PodcastDetailContent'; import { WebxdcEmbed } from '@/components/WebxdcEmbed'; +import { GitRepoCard } from '@/components/GitRepoCard'; +import { PatchCard } from '@/components/PatchCard'; +import { PullRequestCard } from '@/components/PullRequestCard'; +import { CustomNipCard } from '@/components/CustomNipCard'; import { AudioVisualizer } from '@/components/AudioVisualizer'; import { extractAudioUrls } from '@/lib/mediaUrls'; import { type ImetaEntry, parseImetaMap } from '@/lib/imeta'; @@ -57,6 +61,8 @@ const MUSIC_KINDS = new Set([36787, 34139]); const PODCAST_KINDS = new Set([30054, 30055]); /** NIP-52 Calendar Events. */ const CALENDAR_EVENT_KINDS = new Set([31922, 31923]); +/** NIP-34 development kinds. */ +const DEV_KINDS = new Set([30617, 1617, 1618, 30817]); /** Map a kind number to a human-readable shell title for the loading state. */ function shellTitleForKind(kind?: number): string { @@ -66,6 +72,10 @@ function shellTitleForKind(kind?: number): string { if (CALENDAR_EVENT_KINDS.has(kind)) return 'Event Details'; if (FOLLOW_PACK_KINDS.has(kind)) return 'Follow Pack'; if (kind === LIVE_STREAM_KIND) return 'Live Stream'; + if (kind === 30617) return 'Repository'; + if (kind === 1617) return 'Patch'; + if (kind === 1618) return 'Pull Request'; + if (kind === 30817) return 'Custom NIP'; return 'Post Details'; } @@ -272,7 +282,7 @@ export function AddrPostDetailPage({ addr, relays }: AddrPostDetailPageProps) { } return ( - + @@ -639,7 +649,12 @@ function PostDetailContent({ event }: { event: NostrEvent }) { const isReaction = event.kind === 7; const isVideo = event.kind === 21 || event.kind === 22; const isCommunity = event.kind === 34550; - const isTextNote = !isVine && !isPoll && !isGeocache && !isFoundLog && !isColor && !isFollowPack && !isEmojiPack && !isArticle && !isMagicDeck && !isFileMetadata && !isTheme && !isVoiceMessage && !isReaction && !isVideo && !isCommunity; + const isGitRepo = event.kind === 30617; + const isPatch = event.kind === 1617; + const isPullRequest = event.kind === 1618; + const isCustomNip = event.kind === 30817; + const isDevKind = isGitRepo || isPatch || isPullRequest || isCustomNip; + const isTextNote = !isVine && !isPoll && !isGeocache && !isFoundLog && !isColor && !isFollowPack && !isEmojiPack && !isArticle && !isMagicDeck && !isFileMetadata && !isTheme && !isVoiceMessage && !isReaction && !isVideo && !isCommunity && !isDevKind; const videos = useMemo(() => isTextNote ? extractVideos(event.content) : [], [event.content, isTextNote]); const imetaMap = useMemo(() => isTextNote ? parseImetaMap(event.tags) : new Map(), [event.tags, isTextNote]); @@ -1071,6 +1086,14 @@ function PostDetailContent({ event }: { event: NostrEvent }) { ) : isCommunity ? ( + ) : isGitRepo ? ( +
+ ) : isPatch ? ( +
+ ) : isPullRequest ? ( +
+ ) : isCustomNip ? ( +
) : isVine || isPoll || isGeocache || isFoundLog || isColor || isFollowPack || isEmojiPack ? ( <> {isVine && } From 52e20939c90cc05825b3b40e213aaf5b0424a186 Mon Sep 17 00:00:00 2001 From: Derek Ross Date: Tue, 3 Mar 2026 16:10:06 -0500 Subject: [PATCH 3/3] Fix lint errors: remove unused WebxdcEmbed import and DEV_KINDS constant --- src/components/NoteCard.tsx | 1 - src/pages/PostDetailPage.tsx | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/components/NoteCard.tsx b/src/components/NoteCard.tsx index 61b53bee..bb9c1db7 100644 --- a/src/components/NoteCard.tsx +++ b/src/components/NoteCard.tsx @@ -22,7 +22,6 @@ import { CustomNipCard } from '@/components/CustomNipCard'; import { GitRepoCard } from '@/components/GitRepoCard'; import { PatchCard } from '@/components/PatchCard'; import { PullRequestCard } from '@/components/PullRequestCard'; -import { WebxdcEmbed } from '@/components/WebxdcEmbed'; import { MagicDeckContent } from '@/components/MagicDeckContent'; import { EmojiPackContent } from '@/components/EmojiPackContent'; import { FileMetadataContent } from '@/components/FileMetadataContent'; diff --git a/src/pages/PostDetailPage.tsx b/src/pages/PostDetailPage.tsx index e3f39b93..c17b3dae 100644 --- a/src/pages/PostDetailPage.tsx +++ b/src/pages/PostDetailPage.tsx @@ -61,9 +61,6 @@ const MUSIC_KINDS = new Set([36787, 34139]); const PODCAST_KINDS = new Set([30054, 30055]); /** NIP-52 Calendar Events. */ const CALENDAR_EVENT_KINDS = new Set([31922, 31923]); -/** NIP-34 development kinds. */ -const DEV_KINDS = new Set([30617, 1617, 1618, 30817]); - /** Map a kind number to a human-readable shell title for the loading state. */ function shellTitleForKind(kind?: number): string { if (!kind) return 'Loading...';