From 99ca0bf64af0c0005b40215fc8110c48bc10dffe Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 28 May 2026 16:51:47 +0200 Subject: [PATCH] chore(nostr-feed): delete dead-code ReactionService + useReactions duplicates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nostr-feed module had its own copies of ReactionService and useReactions that were never wired in — the live implementations live in src/modules/base/. The nostr-feed copy of ReactionService was a strict subset of the base copy (missing toggleLikeEvent / toggleDislikeEvent) and was never registered in DI. The nostr-feed copy of useReactions was identical to the base copy modulo the type import path; the one consumer (NostrFeed.vue) already imports from the base path. Closes #78. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../nostr-feed/composables/useReactions.ts | 102 --- .../nostr-feed/services/ReactionService.ts | 585 ------------------ 2 files changed, 687 deletions(-) delete mode 100644 src/modules/nostr-feed/composables/useReactions.ts delete mode 100644 src/modules/nostr-feed/services/ReactionService.ts diff --git a/src/modules/nostr-feed/composables/useReactions.ts b/src/modules/nostr-feed/composables/useReactions.ts deleted file mode 100644 index 96e68ac..0000000 --- a/src/modules/nostr-feed/composables/useReactions.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { computed } from 'vue' -import { injectService, SERVICE_TOKENS } from '@/core/di-container' -import type { ReactionService, EventReactions } from '../services/ReactionService' -import { useToast } from '@/core/composables/useToast' - -/** - * Composable for managing reactions in the feed - */ -export function useReactions() { - const reactionService = injectService(SERVICE_TOKENS.REACTION_SERVICE) - const toast = useToast() - - /** - * Get reactions for a specific event - */ - const getEventReactions = (eventId: string): EventReactions => { - if (!reactionService) { - return { - eventId, - likes: 0, - dislikes: 0, - totalReactions: 0, - userHasLiked: false, - userHasDisliked: false, - reactions: [] - } - } - return reactionService.getEventReactions(eventId) - } - - /** - * Subscribe to reactions for a list of event IDs - */ - const subscribeToReactions = async (eventIds: string[]): Promise => { - if (!reactionService || eventIds.length === 0) return - - try { - await reactionService.subscribeToReactions(eventIds) - } catch (error) { - console.error('Failed to subscribe to reactions:', error) - } - } - - /** - * Toggle like on an event - like if not liked, unlike if already liked - */ - const toggleLike = async (eventId: string, eventPubkey: string, eventKind: number): Promise => { - if (!reactionService) { - toast.error('Reaction service not available') - return - } - - try { - await reactionService.toggleLikeEvent(eventId, eventPubkey, eventKind) - - // Check if we liked or unliked - const eventReactions = reactionService.getEventReactions(eventId) - if (eventReactions.userHasLiked) { - toast.success('Post liked!') - } else { - toast.success('Like removed') - } - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to toggle reaction' - - if (message.includes('authenticated')) { - toast.error('Please sign in to react to posts') - } else if (message.includes('Not connected')) { - toast.error('Not connected to relays') - } else { - toast.error(message) - } - - console.error('Failed to toggle like:', error) - } - } - - /** - * Get loading state - */ - const isLoading = computed(() => { - return reactionService?.isLoading ?? false - }) - - /** - * Get all event reactions (for debugging) - */ - const allEventReactions = computed(() => { - return reactionService?.eventReactions ?? new Map() - }) - - return { - // Methods - getEventReactions, - subscribeToReactions, - toggleLike, - - // State - isLoading, - allEventReactions - } -} \ No newline at end of file diff --git a/src/modules/nostr-feed/services/ReactionService.ts b/src/modules/nostr-feed/services/ReactionService.ts deleted file mode 100644 index 24adfb7..0000000 --- a/src/modules/nostr-feed/services/ReactionService.ts +++ /dev/null @@ -1,585 +0,0 @@ -import { ref, reactive } from 'vue' -import { BaseService } from '@/core/base/BaseService' -import { injectService, SERVICE_TOKENS } from '@/core/di-container' -import { finalizeEvent, type EventTemplate } from 'nostr-tools' -import type { Event as NostrEvent } from 'nostr-tools' - -export interface Reaction { - id: string - eventId: string // The event being reacted to - pubkey: string // Who reacted - content: string // The reaction content ('+', '-', emoji) - created_at: number -} - -export interface EventReactions { - eventId: string - likes: number - dislikes: number - totalReactions: number - userHasLiked: boolean - userHasDisliked: boolean - userReactionId?: string // Track the user's reaction ID for deletion - reactions: Reaction[] -} - -export class ReactionService extends BaseService { - protected readonly metadata = { - name: 'ReactionService', - version: '1.0.0', - dependencies: [] - } - - protected relayHub: any = null - protected authService: any = null - - // Reaction state - indexed by event ID - private _eventReactions = reactive(new Map()) - private _isLoading = ref(false) - - // Track reaction subscription - private currentSubscription: string | null = null - private currentUnsubscribe: (() => void) | null = null - - // Track which events we're monitoring - private monitoredEvents = new Set() - - // Track deleted reactions to hide them - private deletedReactions = new Set() - - protected async onInitialize(): Promise { - console.log('ReactionService: Starting initialization...') - - this.relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) - this.authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) - - if (!this.relayHub) { - throw new Error('RelayHub service not available') - } - - // Deletion monitoring is now handled by FeedService's consolidated subscription - console.log('ReactionService: Initialization complete (deletion monitoring handled by FeedService)') - } - - /** - * Get reactions for a specific event - */ - getEventReactions(eventId: string): EventReactions { - if (!this._eventReactions.has(eventId)) { - this._eventReactions.set(eventId, { - eventId, - likes: 0, - dislikes: 0, - totalReactions: 0, - userHasLiked: false, - userHasDisliked: false, - reactions: [] - }) - } - return this._eventReactions.get(eventId)! - } - - /** - * Subscribe to reactions for a list of event IDs - */ - async subscribeToReactions(eventIds: string[]): Promise { - if (eventIds.length === 0) return - - // Filter out events we're already monitoring - const newEventIds = eventIds.filter(id => !this.monitoredEvents.has(id)) - if (newEventIds.length === 0) return - - console.log(`ReactionService: Subscribing to reactions for ${newEventIds.length} events`) - - try { - if (!this.relayHub?.isConnected) { - await this.relayHub?.connect() - } - - // Add to monitored set - newEventIds.forEach(id => this.monitoredEvents.add(id)) - - const subscriptionId = `reactions-${Date.now()}` - - // Subscribe to reactions (kind 7) for these events - // Deletions (kind 5) are now handled by FeedService's consolidated subscription - const filters = [ - { - kinds: [7], // Reactions - '#e': newEventIds, // Events being reacted to - limit: 1000 - } - ] - - const unsubscribe = this.relayHub.subscribe({ - id: subscriptionId, - filters: filters, - onEvent: (event: NostrEvent) => { - this.handleReactionEvent(event) - }, - onEose: () => { - console.log(`ReactionService: Subscription ${subscriptionId} ready`) - } - }) - - // Store subscription info (we can have multiple) - if (!this.currentSubscription) { - this.currentSubscription = subscriptionId - this.currentUnsubscribe = unsubscribe - } - - } catch (error) { - console.error('Failed to subscribe to reactions:', error) - } - } - - /** - * Handle incoming reaction event - * Made public so FeedService can route kind 7 events to this service - */ - public handleReactionEvent(event: NostrEvent): void { - try { - // Find the event being reacted to - const eTag = event.tags.find(tag => tag[0] === 'e') - if (!eTag || !eTag[1]) { - console.warn('Reaction event missing e tag:', event.id) - return - } - - const eventId = eTag[1] - const content = event.content.trim() - - // Create reaction object - const reaction: Reaction = { - id: event.id, - eventId, - pubkey: event.pubkey, - content, - created_at: event.created_at - } - - // Update event reactions - const eventReactions = this.getEventReactions(eventId) - - // Check if this reaction already exists (deduplication) or is deleted - const existingIndex = eventReactions.reactions.findIndex(r => r.id === reaction.id) - if (existingIndex >= 0) { - return // Already have this reaction - } - - // Check if this reaction has been deleted - if (this.deletedReactions.has(reaction.id)) { - return // This reaction was deleted - } - - // IMPORTANT: Remove any previous reaction from the same user - // This ensures one reaction per user per event, even if deletion events aren't processed - const previousReactionIndex = eventReactions.reactions.findIndex(r => - r.pubkey === reaction.pubkey && - r.content === reaction.content - ) - - if (previousReactionIndex >= 0) { - // Replace the old reaction with the new one - eventReactions.reactions[previousReactionIndex] = reaction - } else { - // Add as new reaction - eventReactions.reactions.push(reaction) - } - - // Recalculate counts and user state - this.recalculateEventReactions(eventId) - - } catch (error) { - console.error('Failed to handle reaction event:', error) - } - } - - /** - * Handle deletion event (called by FeedService when a kind 5 event with k=7 is received) - * Made public so FeedService can route deletion events to this service - */ - public handleDeletionEvent(event: NostrEvent): void { - try { - // Process each deleted event - const eTags = event.tags.filter(tag => tag[0] === 'e') - const deletionAuthor = event.pubkey - - for (const eTag of eTags) { - const deletedEventId = eTag[1] - if (deletedEventId) { - // Add to deleted set - this.deletedReactions.add(deletedEventId) - - // Find and remove the reaction from all event reactions - for (const [eventId, eventReactions] of this._eventReactions) { - const reactionIndex = eventReactions.reactions.findIndex(r => r.id === deletedEventId) - - if (reactionIndex >= 0) { - const reaction = eventReactions.reactions[reactionIndex] - - // IMPORTANT: Only process deletion if it's from the same user who created the reaction - // This follows NIP-09 spec: "Relays SHOULD delete or stop publishing any referenced events - // that have an identical `pubkey` as the deletion request" - if (reaction.pubkey === deletionAuthor) { - eventReactions.reactions.splice(reactionIndex, 1) - // Recalculate counts for this event - this.recalculateEventReactions(eventId) - } - } - } - } - } - } catch (error) { - console.error('Failed to handle deletion event:', error) - } - } - - /** - * Recalculate reaction counts and user state for an event - */ - private recalculateEventReactions(eventId: string): void { - const eventReactions = this.getEventReactions(eventId) - const userPubkey = this.authService?.user?.value?.pubkey - - // Use Sets to track unique users who liked/disliked - const likedUsers = new Set() - const dislikedUsers = new Set() - let userHasLiked = false - let userHasDisliked = false - let userReactionId: string | undefined - - // Group reactions by user, keeping only the most recent - const latestReactionsByUser = new Map() - - for (const reaction of eventReactions.reactions) { - // Skip deleted reactions - if (this.deletedReactions.has(reaction.id)) { - continue - } - - // Keep only the latest reaction from each user - const existing = latestReactionsByUser.get(reaction.pubkey) - if (!existing || reaction.created_at > existing.created_at) { - latestReactionsByUser.set(reaction.pubkey, reaction) - } - } - - // Now count unique reactions - for (const reaction of latestReactionsByUser.values()) { - const isLike = reaction.content === '+' || reaction.content === '❤️' || reaction.content === '' - const isDislike = reaction.content === '-' - - if (isLike) { - likedUsers.add(reaction.pubkey) - if (userPubkey && reaction.pubkey === userPubkey) { - userHasLiked = true - userReactionId = reaction.id - } - } else if (isDislike) { - dislikedUsers.add(reaction.pubkey) - if (userPubkey && reaction.pubkey === userPubkey) { - userHasDisliked = true - userReactionId = reaction.id - } - } - } - - // Update the reactive state with unique user counts - eventReactions.likes = likedUsers.size - eventReactions.dislikes = dislikedUsers.size - eventReactions.totalReactions = latestReactionsByUser.size - eventReactions.userHasLiked = userHasLiked - eventReactions.userHasDisliked = userHasDisliked - eventReactions.userReactionId = userReactionId - } - - /** - * Send a heart reaction (like) to an event - */ - async likeEvent(eventId: string, eventPubkey: string, eventKind: number): Promise { - if (!this.authService?.isAuthenticated?.value) { - throw new Error('Must be authenticated to react') - } - - if (!this.relayHub?.isConnected) { - throw new Error('Not connected to relays') - } - - const userPubkey = this.authService.user.value?.pubkey - const userPrivkey = this.authService.user.value?.prvkey - - if (!userPubkey || !userPrivkey) { - throw new Error('User keys not available') - } - - // Check if user already liked this event - const eventReactions = this.getEventReactions(eventId) - if (eventReactions.userHasLiked) { - throw new Error('Already liked this event') - } - - try { - this._isLoading.value = true - - // Create reaction event template according to NIP-25 - const eventTemplate: EventTemplate = { - kind: 7, // Reaction - content: '+', // Like reaction - tags: [ - ['e', eventId, '', eventPubkey], // Event being reacted to - ['p', eventPubkey], // Author of the event being reacted to - ['k', eventKind.toString()] // Kind of the event being reacted to - ], - created_at: Math.floor(Date.now() / 1000) - } - - // Sign the event - const privkeyBytes = this.hexToUint8Array(userPrivkey) - const signedEvent = finalizeEvent(eventTemplate, privkeyBytes) - - // Publish the reaction - await this.relayHub.publishEvent(signedEvent) - - // Optimistically update local state - this.handleReactionEvent(signedEvent) - - } catch (error) { - console.error('Failed to like event:', error) - throw error - } finally { - this._isLoading.value = false - } - } - - /** - * Remove a like from an event (unlike) using NIP-09 deletion events - */ - async unlikeEvent(eventId: string): Promise { - if (!this.authService?.isAuthenticated?.value) { - throw new Error('Must be authenticated to remove reaction') - } - - if (!this.relayHub?.isConnected) { - throw new Error('Not connected to relays') - } - - const userPubkey = this.authService.user.value?.pubkey - const userPrivkey = this.authService.user.value?.prvkey - - if (!userPubkey || !userPrivkey) { - throw new Error('User keys not available') - } - - // Get the user's reaction ID to delete - const eventReactions = this.getEventReactions(eventId) - - if (!eventReactions.userHasLiked || !eventReactions.userReactionId) { - throw new Error('No reaction to remove') - } - - try { - this._isLoading.value = true - - // Create deletion event according to NIP-09 - const eventTemplate: EventTemplate = { - kind: 5, // Deletion request - content: '', // Empty content or reason - tags: [ - ['e', eventReactions.userReactionId], // The reaction event to delete - ['k', '7'] // Kind of event being deleted (reaction) - ], - created_at: Math.floor(Date.now() / 1000) - } - - // Sign the event - const privkeyBytes = this.hexToUint8Array(userPrivkey) - const signedEvent = finalizeEvent(eventTemplate, privkeyBytes) - - // Publish the deletion - const result = await this.relayHub.publishEvent(signedEvent) - - console.log(`ReactionService: Deletion published to ${result.success}/${result.total} relays`) - - // Optimistically update local state - this.handleDeletionEvent(signedEvent) - - } catch (error) { - console.error('Failed to unlike event:', error) - throw error - } finally { - this._isLoading.value = false - } - } - - /** - * Toggle like on an event - like if not liked, unlike if already liked - */ - async toggleLikeEvent(eventId: string, eventPubkey: string, eventKind: number): Promise { - const eventReactions = this.getEventReactions(eventId) - - if (eventReactions.userHasLiked) { - // Unlike the event - await this.unlikeEvent(eventId) - } else { - // Like the event - await this.likeEvent(eventId, eventPubkey, eventKind) - } - } - - /** - * Send a dislike reaction to an event - */ - async dislikeEvent(eventId: string, eventPubkey: string, eventKind: number): Promise { - if (!this.authService?.isAuthenticated?.value) { - throw new Error('Must be authenticated to react') - } - - if (!this.relayHub?.isConnected) { - throw new Error('Not connected to relays') - } - - const userPubkey = this.authService.user.value?.pubkey - const userPrivkey = this.authService.user.value?.prvkey - - if (!userPubkey || !userPrivkey) { - throw new Error('User keys not available') - } - - // Check if user already disliked this event - const eventReactions = this.getEventReactions(eventId) - if (eventReactions.userHasDisliked) { - throw new Error('Already disliked this event') - } - - try { - this._isLoading.value = true - - // Create reaction event template according to NIP-25 - const eventTemplate: EventTemplate = { - kind: 7, // Reaction - content: '-', // Dislike reaction - tags: [ - ['e', eventId, '', eventPubkey], // Event being reacted to - ['p', eventPubkey], // Author of the event being reacted to - ['k', eventKind.toString()] // Kind of the event being reacted to - ], - created_at: Math.floor(Date.now() / 1000) - } - - // Sign the event - const privkeyBytes = this.hexToUint8Array(userPrivkey) - const signedEvent = finalizeEvent(eventTemplate, privkeyBytes) - - // Publish the reaction - await this.relayHub.publishEvent(signedEvent) - - // Optimistically update local state - this.handleReactionEvent(signedEvent) - - } catch (error) { - console.error('Failed to dislike event:', error) - throw error - } finally { - this._isLoading.value = false - } - } - - /** - * Remove a dislike from an event using NIP-09 deletion events - */ - async undislikeEvent(eventId: string): Promise { - if (!this.authService?.isAuthenticated?.value) { - throw new Error('Must be authenticated to remove reaction') - } - - if (!this.relayHub?.isConnected) { - throw new Error('Not connected to relays') - } - - const userPubkey = this.authService.user.value?.pubkey - const userPrivkey = this.authService.user.value?.prvkey - - if (!userPubkey || !userPrivkey) { - throw new Error('User keys not available') - } - - // Get the user's reaction ID to delete - const eventReactions = this.getEventReactions(eventId) - - if (!eventReactions.userHasDisliked || !eventReactions.userReactionId) { - throw new Error('No dislike reaction to remove') - } - - try { - this._isLoading.value = true - - // Create deletion event according to NIP-09 - const eventTemplate: EventTemplate = { - kind: 5, // Deletion request - content: '', // Empty content or reason - tags: [ - ['e', eventReactions.userReactionId], // The reaction event to delete - ['k', '7'] // Kind of event being deleted (reaction) - ], - created_at: Math.floor(Date.now() / 1000) - } - - // Sign the event - const privkeyBytes = this.hexToUint8Array(userPrivkey) - const signedEvent = finalizeEvent(eventTemplate, privkeyBytes) - - // Publish the deletion - const result = await this.relayHub.publishEvent(signedEvent) - - console.log(`ReactionService: Dislike deletion published to ${result.success}/${result.total} relays`) - - // Optimistically update local state - this.handleDeletionEvent(signedEvent) - - } catch (error) { - console.error('Failed to remove dislike:', error) - throw error - } finally { - this._isLoading.value = false - } - } - - /** - * Helper function to convert hex string to Uint8Array - */ - private hexToUint8Array(hex: string): Uint8Array { - const bytes = new Uint8Array(hex.length / 2) - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.substr(i, 2), 16) - } - return bytes - } - - /** - * Get all event reactions - */ - get eventReactions(): Map { - return this._eventReactions - } - - /** - * Check if currently loading - */ - get isLoading(): boolean { - return this._isLoading.value - } - - /** - * Cleanup - */ - protected async onDestroy(): Promise { - if (this.currentUnsubscribe) { - this.currentUnsubscribe() - } - // deletionUnsubscribe is no longer used - deletions handled by FeedService - this._eventReactions.clear() - this.monitoredEvents.clear() - this.deletedReactions.clear() - } -} \ No newline at end of file -- 2.53.0