Browse Source

handle repo announcements, releases, issues

create new release notes
imwald
Silberengel 1 month ago
parent
commit
8be40c234e
  1. 9
      src/components/ContentPreview/index.tsx
  2. 5
      src/components/KindFilter/index.tsx
  3. 164
      src/components/Note/GitRepublicEventCard.tsx
  4. 7
      src/components/Note/index.tsx
  5. 4
      src/components/NoteList/index.tsx
  6. 124
      src/components/PostEditor/PostContent.tsx
  7. 1
      src/components/Profile/ProfileFeedWithPins.tsx
  8. 26
      src/constants.ts
  9. 11
      src/i18n/locales/de.ts
  10. 11
      src/i18n/locales/en.ts
  11. 41
      src/lib/draft-event.ts
  12. 56
      src/lib/git-republic-event.ts
  13. 6
      src/lib/kind-description.ts
  14. 7
      src/services/local-storage.service.ts

9
src/components/ContentPreview/index.tsx

@ -31,6 +31,7 @@ import ApplicationHandlerRecommendation from '../ApplicationHandlerRecommendatio @@ -31,6 +31,7 @@ import ApplicationHandlerRecommendation from '../ApplicationHandlerRecommendatio
import FollowPackPreview from './FollowPackPreview'
import ReactionEmojiDisplay from '../Note/ReactionEmojiDisplay'
import NoteKindLabel from '../Note/NoteKindLabel'
import GitRepublicEventCard from '../Note/GitRepublicEventCard'
/** Inert event so hooks can run before `event` is defined. */
const CONTENT_PREVIEW_HOOK_PLACEHOLDER = {
@ -183,6 +184,14 @@ export default function ContentPreview({ @@ -183,6 +184,14 @@ export default function ContentPreview({
return withKindRow(<FollowPackPreview event={event} />)
}
if (
event.kind === ExtendedKind.GIT_REPO_ANNOUNCEMENT ||
event.kind === ExtendedKind.GIT_ISSUE ||
event.kind === ExtendedKind.GIT_RELEASE
) {
return withKindRow(<GitRepublicEventCard variant="compact" event={event} />)
}
if (isNip25ReactionKind(event.kind)) {
return withKindRow(
<div className="pointer-events-none flex items-center gap-1.5 text-sm text-muted-foreground">

5
src/components/KindFilter/index.tsx

@ -28,7 +28,10 @@ const KIND_FILTER_OPTIONS = [ @@ -28,7 +28,10 @@ const KIND_FILTER_OPTIONS = [
{ kindGroup: [ExtendedKind.DISCUSSION], label: 'Discussions' },
{ kindGroup: [ExtendedKind.CALENDAR_EVENT_DATE, ExtendedKind.CALENDAR_EVENT_TIME], label: 'Calendar Events' },
{ kindGroup: [ExtendedKind.ZAP_RECEIPT], label: 'Zaps' },
{ kindGroup: [kinds.Repost, ExtendedKind.GENERIC_REPOST], label: 'Boosts' }
{ kindGroup: [kinds.Repost, ExtendedKind.GENERIC_REPOST], label: 'Boosts' },
{ kindGroup: [ExtendedKind.GIT_REPO_ANNOUNCEMENT], label: 'Git repositories' },
{ kindGroup: [ExtendedKind.GIT_ISSUE], label: 'Git issues' },
{ kindGroup: [ExtendedKind.GIT_RELEASE], label: 'Git releases' }
]
function buildShowKindsFromOptions(

164
src/components/Note/GitRepublicEventCard.tsx

@ -0,0 +1,164 @@ @@ -0,0 +1,164 @@
import { ExtendedKind } from '@/constants'
import {
getGitRepublicRepoContext,
gitRepublicRepoWebUrl,
type GitRepublicRepoContext
} from '@/lib/git-republic-event'
import { cn } from '@/lib/utils'
import { Event, nip19 } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { ExternalLink, GitBranch, CircleDot, Tag } from 'lucide-react'
import MarkdownArticle from './MarkdownArticle/MarkdownArticle'
function repoHeadline(ctx: GitRepublicRepoContext): string {
const name = ctx.displayName || ctx.repoId
try {
const npub = nip19.npubEncode(ctx.ownerHex)
const short = `${npub.slice(0, 14)}`
return `${short} / ${name}`
} catch {
return name
}
}
export default function GitRepublicEventCard({
event,
className,
variant = 'full'
}: {
event: Event
className?: string
variant?: 'full' | 'compact'
}) {
const { t } = useTranslation()
const ctx = useMemo(() => getGitRepublicRepoContext(event), [event])
const webUrl = useMemo(() => (ctx ? gitRepublicRepoWebUrl(ctx) : null), [ctx])
const subject = event.tags.find((t) => t[0] === 'subject')?.[1]
const titleTag = event.tags.find((t) => t[0] === 'title')?.[1]
const tagName = event.tags.find((t) => t[0] === 'tag')?.[1]
const description =
event.kind === ExtendedKind.GIT_REPO_ANNOUNCEMENT
? event.tags.find((t) => t[0] === 'description')?.[1]
: undefined
const isDraft = event.tags.some((t) => t[0] === 'draft' && t[1] === 'true')
const isPrerelease = event.tags.some((t) => t[0] === 'prerelease' && t[1] === 'true')
const { Icon, badge, headline } = useMemo(() => {
if (event.kind === ExtendedKind.GIT_REPO_ANNOUNCEMENT) {
const name = event.tags.find((t) => t[0] === 'name')?.[1] || ctx?.repoId || t('Git Republic repository')
return {
Icon: GitBranch,
badge: t('Git Republic repository'),
headline: name
}
}
if (event.kind === ExtendedKind.GIT_ISSUE) {
return {
Icon: CircleDot,
badge: t('Git Republic issue'),
headline: subject || t('Git Republic issue')
}
}
if (event.kind === ExtendedKind.GIT_RELEASE) {
const h = titleTag || tagName || t('Git Republic release')
return { Icon: Tag, badge: t('Git Republic release'), headline: h }
}
return { Icon: GitBranch, badge: t('Git Republic'), headline: t('Git Republic event') }
}, [event, ctx?.repoId, subject, tagName, titleTag, t])
const body =
event.kind === ExtendedKind.GIT_REPO_ANNOUNCEMENT
? description || event.content
: event.content
const compact = variant === 'compact'
return (
<div
className={cn(
'rounded-xl border border-violet-500/25 bg-gradient-to-br from-violet-500/[0.08] via-background to-sky-500/[0.06] shadow-sm',
compact ? 'p-3' : 'p-4',
className
)}
>
<div className="flex items-start gap-3">
<div
className={cn(
'flex shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary',
compact ? 'size-9' : 'size-10'
)}
aria-hidden
>
<Icon className={compact ? 'size-4' : 'size-5'} />
</div>
<div className="min-w-0 flex-1 space-y-1">
<div className={cn('flex flex-wrap items-center gap-2', compact && 'sr-only')}>
<span className="text-[0.65rem] font-semibold uppercase tracking-wider text-muted-foreground">
{badge}
</span>
{isDraft ? (
<span className="rounded-md bg-amber-500/15 px-1.5 py-0.5 text-[0.65rem] font-medium text-amber-700 dark:text-amber-400">
{t('Draft')}
</span>
) : null}
{isPrerelease ? (
<span className="rounded-md bg-sky-500/15 px-1.5 py-0.5 text-[0.65rem] font-medium text-sky-700 dark:text-sky-400">
{t('Pre-release')}
</span>
) : null}
</div>
{compact && (isDraft || isPrerelease) ? (
<div className="flex flex-wrap gap-1">
{isDraft ? (
<span className="rounded bg-amber-500/15 px-1 py-0.5 text-[0.6rem] font-medium text-amber-700 dark:text-amber-400">
{t('Draft')}
</span>
) : null}
{isPrerelease ? (
<span className="rounded bg-sky-500/15 px-1 py-0.5 text-[0.6rem] font-medium text-sky-700 dark:text-sky-400">
{t('Pre-release')}
</span>
) : null}
</div>
) : null}
<h3
className={cn(
'font-semibold leading-snug text-foreground break-words',
compact ? 'text-sm' : 'text-base'
)}
>
{headline}
</h3>
{event.kind === ExtendedKind.GIT_RELEASE && tagName ? (
<p className="font-mono text-xs text-muted-foreground">{tagName}</p>
) : null}
{ctx ? (
<p className="truncate text-xs text-muted-foreground" title={repoHeadline(ctx)}>
{repoHeadline(ctx)}
</p>
) : null}
{webUrl ? (
<a
href={webUrl}
target="_blank"
rel="noreferrer noopener"
className="inline-flex max-w-full items-center gap-1 text-xs font-medium text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
<ExternalLink className="size-3 shrink-0" aria-hidden />
<span className="truncate">{t('Open in Git Republic')}</span>
</a>
) : null}
</div>
</div>
{body.trim() ? (
<div className={cn(compact ? 'mt-2 line-clamp-4' : 'mt-3', 'min-w-0 text-sm')}>
<MarkdownArticle event={{ ...event, content: body }} hideMetadata className="prose-sm" />
</div>
) : null}
</div>
)
}

7
src/components/Note/index.tsx

@ -69,6 +69,7 @@ import Zap from './Zap' @@ -69,6 +69,7 @@ import Zap from './Zap'
import CitationCard from '@/components/CitationCard'
import FollowPackPreview from '../ContentPreview/FollowPackPreview'
import CalendarEventContent from '../CalendarEventContent'
import GitRepublicEventCard from './GitRepublicEventCard'
export default function Note({
event,
@ -284,6 +285,12 @@ export default function Note({ @@ -284,6 +285,12 @@ export default function Note({
content = <Zap className="mt-2" event={event} />
} else if (event.kind === ExtendedKind.FOLLOW_PACK) {
content = <FollowPackPreview className="mt-2" event={event} />
} else if (
event.kind === ExtendedKind.GIT_REPO_ANNOUNCEMENT ||
event.kind === ExtendedKind.GIT_ISSUE ||
event.kind === ExtendedKind.GIT_RELEASE
) {
content = <GitRepublicEventCard className="mt-2" event={event} />
} else if (event.kind === kinds.ShortTextNote || event.kind === ExtendedKind.COMMENT) {
// Plain text notes use MarkdownArticle for proper markdown rendering
content = <MarkdownArticle className="mt-2" event={event} hideMetadata={true} />

4
src/components/NoteList/index.tsx

@ -495,6 +495,8 @@ const NoteList = forwardRef( @@ -495,6 +495,8 @@ const NoteList = forwardRef(
}
// Kind 1111 (comments): show only if showKind1111
if (evt.kind === ExtendedKind.COMMENT && !showKind1111) return false
// Git Republic releases: same visibility as kind-1 OPs
if (evt.kind === ExtendedKind.GIT_RELEASE && !showKind1OPs) return false
}
if (shouldHideEvent(evt)) return false
@ -568,6 +570,7 @@ const NoteList = forwardRef( @@ -568,6 +570,7 @@ const NoteList = forwardRef(
if (!isReply && !showKind1OPs) return false
}
if (event.kind === ExtendedKind.COMMENT && !showKind1111) return false
if (event.kind === ExtendedKind.GIT_RELEASE && !showKind1OPs) return false
}
if (shouldHideEvent(event)) return false
@ -1136,6 +1139,7 @@ const NoteList = forwardRef( @@ -1136,6 +1139,7 @@ const NoteList = forwardRef(
if (!isReply && !showKind1OPs) return
}
if (event.kind === ExtendedKind.COMMENT && !showKind1111) return
if (event.kind === ExtendedKind.GIT_RELEASE && !showKind1OPs) return
}
if (shouldHideEventRef.current(event)) return
if (pubkey && event.pubkey === pubkey) {

124
src/components/PostEditor/PostContent.tsx

@ -4,6 +4,7 @@ import { ScrollArea } from '@/components/ui/scroll-area' @@ -4,6 +4,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox'
import { Skeleton } from '@/components/ui/skeleton'
import {
DropdownMenu,
@ -30,10 +31,12 @@ import { @@ -30,10 +31,12 @@ import {
createCitationInternalDraftEvent,
createCitationExternalDraftEvent,
createCitationHardcopyDraftEvent,
createCitationPromptDraftEvent
createCitationPromptDraftEvent,
createGitReleaseDraftEvent
} from '@/lib/draft-event'
import { ExtendedKind } from '@/constants'
import { isTouchDevice } from '@/lib/utils'
import { parseRepoOwnerPubkeyInput } from '@/lib/git-republic-event'
import { cn, isTouchDevice } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import { useFeed } from '@/providers/FeedProvider'
import { useReply } from '@/providers/ReplyProvider'
@ -240,7 +243,16 @@ export default function PostContent({ @@ -240,7 +243,16 @@ export default function PostContent({
const [citationGeohash, setCitationGeohash] = useState('')
const [citationVersion, setCitationVersion] = useState('')
const [citationSummary, setCitationSummary] = useState('')
const [isGitRelease, setIsGitRelease] = useState(false)
const [releaseRepoOwnerInput, setReleaseRepoOwnerInput] = useState('')
const [releaseRepoId, setReleaseRepoId] = useState('')
const [releaseTagName, setReleaseTagName] = useState('')
const [releaseTagHash, setReleaseTagHash] = useState('')
const [releaseTitle, setReleaseTitle] = useState('')
const [releaseDownloadUrl, setReleaseDownloadUrl] = useState('')
const [releaseDraft, setReleaseDraft] = useState(false)
const [releasePrerelease, setReleasePrerelease] = useState(false)
const [hasPrivateRelaysAvailable, setHasPrivateRelaysAvailable] = useState(false)
const [showMediaKindDialog, setShowMediaKindDialog] = useState(false)
const [pendingMediaUpload, setPendingMediaUpload] = useState<{ url: string; tags: string[][]; file: File } | null>(null)
@ -253,20 +265,32 @@ export default function PostContent({ @@ -253,20 +265,32 @@ export default function PostContent({
}
}, [mediaNoteKind, mediaImetaTags])
const isFirstRender = useRef(true)
const releaseFieldsOk = useMemo(() => {
if (!isGitRelease) return true
const owner = parseRepoOwnerPubkeyInput(releaseRepoOwnerInput)
return (
!!owner &&
!!releaseRepoId.trim() &&
!!releaseTagName.trim() &&
/^[0-9a-f]{40}$/i.test(releaseTagHash.trim())
)
}, [isGitRelease, releaseRepoOwnerInput, releaseRepoId, releaseTagName, releaseTagHash])
const canPost = useMemo(() => {
const isArticle = isLongFormArticle || isWikiArticle || isWikiArticleMarkdown || isPublicationContent
const result = (
!!pubkey &&
!posting &&
!uploadProgresses.length &&
// For media notes, text is optional - just need media
((mediaNoteKind !== null && mediaUrl) || !!text) &&
// For media notes, text is optional - just need media; Git releases use the editor as release notes (optional)
((mediaNoteKind !== null && mediaUrl) || !!text || isGitRelease) &&
(!isPoll || pollCreateData.options.filter((option) => !!option.trim()).length >= 2) &&
(!isPublicMessage || extractedMentions.length > 0 || parentEvent?.kind === ExtendedKind.PUBLIC_MESSAGE) &&
(!isProtectedEvent || additionalRelayUrls.length > 0) &&
(!isHighlight || highlightData.sourceValue.trim() !== '') &&
// For articles, dTag is mandatory
(!isArticle || !!articleDTag.trim()) &&
(!isGitRelease || releaseFieldsOk) &&
// For citations, required fields must be filled
(!isCitationInternal || !!citationInternalCTag.trim()) &&
(!isCitationExternal || (!!citationExternalUrl.trim() && !!citationAccessedOn.trim())) &&
@ -296,6 +320,8 @@ export default function PostContent({ @@ -296,6 +320,8 @@ export default function PostContent({
isWikiArticleMarkdown,
isPublicationContent,
articleDTag,
isGitRelease,
releaseFieldsOk,
isCitationInternal,
citationInternalCTag,
isCitationExternal,
@ -387,6 +413,8 @@ export default function PostContent({ @@ -387,6 +413,8 @@ export default function PostContent({
return ExtendedKind.WIKI_ARTICLE_MARKDOWN
} else if (isPublicationContent) {
return ExtendedKind.PUBLICATION_CONTENT
} else if (isGitRelease) {
return ExtendedKind.GIT_RELEASE
} else if (isCitationInternal) {
return ExtendedKind.CITATION_INTERNAL
} else if (isCitationExternal) {
@ -411,6 +439,7 @@ export default function PostContent({ @@ -411,6 +439,7 @@ export default function PostContent({
isWikiArticle,
isWikiArticleMarkdown,
isPublicationContent,
isGitRelease,
isCitationInternal,
isCitationExternal,
isCitationHardcopy,
@ -646,6 +675,23 @@ export default function PostContent({ @@ -646,6 +675,23 @@ export default function PostContent({
})
}
if (isGitRelease) {
const ownerHex = parseRepoOwnerPubkeyInput(releaseRepoOwnerInput)
if (!ownerHex) {
throw new Error(t('Invalid repository owner pubkey'))
}
return createGitReleaseDraftEvent(cleanedText, {
repoOwnerPubkey: ownerHex,
repoId: releaseRepoId.trim(),
tagName: releaseTagName.trim(),
tagHash: releaseTagHash.trim().toLowerCase(),
title: releaseTitle.trim() || undefined,
downloadUrl: releaseDownloadUrl.trim() || undefined,
isDraft: releaseDraft,
isPrerelease: releasePrerelease
})
}
// Citations
if (isCitationInternal) {
return createCitationInternalDraftEvent(cleanedText, {
@ -781,6 +827,15 @@ export default function PostContent({ @@ -781,6 +827,15 @@ export default function PostContent({
isWikiArticle,
isWikiArticleMarkdown,
isPublicationContent,
isGitRelease,
releaseRepoOwnerInput,
releaseRepoId,
releaseTagName,
releaseTagHash,
releaseTitle,
releaseDownloadUrl,
releaseDraft,
releasePrerelease,
isCitationInternal,
isCitationExternal,
isCitationHardcopy,
@ -798,7 +853,8 @@ export default function PostContent({ @@ -798,7 +853,8 @@ export default function PostContent({
articleImage,
articleSubject,
articleSummary,
pubkey
pubkey,
t
])
// Function to generate draft event JSON for preview
@ -808,7 +864,10 @@ export default function PostContent({ @@ -808,7 +864,10 @@ export default function PostContent({
if (isArticle && !articleDTag.trim()) {
throw new Error(t('D-Tag is required for articles'))
}
if (isGitRelease && !releaseFieldsOk) {
throw new Error(t('Fill repository release fields'))
}
if (!pubkey) {
return JSON.stringify({ error: 'Not logged in' }, null, 2)
}
@ -830,6 +889,8 @@ export default function PostContent({ @@ -830,6 +889,8 @@ export default function PostContent({
isWikiArticleMarkdown,
isPublicationContent,
articleDTag,
isGitRelease,
releaseFieldsOk,
createDraftEvent,
t
])
@ -998,6 +1059,11 @@ export default function PostContent({ @@ -998,6 +1059,11 @@ export default function PostContent({
// When enabling poll mode, clear other modes
setIsPublicMessage(false)
setIsHighlight(false)
setIsGitRelease(false)
setIsCitationInternal(false)
setIsCitationExternal(false)
setIsCitationHardcopy(false)
setIsCitationPrompt(false)
}
}
@ -1009,6 +1075,11 @@ export default function PostContent({ @@ -1009,6 +1075,11 @@ export default function PostContent({
// When enabling public message mode, clear other modes
setIsPoll(false)
setIsHighlight(false)
setIsGitRelease(false)
setIsCitationInternal(false)
setIsCitationExternal(false)
setIsCitationHardcopy(false)
setIsCitationPrompt(false)
}
}
@ -1020,6 +1091,11 @@ export default function PostContent({ @@ -1020,6 +1091,11 @@ export default function PostContent({
// When enabling highlight mode, clear other modes and set client tag to true
setIsPoll(false)
setIsPublicMessage(false)
setIsGitRelease(false)
setIsCitationInternal(false)
setIsCitationExternal(false)
setIsCitationHardcopy(false)
setIsCitationPrompt(false)
setAddClientTag(true)
}
}
@ -1407,7 +1483,8 @@ export default function PostContent({ @@ -1407,7 +1483,8 @@ export default function PostContent({
setIsCitationExternal(false)
setIsCitationHardcopy(false)
setIsCitationPrompt(false)
setIsGitRelease(false)
// Clear uploaded file from map and picture accumulation ref
uploadedMediaFileMap.current.clear()
pictureImetaTagsRef.current = []
@ -1426,6 +1503,7 @@ export default function PostContent({ @@ -1426,6 +1503,7 @@ export default function PostContent({
setIsPublicMessage(false)
setIsHighlight(false)
setMediaNoteKind(null)
setIsGitRelease(false)
setIsCitationInternal(false)
setIsCitationExternal(false)
setIsCitationHardcopy(false)
@ -1455,7 +1533,8 @@ export default function PostContent({ @@ -1455,7 +1533,8 @@ export default function PostContent({
const handleCitationToggle = (type: 'internal' | 'external' | 'hardcopy' | 'prompt') => {
if (parentEvent) return // Can't create citations as replies
setIsGitRelease(false)
setIsCitationInternal(type === 'internal')
setIsCitationExternal(type === 'external')
setIsCitationHardcopy(type === 'hardcopy')
@ -1477,6 +1556,24 @@ export default function PostContent({ @@ -1477,6 +1556,24 @@ export default function PostContent({
}
}
const handleGitReleaseFromMenu = () => {
if (parentEvent) return
setIsGitRelease(true)
setIsCitationInternal(false)
setIsCitationExternal(false)
setIsCitationHardcopy(false)
setIsCitationPrompt(false)
setIsPoll(false)
setIsPublicMessage(false)
setIsHighlight(false)
setMediaNoteKind(null)
setIsLongFormArticle(false)
setIsWikiArticle(false)
setIsWikiArticleMarkdown(false)
setIsPublicationContent(false)
}
const handleClear = () => {
// Clear the post editor cache
postEditorCache.clearPostCache({ kind: getDeterminedKind, defaultContent, parentEvent })
@ -1502,6 +1599,15 @@ export default function PostContent({ @@ -1502,6 +1599,15 @@ export default function PostContent({
setIsCitationExternal(false)
setIsCitationHardcopy(false)
setIsCitationPrompt(false)
setIsGitRelease(false)
setReleaseRepoOwnerInput('')
setReleaseRepoId('')
setReleaseTagName('')
setReleaseTagHash('')
setReleaseTitle('')
setReleaseDownloadUrl('')
setReleaseDraft(false)
setReleasePrerelease(false)
// Clear citation fields
setCitationInternalCTag('')
setCitationInternalRelayHint('')

1
src/components/Profile/ProfileFeedWithPins.tsx

@ -99,6 +99,7 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string @@ -99,6 +99,7 @@ const ProfileFeedWithPins = forwardRef<{ refresh: () => void }, { pubkey: string
if (!isReply && !showKind1OPs) return false
}
if (event.kind === ExtendedKind.COMMENT && !showKind1111) return false
if (event.kind === ExtendedKind.GIT_RELEASE && !showKind1OPs) return false
return true
},
[profileTimelineShowKinds, showKind1OPs, showKind1Replies, showKind1111, hideReplies]

26
src/constants.ts

@ -4,6 +4,13 @@ import { kinds, type Filter } from 'nostr-tools' @@ -4,6 +4,13 @@ import { kinds, type Filter } from 'nostr-tools'
export const JUMBLE_API_BASE_URL =
(import.meta.env.VITE_JUMBLE_API_BASE_URL as string | undefined) ?? 'https://api.jumble.imwald.eu'
/** Git Republic web UI for repository links; override with VITE_GITREPUBLIC_WEB_BASE_URL for self-hosted. */
export const GITREPUBLIC_WEB_BASE_URL = (
(import.meta.env.VITE_GITREPUBLIC_WEB_BASE_URL as string | undefined) ?? 'https://gitrepublic.imwald.eu'
)
.trim()
.replace(/\/$/, '')
/**
* Piper TTS proxy (same contract as aitherboard `POST /api/piper-tts`: JSON `{ text, voice?, speed? }`, body `audio/wav`).
* Set `VITE_READ_ALOUD_TTS_URL` to your deployed aitherboard URL, e.g. `https://aitherboard.example.com/api/piper-tts`.
@ -350,7 +357,13 @@ export const ExtendedKind = { @@ -350,7 +357,13 @@ export const ExtendedKind = {
/** NIP-58 Badges: badge definition (addressable) */
BADGE_DEFINITION: 30009,
/** Web page bookmark (URL in i/I or r tags); used in RSS+Web relay discovery */
WEB_BOOKMARK: 39701
WEB_BOOKMARK: 39701,
/** NIP-34 / Git Republic: repository announcement (addressable) */
GIT_REPO_ANNOUNCEMENT: 30617,
/** NIP-34 / Git Republic: issue */
GIT_ISSUE: 1621,
/** Git Republic: release (linked to repo via `a` tag) */
GIT_RELEASE: 1642
}
/**
@ -452,7 +465,10 @@ export const SUPPORTED_KINDS = [ @@ -452,7 +465,10 @@ export const SUPPORTED_KINDS = [
// ExtendedKind.PUBLICATION_CONTENT, // Excluded - publication content should only be embedded in publications
// NIP-89 Application Handlers
ExtendedKind.APPLICATION_HANDLER_RECOMMENDATION,
ExtendedKind.APPLICATION_HANDLER_INFO
ExtendedKind.APPLICATION_HANDLER_INFO,
ExtendedKind.GIT_REPO_ANNOUNCEMENT,
ExtendedKind.GIT_ISSUE,
ExtendedKind.GIT_RELEASE
]
/**
@ -472,7 +488,11 @@ export const PROFILE_FEED_KINDS = SUPPORTED_KINDS.filter( @@ -472,7 +488,11 @@ export const PROFILE_FEED_KINDS = SUPPORTED_KINDS.filter(
* and most faux spells. Reposts are still shown on profile timelines, Spells Following, and Follows latest.
*/
export const DEFAULT_FEED_SHOW_KINDS = PROFILE_FEED_KINDS.filter(
(k) => k !== kinds.Repost && k !== ExtendedKind.GENERIC_REPOST
(k) =>
k !== kinds.Repost &&
k !== ExtendedKind.GENERIC_REPOST &&
k !== ExtendedKind.GIT_REPO_ANNOUNCEMENT &&
k !== ExtendedKind.GIT_ISSUE
)
/** Order for faux-spells in the feed / spell picker. */

11
src/i18n/locales/de.ts

@ -724,6 +724,17 @@ export default { @@ -724,6 +724,17 @@ export default {
'Voice Posts': 'Sprachbeiträge',
'Photo Posts': 'Fotobeiträge',
'Video Posts': 'Videobeiträge',
'Git repositories': 'Git-Repositories',
'Git issues': 'Git-Issues',
'Git releases': 'Git-Releases',
'Git Republic repository': 'Git-Republic-Repository',
'Git Republic issue': 'Git-Republic-Issue',
'Git Republic release': 'Git-Republic-Release',
'Git Republic event': 'Git-Republic-Ereignis',
'Git Republic': 'Git Republic',
'Open in Git Republic': 'In Git Republic öffnen',
'Pre-release': 'Vorabversion',
Draft: 'Entwurf',
'Select All': 'Alle auswählen',
'Clear All': 'Alle löschen',
'Set as default filter': 'Als Standardfilter festlegen',

11
src/i18n/locales/en.ts

@ -769,6 +769,17 @@ export default { @@ -769,6 +769,17 @@ export default {
'Voice Posts': 'Voice Posts',
'Photo Posts': 'Photo Posts',
'Video Posts': 'Video Posts',
'Git repositories': 'Git repositories',
'Git issues': 'Git issues',
'Git releases': 'Git releases',
'Git Republic repository': 'Git Republic repository',
'Git Republic issue': 'Git Republic issue',
'Git Republic release': 'Git Republic release',
'Git Republic event': 'Git Republic event',
'Git Republic': 'Git Republic',
'Open in Git Republic': 'Open in Git Republic',
'Pre-release': 'Pre-release',
Draft: 'Draft',
'Select All': 'Select All',
'Clear All': 'Clear All',
'Set as default filter': 'Set as default filter',

41
src/lib/draft-event.ts

@ -2223,3 +2223,44 @@ export function createCitationPromptDraftEvent( @@ -2223,3 +2223,44 @@ export function createCitationPromptDraftEvent(
created_at: dayjs().unix()
}
}
/** Git Republic release (kind 1642); mirrors `releases-service` tag layout. */
export function createGitReleaseDraftEvent(
content: string,
options: {
repoOwnerPubkey: string
repoId: string
tagName: string
tagHash: string
title?: string
downloadUrl?: string
isDraft?: boolean
isPrerelease?: boolean
}
): TDraftEvent {
const repoAddress = `${ExtendedKind.GIT_REPO_ANNOUNCEMENT}:${options.repoOwnerPubkey}:${options.repoId}`
const tags: string[][] = [
['a', repoAddress],
['p', options.repoOwnerPubkey],
['tag', options.tagName],
['r', options.tagHash, '', 'tag']
]
if (options.title) {
tags.push(['title', options.title])
}
if (options.downloadUrl) {
tags.push(['r', options.downloadUrl, '', 'download'])
}
if (options.isDraft) {
tags.push(['draft', 'true'])
}
if (options.isPrerelease) {
tags.push(['prerelease', 'true'])
}
return {
kind: ExtendedKind.GIT_RELEASE,
content,
tags,
created_at: dayjs().unix()
}
}

56
src/lib/git-republic-event.ts

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
import { ExtendedKind, GITREPUBLIC_WEB_BASE_URL } from '@/constants'
import type { Event } from 'nostr-tools'
import { nip19 } from 'nostr-tools'
export type GitRepublicRepoContext = {
ownerHex: string
repoId: string
/** From kind 30617 `name` tag when available */
displayName?: string
}
/**
* Resolve owner pubkey, repo id, and optional display name for Git Republic events.
* Kind 30617 uses `d` + `name`; issues and releases reference the repo via `a` (30617:pubkey:repoId).
*/
export function getGitRepublicRepoContext(event: Event): GitRepublicRepoContext | null {
if (event.kind === ExtendedKind.GIT_REPO_ANNOUNCEMENT) {
const d = event.tags.find((t) => t[0] === 'd')?.[1]
if (!d) return null
return {
ownerHex: event.pubkey,
repoId: d,
displayName: event.tags.find((t) => t[0] === 'name')?.[1]
}
}
const a = event.tags.find((t) => t[0] === 'a')?.[1]
if (!a) return null
const parts = a.split(':')
if (parts.length !== 3 || parts[0] !== String(ExtendedKind.GIT_REPO_ANNOUNCEMENT)) return null
return { ownerHex: parts[1], repoId: parts[2] }
}
/** Accepts hex pubkey or `npub…` for Git Republic repo owner fields in forms. */
export function parseRepoOwnerPubkeyInput(input: string): string | null {
const t = input.trim()
if (!t) return null
if (/^[0-9a-fA-F]{64}$/.test(t)) return t.toLowerCase()
try {
const dec = nip19.decode(t)
if (dec.type === 'npub') return dec.data as string
} catch {
return null
}
return null
}
export function gitRepublicRepoWebUrl(ctx: GitRepublicRepoContext): string | null {
try {
const npub = nip19.npubEncode(ctx.ownerHex)
const repo = encodeURIComponent(ctx.repoId)
return `${GITREPUBLIC_WEB_BASE_URL}/repos/${npub}/${repo}`
} catch {
return null
}
}

6
src/lib/kind-description.ts

@ -125,6 +125,12 @@ export function getKindDescription( @@ -125,6 +125,12 @@ export function getKindDescription(
return { number: 8, description: 'Badge award' }
case ExtendedKind.WEB_BOOKMARK:
return { number: 39701, description: 'Web bookmark' }
case ExtendedKind.GIT_REPO_ANNOUNCEMENT:
return { number: 30617, description: 'Git repository' }
case ExtendedKind.GIT_ISSUE:
return { number: 1621, description: 'Git issue' }
case ExtendedKind.GIT_RELEASE:
return { number: 1642, description: 'Git release' }
default:
return { number: kind, description: `Event (kind ${kind})` }
}

7
src/services/local-storage.service.ts

@ -301,13 +301,18 @@ class LocalStorageService { @@ -301,13 +301,18 @@ class LocalStorageService {
showKinds.push(ExtendedKind.ZAP_POLL)
}
}
if (showKindsVersion < 11) {
if (!showKinds.includes(ExtendedKind.GIT_RELEASE)) {
showKinds.push(ExtendedKind.GIT_RELEASE)
}
}
// v9: boosts are optional in the same filter list as other kinds; do not auto-enable (leave absent).
this.showKinds = showKinds
// Only persist when we read from localStorage. If SHOW_KINDS is missing here (migrated to IDB and
// keys cleared), persisting would write DEFAULT_FEED_SHOW_KINDS to IndexedDB and wipe the user's
// saved filter before initAsync/applySettings runs.
this.persistSetting(StorageKey.SHOW_KINDS, JSON.stringify(this.showKinds))
this.persistSetting(StorageKey.SHOW_KINDS_VERSION, '10')
this.persistSetting(StorageKey.SHOW_KINDS_VERSION, '11')
}
// Feed filter: kind 1 OPs, kind 1 replies, kind 1111 (migrate from legacy showRepliesAndComments if set)

Loading…
Cancel
Save