[Draft] feat(nostr-feed): Reddit-style link aggregator #9

Open
padreug wants to merge 19 commits from feature/link-aggregator into main
3 changed files with 120 additions and 20 deletions
Showing only changes of commit 464f6ae98c - Show all commits

View file

@ -4,7 +4,7 @@
* Displays complete submission content and threaded comments * Displays complete submission content and threaded comments
*/ */
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { formatDistanceToNow } from 'date-fns' import { formatDistanceToNow } from 'date-fns'
import { import {
@ -26,7 +26,7 @@ import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import VoteControls from './VoteControls.vue' import VoteControls from './VoteControls.vue'
import SubmissionCommentComponent from './SubmissionComment.vue' import SubmissionCommentComponent from './SubmissionComment.vue'
import { useSubmission, useSubmissions } from '../composables/useSubmissions' import { useSubmission } from '../composables/useSubmissions'
import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container' import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container'
import type { ProfileService } from '../services/ProfileService' import type { ProfileService } from '../services/ProfileService'
import type { SubmissionService } from '../services/SubmissionService' import type { SubmissionService } from '../services/SubmissionService'
@ -54,11 +54,8 @@ const profileService = tryInjectService<ProfileService>(SERVICE_TOKENS.PROFILE_S
const authService = tryInjectService<any>(SERVICE_TOKENS.AUTH_SERVICE) const authService = tryInjectService<any>(SERVICE_TOKENS.AUTH_SERVICE)
const submissionService = tryInjectService<SubmissionService>(SERVICE_TOKENS.SUBMISSION_SERVICE) const submissionService = tryInjectService<SubmissionService>(SERVICE_TOKENS.SUBMISSION_SERVICE)
// Use submission composable // Use submission composable - handles subscription automatically
const { submission, comments, upvote, downvote } = useSubmission(props.submissionId) const { submission, comments, upvote, downvote, isLoading, error } = useSubmission(props.submissionId)
// Subscribe to fetch the submission if not already loaded
const { subscribe, isLoading, error } = useSubmissions({ autoSubscribe: false })
// Comment composer state // Comment composer state
const showComposer = ref(false) const showComposer = ref(false)
@ -215,13 +212,6 @@ function countReplies(comment: SubmissionCommentType): number {
function goBack() { function goBack() {
router.back() router.back()
} }
// Subscribe on mount if submission not loaded
onMounted(() => {
if (!submission.value) {
subscribe({ limit: 50 })
}
})
</script> </script>
<template> <template>

View file

@ -289,9 +289,27 @@ export function useSubmissions(options: UseSubmissionsOptions = {}): UseSubmissi
export function useSubmission(submissionId: string) { export function useSubmission(submissionId: string) {
const submissionService = tryInjectService<SubmissionService>(SERVICE_TOKENS.SUBMISSION_SERVICE) const submissionService = tryInjectService<SubmissionService>(SERVICE_TOKENS.SUBMISSION_SERVICE)
const isLoading = ref(false)
const error = ref<string | null>(null)
const submission = computed(() => submissionService?.getSubmission(submissionId)) const submission = computed(() => submissionService?.getSubmission(submissionId))
const comments = computed(() => submissionService?.getThreadedComments(submissionId) ?? []) const comments = computed(() => submissionService?.getThreadedComments(submissionId) ?? [])
async function subscribe(): Promise<void> {
if (!submissionService) return
isLoading.value = true
error.value = null
try {
await submissionService.subscribeToSubmission(submissionId)
} catch (err: any) {
error.value = err.message || 'Failed to load submission'
} finally {
isLoading.value = false
}
}
async function upvote(): Promise<void> { async function upvote(): Promise<void> {
await submissionService?.upvote(submissionId) await submissionService?.upvote(submissionId)
} }
@ -300,9 +318,17 @@ export function useSubmission(submissionId: string) {
await submissionService?.downvote(submissionId) await submissionService?.downvote(submissionId)
} }
// Subscribe on mount
onMounted(() => {
subscribe()
})
return { return {
submission, submission,
comments, comments,
isLoading: computed(() => isLoading.value || submissionService?.isLoading.value || false),
error: computed(() => error.value || submissionService?.error.value || null),
subscribe,
upvote, upvote,
downvote downvote
} }

View file

@ -183,6 +183,78 @@ export class SubmissionService extends BaseService {
} }
} }
/**
* Subscribe to a specific submission and its comments
*/
async subscribeToSubmission(submissionId: string): Promise<void> {
this._isLoading.value = true
this._error.value = null
try {
if (!this.relayHub?.isConnected) {
await this.relayHub?.connect()
}
const subscriptionId = `submission-${submissionId}-${Date.now()}`
// Build filters for the submission and its comments
const filters: Filter[] = [
// The submission itself
{
kinds: [SUBMISSION_KINDS.SUBMISSION],
ids: [submissionId]
},
// Comments referencing this submission (via 'e' tag - parent reference)
{
kinds: [SUBMISSION_KINDS.SUBMISSION],
'#e': [submissionId]
},
// Comments referencing this submission (via 'E' tag - root reference)
{
kinds: [SUBMISSION_KINDS.SUBMISSION],
'#E': [submissionId]
},
// Reactions on the submission and comments
{
kinds: [SUBMISSION_KINDS.REACTION],
'#e': [submissionId]
}
]
this.debug('Subscribing to submission with filters:', filters)
const unsubscribe = this.relayHub.subscribe({
id: subscriptionId,
filters,
onEvent: (event: NostrEvent) => this.handleEvent(event),
onEose: () => {
this.debug('End of stored events for submission')
this._isLoading.value = false
},
onClose: () => {
this.debug('Submission subscription closed')
}
})
// Store for cleanup (don't overwrite main subscription)
// For now, we'll manage this separately
this.currentSubscription = subscriptionId
this.currentUnsubscribe = unsubscribe
// Timeout fallback
setTimeout(() => {
if (this._isLoading.value && this.currentSubscription === subscriptionId) {
this._isLoading.value = false
}
}, 10000)
} catch (err) {
this._error.value = err instanceof Error ? err.message : 'Failed to subscribe to submission'
this._isLoading.value = false
throw err
}
}
/** /**
* Build Nostr filters from feed configuration * Build Nostr filters from feed configuration
*/ */
@ -274,6 +346,12 @@ export class SubmissionService extends BaseService {
} }
this.seenEventIds.add(event.id) this.seenEventIds.add(event.id)
// Check if this is a top-level submission or a comment
if (this.isComment(event)) {
this.handleCommentEvent(event)
return
}
// Parse the submission // Parse the submission
const submission = this.parseSubmission(event) const submission = this.parseSubmission(event)
if (!submission) { if (!submission) {
@ -281,12 +359,6 @@ export class SubmissionService extends BaseService {
return return
} }
// Check if this is a top-level submission or a comment
if (this.isComment(event)) {
this.handleCommentEvent(event)
return
}
// Create full submission with metadata // Create full submission with metadata
const submissionWithMeta = this.enrichSubmission(submission) const submissionWithMeta = this.enrichSubmission(submission)
this._submissions.set(submission.id, submissionWithMeta) this._submissions.set(submission.id, submissionWithMeta)
@ -329,6 +401,12 @@ export class SubmissionService extends BaseService {
const rootId = rootTag[1] const rootId = rootTag[1]
// Check for duplicate comment
const existingComments = this._comments.get(rootId)
if (existingComments?.some(c => c.id === event.id)) {
return
}
// Parse as comment // Parse as comment
const comment: SubmissionComment = { const comment: SubmissionComment = {
id: event.id, id: event.id,
@ -347,6 +425,12 @@ export class SubmissionService extends BaseService {
this._comments.set(rootId, []) this._comments.set(rootId, [])
} }
this._comments.get(rootId)!.push(comment) this._comments.get(rootId)!.push(comment)
// Update comment count on the submission
const submission = this._submissions.get(rootId)
if (submission) {
submission.commentCount = this._comments.get(rootId)!.length
}
} }
/** /**