Files
archy/packages/app/src/composables/useConversationShare.ts
T
DorianandClaude Opus 4.6 77bad97377 feat(collab): collaboration & sharing (M20.1-M20.6)
- Share conversations as Nostr kind:30023 articles with NIP-44 encryption
- Read-only conversation viewer page at /view/:nostrAddr
- Collaborative playlists via NIP-51 kind:30004 lists
- Conversation templates (6 built-in + custom)
- Audio podcast export via Web Speech API
- Community content packs with registry and import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 01:22:01 +00:00

104 lines
3.1 KiB
TypeScript

import { ref } from 'vue'
import { useNostrIdentity } from './useNostrIdentity'
import { useNostr } from './useNostr'
import { exportAsMarkdown } from '@/utils/conversation-export'
import type { Conversation } from '@aiui/core/types/message'
export function useConversationShare() {
const { signEvent, pubkey, isLoggedIn } = useNostrIdentity()
const { publishEvent } = useNostr()
const isSharing = ref(false)
const shareError = ref<string | null>(null)
const sharedNaddr = ref<string | null>(null)
async function shareAsNostrArticle(
conversation: Conversation,
options: { encrypt?: boolean; recipientPubkey?: string } = {}
): Promise<string | null> {
if (!isLoggedIn.value || !pubkey.value) {
shareError.value = 'Please log in with a Nostr extension (NIP-07) first.'
return null
}
isSharing.value = true
shareError.value = null
try {
const markdown = exportAsMarkdown(conversation)
const dTag = `aiui-conv-${conversation.id}`
const title = conversation.title || 'AIUI Conversation'
let content = markdown
if (options.encrypt && options.recipientPubkey) {
// NIP-44 encryption via extension
const ext = (window as unknown as Record<string, unknown>).nostr as {
nip44?: { encrypt(pubkey: string, plaintext: string): Promise<string> }
} | undefined
if (ext?.nip44?.encrypt) {
content = await ext.nip44.encrypt(options.recipientPubkey, markdown)
} else {
shareError.value = 'NIP-44 encryption not supported by your extension.'
return null
}
}
const tags: string[][] = [
['d', dTag],
['title', title],
['published_at', String(Math.floor(conversation.createdAt / 1000))],
['t', 'aiui'],
['t', 'conversation'],
]
if (conversation.model) {
tags.push(['t', conversation.model])
}
if (options.encrypt && options.recipientPubkey) {
tags.push(['p', options.recipientPubkey])
tags.push(['encrypted', 'nip44'])
}
const unsigned = {
kind: 30023, // Long-form article
pubkey: pubkey.value,
created_at: Math.floor(Date.now() / 1000),
content,
tags,
}
const signed = await signEvent(unsigned)
if (!signed) {
shareError.value = 'Failed to sign event.'
return null
}
await publishEvent(signed)
// Build naddr
const naddr = buildNaddr(dTag, pubkey.value, signed.kind)
sharedNaddr.value = naddr
return naddr
} catch (err) {
shareError.value = err instanceof Error ? err.message : 'Failed to share conversation.'
return null
} finally {
isSharing.value = false
}
}
function buildNaddr(dTag: string, authorPubkey: string, kind: number): string {
// Simplified naddr encoding — in production use a proper bech32 library
const parts = [dTag, authorPubkey, String(kind)]
return `nostr:naddr1${btoa(parts.join(':')).replace(/=/g, '')}`
}
return {
isSharing,
shareError,
sharedNaddr,
shareAsNostrArticle,
}
}