Implements a feature to mark scheduled events as complete, replacing the checkbox with a button for improved UX. This commit enhances the Scheduled Events functionality by allowing users to mark events as complete. It also includes: - Replaces the checkbox with a "Mark Complete" button for better usability. - Adds logging for debugging purposes during event completion toggling. - Routes completion events (kind 31925) to the ScheduledEventService. - Optimistically updates the local state after publishing completion events.
672 lines
No EOL
22 KiB
TypeScript
672 lines
No EOL
22 KiB
TypeScript
import { ref, computed } from 'vue'
|
|
import { BaseService } from '@/core/base/BaseService'
|
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
|
import { eventBus } from '@/core/event-bus'
|
|
import type { Event as NostrEvent, Filter } from 'nostr-tools'
|
|
|
|
export interface FeedPost {
|
|
id: string
|
|
pubkey: string
|
|
content: string
|
|
created_at: number
|
|
kind: number
|
|
tags: string[][]
|
|
mentions: string[]
|
|
isReply: boolean
|
|
replyTo?: string // Direct parent ID
|
|
rootId?: string // Thread root ID
|
|
replies?: FeedPost[] // Child replies
|
|
depth?: number // Depth in reply tree (0 for root posts)
|
|
}
|
|
|
|
export interface ContentFilter {
|
|
id: string
|
|
label: string
|
|
kinds: number[]
|
|
description: string
|
|
requiresAuth?: boolean
|
|
filterByAuthor?: 'admin' | 'exclude-admin' | 'none'
|
|
tags?: string[] // NIP-12 tags to filter by
|
|
keywords?: string[] // Content keywords to search for
|
|
}
|
|
|
|
export interface FeedConfig {
|
|
feedType: 'all' | 'announcements' | 'rideshare' | 'custom'
|
|
maxPosts?: number
|
|
adminPubkeys?: string[]
|
|
contentFilters?: ContentFilter[]
|
|
}
|
|
|
|
export class FeedService extends BaseService {
|
|
protected readonly metadata = {
|
|
name: 'FeedService',
|
|
version: '1.0.0',
|
|
dependencies: []
|
|
}
|
|
|
|
protected relayHub: any = null
|
|
protected visibilityService: any = null
|
|
protected reactionService: any = null
|
|
protected scheduledEventService: any = null
|
|
|
|
// Event ID tracking for deduplication
|
|
private seenEventIds = new Set<string>()
|
|
|
|
// Feed state
|
|
private _posts = ref<FeedPost[]>([])
|
|
private _isLoading = ref(false)
|
|
private _error = ref<string | null>(null)
|
|
|
|
// Current subscription state
|
|
private currentSubscription: string | null = null
|
|
private currentUnsubscribe: (() => void) | null = null
|
|
private currentConfig: FeedConfig | null = null
|
|
|
|
// Public reactive state
|
|
public readonly posts = computed(() => this._posts.value)
|
|
public readonly isLoading = computed(() => this._isLoading.value)
|
|
public readonly error = computed(() => this._error.value)
|
|
|
|
protected async onInitialize(): Promise<void> {
|
|
console.log('FeedService: Starting initialization...')
|
|
|
|
this.relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
|
this.visibilityService = injectService(SERVICE_TOKENS.VISIBILITY_SERVICE)
|
|
this.reactionService = injectService(SERVICE_TOKENS.REACTION_SERVICE)
|
|
this.scheduledEventService = injectService(SERVICE_TOKENS.SCHEDULED_EVENT_SERVICE)
|
|
|
|
console.log('FeedService: RelayHub injected:', !!this.relayHub)
|
|
console.log('FeedService: VisibilityService injected:', !!this.visibilityService)
|
|
console.log('FeedService: ReactionService injected:', !!this.reactionService)
|
|
console.log('FeedService: ScheduledEventService injected:', !!this.scheduledEventService)
|
|
|
|
if (!this.relayHub) {
|
|
throw new Error('RelayHub service not available')
|
|
}
|
|
|
|
// Register with visibility service for proper connection management
|
|
if (this.visibilityService) {
|
|
this.visibilityService.registerService(
|
|
'FeedService',
|
|
this.onResume.bind(this),
|
|
this.onPause.bind(this)
|
|
)
|
|
}
|
|
|
|
console.log('FeedService: Initialization complete')
|
|
}
|
|
|
|
/**
|
|
* Subscribe to feed with deduplication
|
|
*/
|
|
async subscribeFeed(config: FeedConfig): Promise<void> {
|
|
// If already subscribed with same config, don't resubscribe
|
|
if (this.currentSubscription && this.currentConfig &&
|
|
JSON.stringify(this.currentConfig) === JSON.stringify(config)) {
|
|
return
|
|
}
|
|
|
|
// Unsubscribe from previous feed if exists
|
|
await this.unsubscribeFeed()
|
|
|
|
this.currentConfig = config
|
|
this._isLoading.value = true
|
|
this._error.value = null
|
|
|
|
try {
|
|
// Check if RelayHub is connected
|
|
if (!this.relayHub) {
|
|
throw new Error('RelayHub not available')
|
|
}
|
|
|
|
if (!this.relayHub.isConnected) {
|
|
console.log('RelayHub not connected, attempting to connect...')
|
|
await this.relayHub.connect()
|
|
}
|
|
|
|
if (!this.relayHub.isConnected) {
|
|
throw new Error('Unable to connect to relays')
|
|
}
|
|
|
|
// Create subscription ID
|
|
const subscriptionId = `feed-service-${config.feedType}-${Date.now()}`
|
|
|
|
// Create filters based on feed type and content filters
|
|
const filters: Filter[] = []
|
|
|
|
if (config.feedType === 'custom' && config.contentFilters) {
|
|
// Use custom content filters
|
|
// Using custom content filters
|
|
for (const contentFilter of config.contentFilters) {
|
|
const filter: Filter = {
|
|
kinds: contentFilter.kinds,
|
|
limit: Math.floor((config.maxPosts || 50) / config.contentFilters.length)
|
|
}
|
|
|
|
// Apply author filtering if specified
|
|
if (contentFilter.filterByAuthor === 'admin') {
|
|
if (config.adminPubkeys?.length) {
|
|
filter.authors = config.adminPubkeys
|
|
// Using admin authors for filtering
|
|
} else {
|
|
// No admin pubkeys configured - include all authors for admin filters
|
|
// No admin pubkeys configured - include all authors
|
|
}
|
|
} else if (contentFilter.filterByAuthor === 'exclude-admin' && config.adminPubkeys?.length) {
|
|
// Note: Nostr doesn't support negative filters natively,
|
|
// we'll filter these out in post-processing
|
|
// Will exclude admin in post-processing
|
|
}
|
|
|
|
// Apply tag filtering if specified (NIP-12)
|
|
if (contentFilter.tags && contentFilter.tags.length > 0) {
|
|
filter['#t'] = contentFilter.tags
|
|
}
|
|
|
|
filters.push(filter)
|
|
}
|
|
} else {
|
|
// Handle default feed types (all, announcements, general, etc.)
|
|
const filter: Filter = {
|
|
kinds: [1], // Text notes
|
|
limit: config.maxPosts || 50
|
|
}
|
|
|
|
// Apply feed-specific filtering
|
|
switch (config.feedType) {
|
|
case 'announcements':
|
|
if (config.adminPubkeys?.length) {
|
|
filter.authors = config.adminPubkeys
|
|
}
|
|
break
|
|
case 'rideshare':
|
|
// Rideshare posts handled via content filters
|
|
break
|
|
case 'all':
|
|
default:
|
|
// All posts - no specific filtering
|
|
break
|
|
}
|
|
|
|
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)
|
|
})
|
|
|
|
// Add scheduled events (kind 31922) and RSVPs (kind 31925)
|
|
filters.push({
|
|
kinds: [31922, 31925], // Calendar events and RSVPs
|
|
limit: 200
|
|
})
|
|
|
|
console.log(`Creating feed subscription for ${config.feedType} with filters:`, filters)
|
|
|
|
// Subscribe to all events (posts, reactions, deletions) with deduplication
|
|
const unsubscribe = this.relayHub.subscribe({
|
|
id: subscriptionId,
|
|
filters: filters,
|
|
onEvent: (event: NostrEvent) => {
|
|
this.handleNewEvent(event, config)
|
|
},
|
|
onEose: () => {
|
|
console.log(`Feed subscription ${subscriptionId} end of stored events`)
|
|
console.log('FeedService: Setting isLoading to false')
|
|
this._isLoading.value = false
|
|
console.log('FeedService: isLoading is now:', this._isLoading.value)
|
|
},
|
|
onClose: () => {
|
|
console.log(`Feed subscription ${subscriptionId} closed`)
|
|
}
|
|
})
|
|
|
|
// Store the subscription info for later cleanup
|
|
this.currentSubscription = subscriptionId
|
|
this.currentUnsubscribe = unsubscribe
|
|
|
|
// Set a timeout to stop loading if no EOSE is received
|
|
setTimeout(() => {
|
|
console.log(`Feed subscription ${subscriptionId} timeout check: isLoading=${this._isLoading.value}, currentSub=${this.currentSubscription}`)
|
|
if (this._isLoading.value && this.currentSubscription === subscriptionId) {
|
|
console.log(`Feed subscription ${subscriptionId} timeout, stopping loading`)
|
|
this._isLoading.value = false
|
|
}
|
|
}, 5000) // 5 second timeout (reduced for testing)
|
|
|
|
} catch (err) {
|
|
console.error('Failed to subscribe to feed:', err)
|
|
this._error.value = err instanceof Error ? err.message : 'Failed to subscribe to feed'
|
|
this._isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle new event with robust deduplication
|
|
*/
|
|
private handleNewEvent(event: NostrEvent, config: FeedConfig): void {
|
|
// 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
|
|
}
|
|
|
|
// Route scheduled events (kind 31922) to ScheduledEventService
|
|
if (event.kind === 31922) {
|
|
if (this.scheduledEventService) {
|
|
this.scheduledEventService.handleScheduledEvent(event)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Route RSVP/completion events (kind 31925) to ScheduledEventService
|
|
if (event.kind === 31925) {
|
|
console.log('🔀 FeedService: Routing kind 31925 (completion) to ScheduledEventService')
|
|
if (this.scheduledEventService) {
|
|
this.scheduledEventService.handleCompletionEvent(event)
|
|
} else {
|
|
console.warn('⚠️ FeedService: ScheduledEventService not available')
|
|
}
|
|
return
|
|
}
|
|
|
|
// Skip if event already seen (for posts only, kind 1)
|
|
if (this.seenEventIds.has(event.id)) {
|
|
return
|
|
}
|
|
|
|
// Add to seen events
|
|
this.seenEventIds.add(event.id)
|
|
|
|
// Check if event should be included based on feed type
|
|
if (!this.shouldIncludeEvent(event, config)) {
|
|
return
|
|
}
|
|
|
|
// Extract reply information according to NIP-10
|
|
let rootId: string | undefined
|
|
let replyTo: string | undefined
|
|
let isReply = false
|
|
|
|
// Look for marked e-tags first (preferred method)
|
|
const markedRootTag = event.tags?.find((tag: string[]) => tag[0] === 'e' && tag[3] === 'root')
|
|
const markedReplyTag = event.tags?.find((tag: string[]) => tag[0] === 'e' && tag[3] === 'reply')
|
|
|
|
if (markedRootTag || markedReplyTag) {
|
|
// Using marked tags (NIP-10 preferred method)
|
|
rootId = markedRootTag?.[1]
|
|
replyTo = markedReplyTag?.[1] || markedRootTag?.[1] // Direct reply to root if no reply tag
|
|
isReply = true
|
|
} else {
|
|
// Fallback to positional tags (deprecated but still in use)
|
|
const eTags = event.tags?.filter((tag: string[]) => tag[0] === 'e') || []
|
|
if (eTags.length === 1) {
|
|
// Single e-tag means this is a direct reply
|
|
replyTo = eTags[0][1]
|
|
rootId = eTags[0][1]
|
|
isReply = true
|
|
} else if (eTags.length >= 2) {
|
|
// Multiple e-tags: first is root, last is direct reply
|
|
rootId = eTags[0][1]
|
|
replyTo = eTags[eTags.length - 1][1]
|
|
isReply = true
|
|
}
|
|
}
|
|
|
|
// Transform to FeedPost
|
|
const post: FeedPost = {
|
|
id: event.id,
|
|
pubkey: event.pubkey,
|
|
content: event.content,
|
|
created_at: event.created_at,
|
|
kind: event.kind,
|
|
tags: event.tags || [],
|
|
mentions: event.tags?.filter((tag: string[]) => tag[0] === 'p').map((tag: string[]) => tag[1]) || [],
|
|
isReply,
|
|
replyTo,
|
|
rootId,
|
|
replies: [],
|
|
depth: 0
|
|
}
|
|
|
|
// Add to posts (newest first)
|
|
this._posts.value = [post, ...this._posts.value]
|
|
|
|
// Limit array size and clean up seen IDs
|
|
const maxPosts = config.maxPosts || 100
|
|
if (this._posts.value.length > maxPosts) {
|
|
const removedPosts = this._posts.value.slice(maxPosts)
|
|
this._posts.value = this._posts.value.slice(0, maxPosts)
|
|
|
|
// Clean up seen IDs for removed posts
|
|
removedPosts.forEach(post => {
|
|
this.seenEventIds.delete(post.id)
|
|
})
|
|
}
|
|
|
|
// Emit event for other modules
|
|
eventBus.emit('nostr-feed:new-post', {
|
|
event,
|
|
feedType: config.feedType
|
|
}, '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) 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 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)
|
|
|
|
// Check if event kind matches
|
|
if (!filter.kinds.includes(event.kind)) {
|
|
console.log('FeedService: Kind mismatch, event kind:', event.kind, 'filter kinds:', filter.kinds)
|
|
return false
|
|
}
|
|
|
|
// Apply author filtering
|
|
if (filter.filterByAuthor === 'admin') {
|
|
console.log('FeedService: Admin filter, isAdminPost:', isAdminPost)
|
|
if (!isAdminPost) return false
|
|
} else if (filter.filterByAuthor === 'exclude-admin') {
|
|
console.log('FeedService: Exclude admin filter, isAdminPost:', isAdminPost)
|
|
if (isAdminPost) 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
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
console.log('FeedService: Filter passed all checks')
|
|
return true
|
|
})
|
|
console.log('FeedService: Custom filter result:', result)
|
|
return result
|
|
}
|
|
|
|
// Feed type handling
|
|
switch (config.feedType) {
|
|
case 'announcements':
|
|
return isAdminPost
|
|
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
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe from current feed
|
|
*/
|
|
async unsubscribeFeed(): Promise<void> {
|
|
if (this.currentUnsubscribe) {
|
|
this.currentUnsubscribe()
|
|
this.currentSubscription = null
|
|
this.currentUnsubscribe = null
|
|
this.currentConfig = null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh feed (clear and reload)
|
|
*/
|
|
async refreshFeed(): Promise<void> {
|
|
if (!this.currentConfig) return
|
|
|
|
// Clear existing state
|
|
this._posts.value = []
|
|
this.seenEventIds.clear()
|
|
|
|
// Resubscribe
|
|
await this.subscribeFeed(this.currentConfig)
|
|
}
|
|
|
|
/**
|
|
* Build threaded reply structure from flat posts
|
|
*/
|
|
buildThreadedPosts(posts: FeedPost[]): FeedPost[] {
|
|
console.log('FeedService.buildThreadedPosts: Input posts count:', posts.length)
|
|
|
|
// Create a map for quick lookup
|
|
const postMap = new Map<string, FeedPost>()
|
|
posts.forEach(post => {
|
|
postMap.set(post.id, { ...post, replies: [], depth: 0 })
|
|
})
|
|
|
|
// Build the tree structure
|
|
const rootPosts: FeedPost[] = []
|
|
|
|
posts.forEach(post => {
|
|
const currentPost = postMap.get(post.id)!
|
|
|
|
if (post.isReply && post.replyTo) {
|
|
// This is a reply, attach it to its parent if parent exists
|
|
const parentPost = postMap.get(post.replyTo)
|
|
if (parentPost) {
|
|
currentPost.depth = (parentPost.depth || 0) + 1
|
|
parentPost.replies = parentPost.replies || []
|
|
parentPost.replies.push(currentPost)
|
|
} else {
|
|
// Parent not found, treat as root post
|
|
rootPosts.push(currentPost)
|
|
}
|
|
} else {
|
|
// This is a root post
|
|
rootPosts.push(currentPost)
|
|
}
|
|
})
|
|
|
|
// Sort posts using like count and timestamp
|
|
this.sortPostsByLikesAndTime(rootPosts)
|
|
|
|
// Sort all reply threads recursively
|
|
rootPosts.forEach(post => this.sortRepliesRecursively(post))
|
|
|
|
return rootPosts
|
|
}
|
|
|
|
/**
|
|
* Sort posts by likes first, then by time (newest first)
|
|
*/
|
|
private sortPostsByLikesAndTime(posts: FeedPost[]): void {
|
|
posts.sort((a, b) => {
|
|
// Get like counts from reaction service if available
|
|
const aLikes = this.getLikeCount(a.id)
|
|
const bLikes = this.getLikeCount(b.id)
|
|
|
|
// Sort by likes first (descending)
|
|
if (aLikes !== bLikes) {
|
|
return bLikes - aLikes
|
|
}
|
|
|
|
// If likes are equal, sort by time (newest first)
|
|
return b.created_at - a.created_at
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Recursively sort replies within each thread
|
|
*/
|
|
private sortRepliesRecursively(post: FeedPost): void {
|
|
if (post.replies && post.replies.length > 0) {
|
|
// Sort replies by likes first, then time
|
|
this.sortPostsByLikesAndTime(post.replies)
|
|
|
|
// Recursively sort nested replies
|
|
post.replies.forEach(reply => this.sortRepliesRecursively(reply))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get like count for a post from ReactionService
|
|
*/
|
|
private getLikeCount(postId: string): number {
|
|
try {
|
|
if (this.reactionService && typeof this.reactionService.getEventReactions === 'function') {
|
|
const reactions = this.reactionService.getEventReactions(postId)
|
|
return reactions?.likes || 0
|
|
}
|
|
} catch (error) {
|
|
// Silently fail if reaction service is not available
|
|
console.debug('FeedService: Could not get like count for post', postId, error)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
/**
|
|
* Get filtered posts for specific feed type
|
|
*/
|
|
getFilteredPosts(config: FeedConfig): FeedPost[] {
|
|
console.log('FeedService: getFilteredPosts called, total posts:', this._posts.value.length, 'config:', config.feedType)
|
|
const filtered = this._posts.value
|
|
.filter(post => {
|
|
const shouldInclude = this.shouldIncludeEvent({
|
|
id: post.id,
|
|
pubkey: post.pubkey,
|
|
content: post.content,
|
|
created_at: post.created_at,
|
|
kind: post.kind,
|
|
tags: post.tags
|
|
} as NostrEvent, config)
|
|
if (!shouldInclude) {
|
|
console.log('FeedService: Post filtered out in getFilteredPosts:', post.id)
|
|
}
|
|
return shouldInclude
|
|
})
|
|
.sort((a, b) => b.created_at - a.created_at)
|
|
.slice(0, config.maxPosts || 100)
|
|
|
|
console.log('FeedService: getFilteredPosts returning', filtered.length, 'posts')
|
|
return filtered
|
|
}
|
|
|
|
/**
|
|
* Visibility service callbacks
|
|
*/
|
|
private async onResume(): Promise<void> {
|
|
console.log('FeedService: App resumed, checking connections')
|
|
|
|
// Check if we need to reconnect
|
|
if (this.currentConfig && this.relayHub) {
|
|
const isConnected = this.relayHub.isConnected.value
|
|
console.log('FeedService: RelayHub connection status:', isConnected)
|
|
if (!isConnected) {
|
|
console.log('FeedService: Reconnecting after resume')
|
|
await this.subscribeFeed(this.currentConfig)
|
|
}
|
|
}
|
|
}
|
|
|
|
private onPause(): void {
|
|
console.log('FeedService: App paused, maintaining state')
|
|
// Don't clear state, just log for debugging
|
|
}
|
|
|
|
/**
|
|
* Cleanup
|
|
*/
|
|
protected async onDestroy(): Promise<void> {
|
|
await this.unsubscribeFeed()
|
|
this.seenEventIds.clear()
|
|
this._posts.value = []
|
|
}
|
|
} |