fix(nostr-feed): Fix comment loading and deduplication

- Add subscribeToSubmission() to fetch submission + comments by ID
- Fix isComment check order - check before parseSubmission (which fails for comments)
- Update comment count on submission when comments are added
- Add duplicate comment detection to prevent double-display
- Update useSubmission composable to auto-subscribe on mount

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-01-01 19:59:49 +01:00
parent 9c8abe2f5c
commit 464f6ae98c
3 changed files with 120 additions and 20 deletions

View file

@ -4,7 +4,7 @@
* 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 { formatDistanceToNow } from 'date-fns'
import {
@ -26,7 +26,7 @@ import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import VoteControls from './VoteControls.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 type { ProfileService } from '../services/ProfileService'
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 submissionService = tryInjectService<SubmissionService>(SERVICE_TOKENS.SUBMISSION_SERVICE)
// Use submission composable
const { submission, comments, upvote, downvote } = useSubmission(props.submissionId)
// Subscribe to fetch the submission if not already loaded
const { subscribe, isLoading, error } = useSubmissions({ autoSubscribe: false })
// Use submission composable - handles subscription automatically
const { submission, comments, upvote, downvote, isLoading, error } = useSubmission(props.submissionId)
// Comment composer state
const showComposer = ref(false)
@ -215,13 +212,6 @@ function countReplies(comment: SubmissionCommentType): number {
function goBack() {
router.back()
}
// Subscribe on mount if submission not loaded
onMounted(() => {
if (!submission.value) {
subscribe({ limit: 50 })
}
})
</script>
<template>

View file

@ -289,9 +289,27 @@ export function useSubmissions(options: UseSubmissionsOptions = {}): UseSubmissi
export function useSubmission(submissionId: string) {
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 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> {
await submissionService?.upvote(submissionId)
}
@ -300,9 +318,17 @@ export function useSubmission(submissionId: string) {
await submissionService?.downvote(submissionId)
}
// Subscribe on mount
onMounted(() => {
subscribe()
})
return {
submission,
comments,
isLoading: computed(() => isLoading.value || submissionService?.isLoading.value || false),
error: computed(() => error.value || submissionService?.error.value || null),
subscribe,
upvote,
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
*/
@ -274,6 +346,12 @@ export class SubmissionService extends BaseService {
}
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
const submission = this.parseSubmission(event)
if (!submission) {
@ -281,12 +359,6 @@ export class SubmissionService extends BaseService {
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
const submissionWithMeta = this.enrichSubmission(submission)
this._submissions.set(submission.id, submissionWithMeta)
@ -329,6 +401,12 @@ export class SubmissionService extends BaseService {
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
const comment: SubmissionComment = {
id: event.id,
@ -347,6 +425,12 @@ export class SubmissionService extends BaseService {
this._comments.set(rootId, [])
}
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
}
}
/**