Squash merge nostrfeed-ui into main

This commit is contained in:
padreug 2025-10-21 21:31:25 +02:00
parent 5063a3e121
commit cc5e0dbef6
10 changed files with 379 additions and 258 deletions

View file

@ -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