Squash merge nostrfeed-ui into main
This commit is contained in:
parent
5063a3e121
commit
cc5e0dbef6
10 changed files with 379 additions and 258 deletions
|
|
@ -31,7 +31,7 @@ export interface ContentFilter {
|
|||
}
|
||||
|
||||
export interface FeedConfig {
|
||||
feedType: 'announcements' | 'general' | 'mentions' | 'events' | 'all' | 'custom'
|
||||
feedType: 'all' | 'announcements' | 'rideshare' | 'custom'
|
||||
maxPosts?: number
|
||||
adminPubkeys?: string[]
|
||||
contentFilters?: ContentFilter[]
|
||||
|
|
@ -176,8 +176,8 @@ export class FeedService extends BaseService {
|
|||
filter.authors = config.adminPubkeys
|
||||
}
|
||||
break
|
||||
case 'general':
|
||||
// General posts - no specific author filtering
|
||||
case 'rideshare':
|
||||
// Rideshare posts handled via content filters
|
||||
break
|
||||
case 'all':
|
||||
default:
|
||||
|
|
@ -188,9 +188,20 @@ export class FeedService extends BaseService {
|
|||
filters.push(filter)
|
||||
}
|
||||
|
||||
// Add reactions (kind 7) to the filters
|
||||
filters.push({
|
||||
kinds: [7], // Reactions
|
||||
limit: 500
|
||||
})
|
||||
|
||||
// Add ALL deletion events (kind 5) - we'll route them based on the 'k' tag
|
||||
filters.push({
|
||||
kinds: [5] // All deletion events (for both posts and reactions)
|
||||
})
|
||||
|
||||
console.log(`Creating feed subscription for ${config.feedType} with filters:`, filters)
|
||||
|
||||
// Subscribe to events with deduplication
|
||||
// Subscribe to all events (posts, reactions, deletions) with deduplication
|
||||
const unsubscribe = this.relayHub.subscribe({
|
||||
id: subscriptionId,
|
||||
filters: filters,
|
||||
|
|
@ -232,7 +243,21 @@ export class FeedService extends BaseService {
|
|||
* Handle new event with robust deduplication
|
||||
*/
|
||||
private handleNewEvent(event: NostrEvent, config: FeedConfig): void {
|
||||
// Skip if event already seen
|
||||
// Route deletion events (kind 5) based on what's being deleted
|
||||
if (event.kind === 5) {
|
||||
this.handleDeletionEvent(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Route reaction events (kind 7) to ReactionService
|
||||
if (event.kind === 7) {
|
||||
if (this.reactionService) {
|
||||
this.reactionService.handleReactionEvent(event)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if event already seen (for posts only, kind 1)
|
||||
if (this.seenEventIds.has(event.id)) {
|
||||
return
|
||||
}
|
||||
|
|
@ -313,21 +338,62 @@ export class FeedService extends BaseService {
|
|||
}, 'nostr-feed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle deletion events (NIP-09)
|
||||
* Routes deletions to appropriate service based on the 'k' tag
|
||||
*/
|
||||
private handleDeletionEvent(event: NostrEvent): void {
|
||||
// Check the 'k' tag to determine what kind of event is being deleted
|
||||
const kTag = event.tags?.find((tag: string[]) => tag[0] === 'k')
|
||||
const deletedKind = kTag ? kTag[1] : null
|
||||
|
||||
// Route to ReactionService for reaction deletions (kind 7)
|
||||
if (deletedKind === '7') {
|
||||
if (this.reactionService) {
|
||||
this.reactionService.handleDeletionEvent(event)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle post deletions (kind 1) in FeedService
|
||||
if (deletedKind === '1' || !deletedKind) {
|
||||
// Extract event IDs to delete from 'e' tags
|
||||
const eventIdsToDelete = event.tags
|
||||
?.filter((tag: string[]) => tag[0] === 'e')
|
||||
.map((tag: string[]) => tag[1]) || []
|
||||
|
||||
if (eventIdsToDelete.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove deleted posts from the feed
|
||||
this._posts.value = this._posts.value.filter(post => {
|
||||
// Only delete if the deletion request comes from the same author (NIP-09 validation)
|
||||
if (eventIdsToDelete.includes(post.id) && post.pubkey === event.pubkey) {
|
||||
// Also remove from seen events so it won't be re-added
|
||||
this.seenEventIds.delete(post.id)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event should be included in feed
|
||||
*/
|
||||
private shouldIncludeEvent(event: NostrEvent, config: FeedConfig): boolean {
|
||||
// Never include reactions (kind 7) or deletions (kind 5) in the main feed
|
||||
// These should only be processed by the ReactionService
|
||||
if (event.kind === 7 || event.kind === 5) {
|
||||
// Never include reactions (kind 7) in the main feed
|
||||
// Reactions should only be processed by the ReactionService
|
||||
if (event.kind === 7) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
const isAdminPost = config.adminPubkeys?.includes(event.pubkey) || false
|
||||
|
||||
// For custom content filters, check if event matches any active filter
|
||||
if (config.feedType === 'custom' && config.contentFilters) {
|
||||
// For custom content filters or specific feed types with filters, check if event matches any active filter
|
||||
if ((config.feedType === 'custom' || config.feedType === 'rideshare') && config.contentFilters) {
|
||||
console.log('FeedService: Using custom filters, count:', config.contentFilters.length)
|
||||
const result = config.contentFilters.some(filter => {
|
||||
console.log('FeedService: Checking filter:', filter.id, 'kinds:', filter.kinds, 'filterByAuthor:', filter.filterByAuthor)
|
||||
|
|
@ -347,26 +413,34 @@ export class FeedService extends BaseService {
|
|||
if (isAdminPost) return false
|
||||
}
|
||||
|
||||
// Apply keyword filtering if specified
|
||||
if (filter.keywords && filter.keywords.length > 0) {
|
||||
const content = event.content.toLowerCase()
|
||||
const hasMatchingKeyword = filter.keywords.some(keyword =>
|
||||
content.includes(keyword.toLowerCase())
|
||||
)
|
||||
if (!hasMatchingKeyword) {
|
||||
console.log('FeedService: No matching keywords found')
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Apply keyword and tag filtering (OR logic when both are specified)
|
||||
const hasKeywordFilter = filter.keywords && filter.keywords.length > 0
|
||||
const hasTagFilter = filter.tags && filter.tags.length > 0
|
||||
|
||||
// Apply tag filtering if specified (check if event has any matching tags)
|
||||
if (filter.tags && filter.tags.length > 0) {
|
||||
const eventTags = event.tags?.filter(tag => tag[0] === 't').map(tag => tag[1]) || []
|
||||
const hasMatchingTag = filter.tags.some(filterTag =>
|
||||
eventTags.includes(filterTag)
|
||||
)
|
||||
if (!hasMatchingTag) {
|
||||
console.log('FeedService: No matching tags found')
|
||||
if (hasKeywordFilter || hasTagFilter) {
|
||||
let keywordMatch = false
|
||||
let tagMatch = false
|
||||
|
||||
// Check keywords
|
||||
if (hasKeywordFilter) {
|
||||
const content = event.content.toLowerCase()
|
||||
keywordMatch = filter.keywords!.some(keyword =>
|
||||
content.includes(keyword.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
// Check tags
|
||||
if (hasTagFilter) {
|
||||
const eventTags = event.tags?.filter(tag => tag[0] === 't').map(tag => tag[1]) || []
|
||||
tagMatch = filter.tags!.some(filterTag =>
|
||||
eventTags.includes(filterTag)
|
||||
)
|
||||
}
|
||||
|
||||
// Must match at least one: keywords OR tags
|
||||
const hasMatch = (hasKeywordFilter && keywordMatch) || (hasTagFilter && tagMatch)
|
||||
if (!hasMatch) {
|
||||
console.log('FeedService: No matching keywords or tags found')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -378,18 +452,14 @@ export class FeedService extends BaseService {
|
|||
return result
|
||||
}
|
||||
|
||||
// Legacy feed type handling
|
||||
// Feed type handling
|
||||
switch (config.feedType) {
|
||||
case 'announcements':
|
||||
return isAdminPost
|
||||
case 'general':
|
||||
return !isAdminPost
|
||||
case 'events':
|
||||
// Events feed could show all posts for now, or implement event-specific filtering
|
||||
return true
|
||||
case 'mentions':
|
||||
// TODO: Implement mention detection if needed
|
||||
return true
|
||||
case 'rideshare':
|
||||
// Rideshare filtering handled via content filters above
|
||||
// If we reach here, contentFilters weren't provided - show nothing
|
||||
return false
|
||||
case 'all':
|
||||
default:
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -41,9 +41,6 @@ export class ReactionService extends BaseService {
|
|||
private currentSubscription: string | null = null
|
||||
private currentUnsubscribe: (() => void) | null = null
|
||||
|
||||
// Track deletion subscription separately
|
||||
private deletionUnsubscribe: (() => void) | null = null
|
||||
|
||||
// Track which events we're monitoring
|
||||
private monitoredEvents = new Set<string>()
|
||||
|
||||
|
|
@ -60,50 +57,8 @@ export class ReactionService extends BaseService {
|
|||
throw new Error('RelayHub service not available')
|
||||
}
|
||||
|
||||
// Start monitoring deletion events globally
|
||||
await this.startDeletionMonitoring()
|
||||
|
||||
console.log('ReactionService: Initialization complete')
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring deletion events globally
|
||||
*/
|
||||
private async startDeletionMonitoring(): Promise<void> {
|
||||
try {
|
||||
if (!this.relayHub?.isConnected) {
|
||||
await this.relayHub?.connect()
|
||||
}
|
||||
|
||||
const subscriptionId = `reaction-deletions-${Date.now()}`
|
||||
|
||||
// Subscribe to ALL deletion events for reactions
|
||||
const filter = {
|
||||
kinds: [5], // Deletion requests
|
||||
'#k': ['7'], // Only for reaction events
|
||||
since: Math.floor(Date.now() / 1000) - 86400, // Last 24 hours
|
||||
limit: 1000
|
||||
}
|
||||
|
||||
console.log('ReactionService: Starting global deletion monitoring')
|
||||
|
||||
const unsubscribe = this.relayHub.subscribe({
|
||||
id: subscriptionId,
|
||||
filters: [filter],
|
||||
onEvent: (event: NostrEvent) => {
|
||||
this.handleDeletionEvent(event)
|
||||
},
|
||||
onEose: () => {
|
||||
console.log('ReactionService: Initial deletion events loaded')
|
||||
}
|
||||
})
|
||||
|
||||
// Store subscription ID if needed for tracking
|
||||
this.deletionUnsubscribe = unsubscribe
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start deletion monitoring:', error)
|
||||
}
|
||||
// Deletion monitoring is now handled by FeedService's consolidated subscription
|
||||
console.log('ReactionService: Initialization complete (deletion monitoring handled by FeedService)')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -146,34 +101,24 @@ export class ReactionService extends BaseService {
|
|||
|
||||
const subscriptionId = `reactions-${Date.now()}`
|
||||
|
||||
// Subscribe to reactions (kind 7) and deletions (kind 5) for these events
|
||||
// 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
|
||||
},
|
||||
{
|
||||
kinds: [5], // Deletion requests for ALL users
|
||||
'#k': ['7'], // Only deletions of reaction events (kind 7)
|
||||
limit: 500
|
||||
}
|
||||
]
|
||||
|
||||
console.log('ReactionService: Creating reaction subscription', filters)
|
||||
|
||||
const unsubscribe = this.relayHub.subscribe({
|
||||
id: subscriptionId,
|
||||
filters: filters,
|
||||
onEvent: (event: NostrEvent) => {
|
||||
if (event.kind === 7) {
|
||||
this.handleReactionEvent(event)
|
||||
} else if (event.kind === 5) {
|
||||
this.handleDeletionEvent(event)
|
||||
}
|
||||
this.handleReactionEvent(event)
|
||||
},
|
||||
onEose: () => {
|
||||
console.log(`Reaction subscription ${subscriptionId} complete`)
|
||||
console.log(`ReactionService: Subscription ${subscriptionId} ready`)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -190,8 +135,9 @@ export class ReactionService extends BaseService {
|
|||
|
||||
/**
|
||||
* Handle incoming reaction event
|
||||
* Made public so FeedService can route kind 7 events to this service
|
||||
*/
|
||||
private handleReactionEvent(event: NostrEvent): void {
|
||||
public handleReactionEvent(event: NostrEvent): void {
|
||||
try {
|
||||
// Find the event being reacted to
|
||||
const eTag = event.tags.find(tag => tag[0] === 'e')
|
||||
|
|
@ -235,7 +181,6 @@ export class ReactionService extends BaseService {
|
|||
|
||||
if (previousReactionIndex >= 0) {
|
||||
// Replace the old reaction with the new one
|
||||
console.log(`ReactionService: Replacing previous reaction from ${reaction.pubkey.slice(0, 8)}...`)
|
||||
eventReactions.reactions[previousReactionIndex] = reaction
|
||||
} else {
|
||||
// Add as new reaction
|
||||
|
|
@ -245,17 +190,16 @@ export class ReactionService extends BaseService {
|
|||
// Recalculate counts and user state
|
||||
this.recalculateEventReactions(eventId)
|
||||
|
||||
console.log(`ReactionService: Added/updated reaction ${content} to event ${eventId.slice(0, 8)}...`)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to handle reaction event:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle deletion event
|
||||
* 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
|
||||
*/
|
||||
private handleDeletionEvent(event: NostrEvent): void {
|
||||
public handleDeletionEvent(event: NostrEvent): void {
|
||||
try {
|
||||
// Process each deleted event
|
||||
const eTags = event.tags.filter(tag => tag[0] === 'e')
|
||||
|
|
@ -281,9 +225,6 @@ export class ReactionService extends BaseService {
|
|||
eventReactions.reactions.splice(reactionIndex, 1)
|
||||
// Recalculate counts for this event
|
||||
this.recalculateEventReactions(eventId)
|
||||
console.log(`ReactionService: Removed deleted reaction ${deletedEventId.slice(0, 8)}... from ${deletionAuthor.slice(0, 8)}...`)
|
||||
} else {
|
||||
console.log(`ReactionService: Ignoring deletion request from ${deletionAuthor.slice(0, 8)}... for reaction by ${reaction.pubkey.slice(0, 8)}...`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -393,18 +334,12 @@ export class ReactionService extends BaseService {
|
|||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
console.log('ReactionService: Creating like reaction:', eventTemplate)
|
||||
|
||||
// Sign the event
|
||||
const privkeyBytes = this.hexToUint8Array(userPrivkey)
|
||||
const signedEvent = finalizeEvent(eventTemplate, privkeyBytes)
|
||||
|
||||
console.log('ReactionService: Publishing like reaction:', signedEvent)
|
||||
|
||||
// Publish the reaction
|
||||
const result = await this.relayHub.publishEvent(signedEvent)
|
||||
|
||||
console.log(`ReactionService: Like published to ${result.success}/${result.total} relays`)
|
||||
await this.relayHub.publishEvent(signedEvent)
|
||||
|
||||
// Optimistically update local state
|
||||
this.handleReactionEvent(signedEvent)
|
||||
|
|
@ -438,6 +373,7 @@ export class ReactionService extends BaseService {
|
|||
|
||||
// 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')
|
||||
}
|
||||
|
|
@ -456,14 +392,10 @@ export class ReactionService extends BaseService {
|
|||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
console.log('ReactionService: Creating deletion event for reaction:', eventReactions.userReactionId)
|
||||
|
||||
// Sign the event
|
||||
const privkeyBytes = this.hexToUint8Array(userPrivkey)
|
||||
const signedEvent = finalizeEvent(eventTemplate, privkeyBytes)
|
||||
|
||||
console.log('ReactionService: Publishing deletion event:', signedEvent)
|
||||
|
||||
// Publish the deletion
|
||||
const result = await this.relayHub.publishEvent(signedEvent)
|
||||
|
||||
|
|
@ -527,9 +459,7 @@ export class ReactionService extends BaseService {
|
|||
if (this.currentUnsubscribe) {
|
||||
this.currentUnsubscribe()
|
||||
}
|
||||
if (this.deletionUnsubscribe) {
|
||||
this.deletionUnsubscribe()
|
||||
}
|
||||
// deletionUnsubscribe is no longer used - deletions handled by FeedService
|
||||
this._eventReactions.clear()
|
||||
this.monitoredEvents.clear()
|
||||
this.deletedReactions.clear()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue