Release v0.3.1
- Feed bounded context with DDD implementation (Phases 1-5) - Domain event handlers for cross-context coordination - Fix Blossom media upload setting persistence - Fix wallet connection persistence on page reload - New branding assets and icons - Vitest testing infrastructure with 151 domain model tests - Help page scaffolding - Keyboard navigation provider 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
257
src/application/handlers/ContentEventHandlers.ts
Normal file
257
src/application/handlers/ContentEventHandlers.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import {
|
||||
EventBookmarked,
|
||||
EventUnbookmarked,
|
||||
BookmarkListPublished,
|
||||
NotePinned,
|
||||
NoteUnpinned,
|
||||
PinsLimitExceeded,
|
||||
PinListPublished,
|
||||
ReactionAdded,
|
||||
ContentReposted
|
||||
} from '@/domain/content'
|
||||
import { EventHandler, eventDispatcher } from '@/domain/shared'
|
||||
|
||||
/**
|
||||
* Handlers for content domain events
|
||||
*
|
||||
* These handlers coordinate cross-context updates when content events occur.
|
||||
* They enable real-time UI updates and cross-context coordination.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Callback for updating reaction counts in UI
|
||||
*/
|
||||
export type UpdateReactionCountCallback = (eventId: string, emoji: string, delta: number) => void
|
||||
|
||||
/**
|
||||
* Callback for updating repost counts in UI
|
||||
*/
|
||||
export type UpdateRepostCountCallback = (eventId: string, delta: number) => void
|
||||
|
||||
/**
|
||||
* Callback for creating notifications
|
||||
*/
|
||||
export type CreateNotificationCallback = (
|
||||
type: 'reaction' | 'repost' | 'mention' | 'reply',
|
||||
actorPubkey: string,
|
||||
targetEventId: string
|
||||
) => void
|
||||
|
||||
/**
|
||||
* Callback for showing toast messages
|
||||
*/
|
||||
export type ShowToastCallback = (message: string, type: 'info' | 'warning' | 'error') => void
|
||||
|
||||
/**
|
||||
* Callback for updating profile pinned notes
|
||||
*/
|
||||
export type UpdateProfilePinsCallback = (pubkey: string) => void
|
||||
|
||||
/**
|
||||
* Service callbacks for cross-context coordination
|
||||
*/
|
||||
export interface ContentHandlerCallbacks {
|
||||
onUpdateReactionCount?: UpdateReactionCountCallback
|
||||
onUpdateRepostCount?: UpdateRepostCountCallback
|
||||
onCreateNotification?: CreateNotificationCallback
|
||||
onShowToast?: ShowToastCallback
|
||||
onUpdateProfilePins?: UpdateProfilePinsCallback
|
||||
}
|
||||
|
||||
let callbacks: ContentHandlerCallbacks = {}
|
||||
|
||||
/**
|
||||
* Set the callbacks for cross-context coordination
|
||||
* Call this during provider initialization
|
||||
*/
|
||||
export function setContentHandlerCallbacks(newCallbacks: ContentHandlerCallbacks): void {
|
||||
callbacks = { ...callbacks, ...newCallbacks }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all callbacks (for cleanup/testing)
|
||||
*/
|
||||
export function clearContentHandlerCallbacks(): void {
|
||||
callbacks = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for event bookmarked
|
||||
* Can be used to:
|
||||
* - Update bookmark count displays
|
||||
* - Prefetch bookmarked content for offline access
|
||||
*/
|
||||
export const handleEventBookmarked: EventHandler<EventBookmarked> = async (event) => {
|
||||
console.debug('[ContentEventHandler] Event bookmarked:', {
|
||||
actor: event.actor.formatted,
|
||||
bookmarkedEventId: event.bookmarkedEventId,
|
||||
type: event.bookmarkType
|
||||
})
|
||||
|
||||
// Future: Trigger prefetch of bookmarked content
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for event unbookmarked
|
||||
*/
|
||||
export const handleEventUnbookmarked: EventHandler<EventUnbookmarked> = async (event) => {
|
||||
console.debug('[ContentEventHandler] Event unbookmarked:', {
|
||||
actor: event.actor.formatted,
|
||||
unbookmarkedEventId: event.unbookmarkedEventId
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for bookmark list published
|
||||
*/
|
||||
export const handleBookmarkListPublished: EventHandler<BookmarkListPublished> = async (event) => {
|
||||
console.debug('[ContentEventHandler] Bookmark list published:', {
|
||||
owner: event.owner.formatted,
|
||||
bookmarkCount: event.bookmarkCount
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for note pinned
|
||||
* Coordinates with:
|
||||
* - Profile context: Update pinned notes display
|
||||
* - Cache context: Ensure pinned content is cached
|
||||
*/
|
||||
export const handleNotePinned: EventHandler<NotePinned> = async (event) => {
|
||||
console.debug('[ContentEventHandler] Note pinned:', {
|
||||
actor: event.actor.formatted,
|
||||
pinnedEventId: event.pinnedEventId.hex
|
||||
})
|
||||
|
||||
// Update profile display to show new pinned note
|
||||
if (callbacks.onUpdateProfilePins) {
|
||||
callbacks.onUpdateProfilePins(event.actor.hex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for note unpinned
|
||||
* Coordinates with:
|
||||
* - Profile context: Update pinned notes display
|
||||
*/
|
||||
export const handleNoteUnpinned: EventHandler<NoteUnpinned> = async (event) => {
|
||||
console.debug('[ContentEventHandler] Note unpinned:', {
|
||||
actor: event.actor.formatted,
|
||||
unpinnedEventId: event.unpinnedEventId
|
||||
})
|
||||
|
||||
// Update profile display to remove unpinned note
|
||||
if (callbacks.onUpdateProfilePins) {
|
||||
callbacks.onUpdateProfilePins(event.actor.hex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for pins limit exceeded
|
||||
* Coordinates with:
|
||||
* - UI context: Show toast notification about removed pins
|
||||
*/
|
||||
export const handlePinsLimitExceeded: EventHandler<PinsLimitExceeded> = async (event) => {
|
||||
console.debug('[ContentEventHandler] Pins limit exceeded:', {
|
||||
actor: event.actor.formatted,
|
||||
removedCount: event.removedEventIds.length
|
||||
})
|
||||
|
||||
// Show toast notification about removed pins
|
||||
if (callbacks.onShowToast) {
|
||||
callbacks.onShowToast(
|
||||
`Pin limit reached. ${event.removedEventIds.length} older pin(s) were removed.`,
|
||||
'warning'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for pin list published
|
||||
*/
|
||||
export const handlePinListPublished: EventHandler<PinListPublished> = async (event) => {
|
||||
console.debug('[ContentEventHandler] Pin list published:', {
|
||||
owner: event.owner.formatted,
|
||||
pinCount: event.pinCount
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for reaction added
|
||||
* Coordinates with:
|
||||
* - UI context: Update reaction counts in real-time
|
||||
* - Notification context: Create notification for content author
|
||||
*/
|
||||
export const handleReactionAdded: EventHandler<ReactionAdded> = async (event) => {
|
||||
console.debug('[ContentEventHandler] Reaction added:', {
|
||||
actor: event.actor.formatted,
|
||||
targetEventId: event.targetEventId.hex,
|
||||
targetAuthor: event.targetAuthor.formatted,
|
||||
emoji: event.emoji,
|
||||
isLike: event.isLike
|
||||
})
|
||||
|
||||
// Update reaction count in UI
|
||||
if (callbacks.onUpdateReactionCount) {
|
||||
callbacks.onUpdateReactionCount(event.targetEventId.hex, event.emoji, 1)
|
||||
}
|
||||
|
||||
// Create notification for the content author (if not self)
|
||||
if (callbacks.onCreateNotification && event.actor.hex !== event.targetAuthor.hex) {
|
||||
callbacks.onCreateNotification('reaction', event.actor.hex, event.targetEventId.hex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for content reposted
|
||||
* Coordinates with:
|
||||
* - UI context: Update repost counts in real-time
|
||||
* - Notification context: Create notification for original author
|
||||
*/
|
||||
export const handleContentReposted: EventHandler<ContentReposted> = async (event) => {
|
||||
console.debug('[ContentEventHandler] Content reposted:', {
|
||||
actor: event.actor.formatted,
|
||||
originalEventId: event.originalEventId.hex,
|
||||
originalAuthor: event.originalAuthor.formatted
|
||||
})
|
||||
|
||||
// Update repost count in UI
|
||||
if (callbacks.onUpdateRepostCount) {
|
||||
callbacks.onUpdateRepostCount(event.originalEventId.hex, 1)
|
||||
}
|
||||
|
||||
// Create notification for the original author (if not self)
|
||||
if (callbacks.onCreateNotification && event.actor.hex !== event.originalAuthor.hex) {
|
||||
callbacks.onCreateNotification('repost', event.actor.hex, event.originalEventId.hex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all content event handlers with the event dispatcher
|
||||
*/
|
||||
export function registerContentEventHandlers(): void {
|
||||
eventDispatcher.on('content.event_bookmarked', handleEventBookmarked)
|
||||
eventDispatcher.on('content.event_unbookmarked', handleEventUnbookmarked)
|
||||
eventDispatcher.on('content.bookmark_list_published', handleBookmarkListPublished)
|
||||
eventDispatcher.on('content.note_pinned', handleNotePinned)
|
||||
eventDispatcher.on('content.note_unpinned', handleNoteUnpinned)
|
||||
eventDispatcher.on('content.pins_limit_exceeded', handlePinsLimitExceeded)
|
||||
eventDispatcher.on('content.pin_list_published', handlePinListPublished)
|
||||
eventDispatcher.on('content.reaction_added', handleReactionAdded)
|
||||
eventDispatcher.on('content.reposted', handleContentReposted)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister all content event handlers
|
||||
*/
|
||||
export function unregisterContentEventHandlers(): void {
|
||||
eventDispatcher.off('content.event_bookmarked', handleEventBookmarked)
|
||||
eventDispatcher.off('content.event_unbookmarked', handleEventUnbookmarked)
|
||||
eventDispatcher.off('content.bookmark_list_published', handleBookmarkListPublished)
|
||||
eventDispatcher.off('content.note_pinned', handleNotePinned)
|
||||
eventDispatcher.off('content.note_unpinned', handleNoteUnpinned)
|
||||
eventDispatcher.off('content.pins_limit_exceeded', handlePinsLimitExceeded)
|
||||
eventDispatcher.off('content.pin_list_published', handlePinListPublished)
|
||||
eventDispatcher.off('content.reaction_added', handleReactionAdded)
|
||||
eventDispatcher.off('content.reposted', handleContentReposted)
|
||||
}
|
||||
215
src/application/handlers/FeedEventHandlers.ts
Normal file
215
src/application/handlers/FeedEventHandlers.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import {
|
||||
FeedSwitched,
|
||||
ContentFilterUpdated,
|
||||
FeedRefreshed,
|
||||
NoteCreated,
|
||||
NoteDeleted,
|
||||
NoteReplied,
|
||||
UsersMentioned,
|
||||
TimelineEventsReceived,
|
||||
TimelineEOSED
|
||||
} from '@/domain/feed/events'
|
||||
import { EventHandler, eventDispatcher } from '@/domain/shared'
|
||||
|
||||
/**
|
||||
* Handlers for Feed domain events
|
||||
*
|
||||
* These handlers coordinate cross-context updates when feed events occur.
|
||||
* They enable coordination between Feed, Social, Content, and UI contexts.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handler for feed switched events
|
||||
* Can be used to:
|
||||
* - Clear timeline caches for the old feed
|
||||
* - Prefetch content for the new feed
|
||||
* - Update URL/navigation state
|
||||
* - Log analytics
|
||||
*/
|
||||
export const handleFeedSwitched: EventHandler<FeedSwitched> = async (event) => {
|
||||
console.debug('[FeedEventHandler] Feed switched:', {
|
||||
owner: event.owner?.formatted,
|
||||
fromType: event.fromType?.value ?? 'none',
|
||||
toType: event.toType.value,
|
||||
relaySetId: event.relaySetId
|
||||
})
|
||||
|
||||
// Future: Clear old timeline cache
|
||||
// Future: Trigger new timeline fetch
|
||||
// Future: Update analytics
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for content filter updated events
|
||||
* Can be used to:
|
||||
* - Re-filter current timeline with new settings
|
||||
* - Persist filter preferences
|
||||
* - Update filter indicators in UI
|
||||
*/
|
||||
export const handleContentFilterUpdated: EventHandler<ContentFilterUpdated> = async (event) => {
|
||||
console.debug('[FeedEventHandler] Content filter updated:', {
|
||||
owner: event.owner.formatted,
|
||||
hideRepliesChanged: event.previousFilter.hideReplies !== event.newFilter.hideReplies,
|
||||
hideRepostsChanged: event.previousFilter.hideReposts !== event.newFilter.hideReposts,
|
||||
nsfwPolicyChanged: event.previousFilter.nsfwPolicy !== event.newFilter.nsfwPolicy
|
||||
})
|
||||
|
||||
// Future: Trigger timeline re-filter
|
||||
// Future: Persist filter preferences
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for feed refreshed events
|
||||
* Can be used to:
|
||||
* - Update last refresh timestamp display
|
||||
* - Trigger background data fetch
|
||||
* - Reset scroll position indicators
|
||||
*/
|
||||
export const handleFeedRefreshed: EventHandler<FeedRefreshed> = async (event) => {
|
||||
console.debug('[FeedEventHandler] Feed refreshed:', {
|
||||
owner: event.owner?.formatted,
|
||||
feedType: event.feedType.value
|
||||
})
|
||||
|
||||
// Future: Update refresh timestamp in UI
|
||||
// Future: Trigger stale data cleanup
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for note created events
|
||||
* Can be used to:
|
||||
* - Add note to local timeline immediately (optimistic UI)
|
||||
* - Create notifications for mentioned users
|
||||
* - Update post count displays
|
||||
*/
|
||||
export const handleNoteCreated: EventHandler<NoteCreated> = async (event) => {
|
||||
console.debug('[FeedEventHandler] Note created:', {
|
||||
author: event.author.formatted,
|
||||
noteId: event.noteId.hex,
|
||||
mentionCount: event.mentions.length,
|
||||
isReply: event.isReply,
|
||||
isQuote: event.isQuote
|
||||
})
|
||||
|
||||
// Future: Add to local timeline if author is self
|
||||
// Future: Create mention notifications
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for note deleted events
|
||||
* Can be used to:
|
||||
* - Remove note from all timelines
|
||||
* - Update reply counts on parent notes
|
||||
* - Clean up cached data
|
||||
*/
|
||||
export const handleNoteDeleted: EventHandler<NoteDeleted> = async (event) => {
|
||||
console.debug('[FeedEventHandler] Note deleted:', {
|
||||
author: event.author.formatted,
|
||||
noteId: event.noteId.hex
|
||||
})
|
||||
|
||||
// Future: Remove from timeline display
|
||||
// Future: Remove from caches
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for note replied events
|
||||
* Can be used to:
|
||||
* - Increment reply count on parent note
|
||||
* - Create notification for parent note author
|
||||
* - Update thread view if open
|
||||
*/
|
||||
export const handleNoteReplied: EventHandler<NoteReplied> = async (event) => {
|
||||
console.debug('[FeedEventHandler] Note replied:', {
|
||||
replier: event.replier.formatted,
|
||||
replyNoteId: event.replyNoteId.hex,
|
||||
originalNoteId: event.originalNoteId.hex,
|
||||
originalAuthor: event.originalAuthor.formatted
|
||||
})
|
||||
|
||||
// Future: Increment reply count
|
||||
// Future: Create reply notification for parent author
|
||||
// Future: Update thread view
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for users mentioned events
|
||||
* Can be used to:
|
||||
* - Create mention notifications for each mentioned user
|
||||
* - Highlight mentions in the source note
|
||||
*/
|
||||
export const handleUsersMentioned: EventHandler<UsersMentioned> = async (event) => {
|
||||
console.debug('[FeedEventHandler] Users mentioned:', {
|
||||
author: event.author.formatted,
|
||||
noteId: event.noteId.hex,
|
||||
mentionedCount: event.mentionedPubkeys.length
|
||||
})
|
||||
|
||||
// Future: Create mention notifications
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for timeline events received
|
||||
* Can be used to:
|
||||
* - Update event cache
|
||||
* - Trigger profile/metadata fetches for new authors
|
||||
* - Update unread counts
|
||||
*/
|
||||
export const handleTimelineEventsReceived: EventHandler<TimelineEventsReceived> = async (event) => {
|
||||
console.debug('[FeedEventHandler] Timeline events received:', {
|
||||
feedType: event.feedType.value,
|
||||
eventCount: event.eventCount,
|
||||
newestTimestamp: event.newestTimestamp.unix,
|
||||
isHistorical: event.isHistorical
|
||||
})
|
||||
|
||||
// Future: Prefetch profiles for new authors
|
||||
// Future: Update new post indicators
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for timeline EOSE (end of stored events)
|
||||
* Can be used to:
|
||||
* - Mark initial load as complete
|
||||
* - Switch from loading to live mode
|
||||
* - Update loading indicators
|
||||
*/
|
||||
export const handleTimelineEOSED: EventHandler<TimelineEOSED> = async (event) => {
|
||||
console.debug('[FeedEventHandler] Timeline EOSE:', {
|
||||
feedType: event.feedType.value,
|
||||
totalEvents: event.totalEvents
|
||||
})
|
||||
|
||||
// Future: Update loading state
|
||||
// Future: Show "up to date" indicator
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all feed event handlers with the event dispatcher
|
||||
*/
|
||||
export function registerFeedEventHandlers(): void {
|
||||
eventDispatcher.on('feed.switched', handleFeedSwitched)
|
||||
eventDispatcher.on('feed.content_filter_updated', handleContentFilterUpdated)
|
||||
eventDispatcher.on('feed.refreshed', handleFeedRefreshed)
|
||||
eventDispatcher.on('feed.note_created', handleNoteCreated)
|
||||
eventDispatcher.on('feed.note_deleted', handleNoteDeleted)
|
||||
eventDispatcher.on('feed.note_replied', handleNoteReplied)
|
||||
eventDispatcher.on('feed.users_mentioned', handleUsersMentioned)
|
||||
eventDispatcher.on('feed.timeline_events_received', handleTimelineEventsReceived)
|
||||
eventDispatcher.on('feed.timeline_eosed', handleTimelineEOSED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister all feed event handlers
|
||||
*/
|
||||
export function unregisterFeedEventHandlers(): void {
|
||||
eventDispatcher.off('feed.switched', handleFeedSwitched)
|
||||
eventDispatcher.off('feed.content_filter_updated', handleContentFilterUpdated)
|
||||
eventDispatcher.off('feed.refreshed', handleFeedRefreshed)
|
||||
eventDispatcher.off('feed.note_created', handleNoteCreated)
|
||||
eventDispatcher.off('feed.note_deleted', handleNoteDeleted)
|
||||
eventDispatcher.off('feed.note_replied', handleNoteReplied)
|
||||
eventDispatcher.off('feed.users_mentioned', handleUsersMentioned)
|
||||
eventDispatcher.off('feed.timeline_events_received', handleTimelineEventsReceived)
|
||||
eventDispatcher.off('feed.timeline_eosed', handleTimelineEOSED)
|
||||
}
|
||||
220
src/application/handlers/RelayEventHandlers.ts
Normal file
220
src/application/handlers/RelayEventHandlers.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
FavoriteRelayAdded,
|
||||
FavoriteRelayRemoved,
|
||||
FavoriteRelaysPublished,
|
||||
RelaySetCreated,
|
||||
RelaySetUpdated,
|
||||
RelaySetDeleted,
|
||||
MailboxRelayAdded,
|
||||
MailboxRelayRemoved,
|
||||
MailboxRelayScopeChanged,
|
||||
RelayListPublished
|
||||
} from '@/domain/relay/events'
|
||||
import { EventHandler, eventDispatcher } from '@/domain/shared'
|
||||
|
||||
/**
|
||||
* Handlers for Relay domain events
|
||||
*
|
||||
* These handlers coordinate cross-context updates when relay configuration changes.
|
||||
* They enable coordination between Relay, Feed, and Identity contexts.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handler for favorite relay added events
|
||||
* Can be used to:
|
||||
* - Update relay picker UI
|
||||
* - Add relay to connection pool
|
||||
*/
|
||||
export const handleFavoriteRelayAdded: EventHandler<FavoriteRelayAdded> = async (event) => {
|
||||
console.debug('[RelayEventHandler] Favorite relay added:', {
|
||||
owner: event.owner.formatted,
|
||||
relay: event.relayUrl.value
|
||||
})
|
||||
|
||||
// Future: Update relay picker options
|
||||
// Future: Pre-connect to new favorite relay
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for favorite relay removed events
|
||||
* Can be used to:
|
||||
* - Update relay picker UI
|
||||
* - Close connection if no longer needed
|
||||
*/
|
||||
export const handleFavoriteRelayRemoved: EventHandler<FavoriteRelayRemoved> = async (event) => {
|
||||
console.debug('[RelayEventHandler] Favorite relay removed:', {
|
||||
owner: event.owner.formatted,
|
||||
relay: event.relayUrl.value
|
||||
})
|
||||
|
||||
// Future: Update relay picker options
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for favorite relays published events
|
||||
* Can be used to:
|
||||
* - Invalidate relay preference caches
|
||||
* - Sync with remote state
|
||||
*/
|
||||
export const handleFavoriteRelaysPublished: EventHandler<FavoriteRelaysPublished> = async (event) => {
|
||||
console.debug('[RelayEventHandler] Favorite relays published:', {
|
||||
owner: event.owner.formatted,
|
||||
relayCount: event.relayCount
|
||||
})
|
||||
|
||||
// Future: Invalidate caches
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for relay set created events
|
||||
* Can be used to:
|
||||
* - Update feed type options in UI
|
||||
* - Add new relay set to navigation
|
||||
*/
|
||||
export const handleRelaySetCreated: EventHandler<RelaySetCreated> = async (event) => {
|
||||
console.debug('[RelayEventHandler] Relay set created:', {
|
||||
owner: event.owner.formatted,
|
||||
setId: event.setId,
|
||||
name: event.name,
|
||||
relayCount: event.relays.length
|
||||
})
|
||||
|
||||
// Future: Update feed selector options
|
||||
// Future: Add to relay set navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for relay set updated events
|
||||
* Can be used to:
|
||||
* - Refresh active feed if using this relay set
|
||||
* - Update relay set display
|
||||
*/
|
||||
export const handleRelaySetUpdated: EventHandler<RelaySetUpdated> = async (event) => {
|
||||
console.debug('[RelayEventHandler] Relay set updated:', {
|
||||
owner: event.owner.formatted,
|
||||
setId: event.setId,
|
||||
nameChanged: event.nameChanged,
|
||||
changes: {
|
||||
addedCount: event.changes.addedRelays?.length ?? 0,
|
||||
removedCount: event.changes.removedRelays?.length ?? 0
|
||||
}
|
||||
})
|
||||
|
||||
// Future: Refresh feed if currently using this relay set
|
||||
// Future: Update relay set display
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for relay set deleted events
|
||||
* Can be used to:
|
||||
* - Switch to different feed if current feed uses deleted set
|
||||
* - Remove from navigation
|
||||
*/
|
||||
export const handleRelaySetDeleted: EventHandler<RelaySetDeleted> = async (event) => {
|
||||
console.debug('[RelayEventHandler] Relay set deleted:', {
|
||||
owner: event.owner.formatted,
|
||||
setId: event.setId
|
||||
})
|
||||
|
||||
// Future: Switch feed if currently using this relay set
|
||||
// Future: Remove from feed selector options
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for mailbox relay added events
|
||||
* Can be used to:
|
||||
* - Update relay list display
|
||||
* - Connect to new mailbox relay
|
||||
*/
|
||||
export const handleMailboxRelayAdded: EventHandler<MailboxRelayAdded> = async (event) => {
|
||||
console.debug('[RelayEventHandler] Mailbox relay added:', {
|
||||
owner: event.owner.formatted,
|
||||
relay: event.relayUrl.value,
|
||||
scope: event.scope
|
||||
})
|
||||
|
||||
// Future: Update relay list in settings
|
||||
// Future: Connect to relay based on scope
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for mailbox relay removed events
|
||||
* Can be used to:
|
||||
* - Update relay list display
|
||||
* - Disconnect if no longer needed
|
||||
*/
|
||||
export const handleMailboxRelayRemoved: EventHandler<MailboxRelayRemoved> = async (event) => {
|
||||
console.debug('[RelayEventHandler] Mailbox relay removed:', {
|
||||
owner: event.owner.formatted,
|
||||
relay: event.relayUrl.value
|
||||
})
|
||||
|
||||
// Future: Update relay list in settings
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for mailbox relay scope changed events
|
||||
* Can be used to:
|
||||
* - Update relay list display
|
||||
* - Adjust connection strategy
|
||||
*/
|
||||
export const handleMailboxRelayScopeChanged: EventHandler<MailboxRelayScopeChanged> = async (event) => {
|
||||
console.debug('[RelayEventHandler] Mailbox relay scope changed:', {
|
||||
owner: event.owner.formatted,
|
||||
relay: event.relayUrl.value,
|
||||
fromScope: event.fromScope,
|
||||
toScope: event.toScope
|
||||
})
|
||||
|
||||
// Future: Update relay list in settings
|
||||
// Future: Adjust write/read connection strategy
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for relay list published events
|
||||
* Can be used to:
|
||||
* - Invalidate relay caches
|
||||
* - Trigger feed refresh if relay configuration changed
|
||||
*/
|
||||
export const handleRelayListPublished: EventHandler<RelayListPublished> = async (event) => {
|
||||
console.debug('[RelayEventHandler] Relay list published:', {
|
||||
owner: event.owner.formatted,
|
||||
readRelayCount: event.readRelayCount,
|
||||
writeRelayCount: event.writeRelayCount
|
||||
})
|
||||
|
||||
// Future: Invalidate relay caches
|
||||
// Future: Trigger feed refresh if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all relay event handlers with the event dispatcher
|
||||
*/
|
||||
export function registerRelayEventHandlers(): void {
|
||||
eventDispatcher.on('relay.favorite_added', handleFavoriteRelayAdded)
|
||||
eventDispatcher.on('relay.favorite_removed', handleFavoriteRelayRemoved)
|
||||
eventDispatcher.on('relay.favorites_published', handleFavoriteRelaysPublished)
|
||||
eventDispatcher.on('relay.set_created', handleRelaySetCreated)
|
||||
eventDispatcher.on('relay.set_updated', handleRelaySetUpdated)
|
||||
eventDispatcher.on('relay.set_deleted', handleRelaySetDeleted)
|
||||
eventDispatcher.on('relay.mailbox_added', handleMailboxRelayAdded)
|
||||
eventDispatcher.on('relay.mailbox_removed', handleMailboxRelayRemoved)
|
||||
eventDispatcher.on('relay.mailbox_scope_changed', handleMailboxRelayScopeChanged)
|
||||
eventDispatcher.on('relay.list_published', handleRelayListPublished)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister all relay event handlers
|
||||
*/
|
||||
export function unregisterRelayEventHandlers(): void {
|
||||
eventDispatcher.off('relay.favorite_added', handleFavoriteRelayAdded)
|
||||
eventDispatcher.off('relay.favorite_removed', handleFavoriteRelayRemoved)
|
||||
eventDispatcher.off('relay.favorites_published', handleFavoriteRelaysPublished)
|
||||
eventDispatcher.off('relay.set_created', handleRelaySetCreated)
|
||||
eventDispatcher.off('relay.set_updated', handleRelaySetUpdated)
|
||||
eventDispatcher.off('relay.set_deleted', handleRelaySetDeleted)
|
||||
eventDispatcher.off('relay.mailbox_added', handleMailboxRelayAdded)
|
||||
eventDispatcher.off('relay.mailbox_removed', handleMailboxRelayRemoved)
|
||||
eventDispatcher.off('relay.mailbox_scope_changed', handleMailboxRelayScopeChanged)
|
||||
eventDispatcher.off('relay.list_published', handleRelayListPublished)
|
||||
}
|
||||
205
src/application/handlers/SocialEventHandlers.ts
Normal file
205
src/application/handlers/SocialEventHandlers.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import {
|
||||
UserFollowed,
|
||||
UserUnfollowed,
|
||||
UserMuted,
|
||||
UserUnmuted,
|
||||
MuteVisibilityChanged,
|
||||
FollowListPublished,
|
||||
MuteListPublished
|
||||
} from '@/domain/social/events'
|
||||
import { EventHandler, eventDispatcher } from '@/domain/shared'
|
||||
|
||||
/**
|
||||
* Handlers for social domain events
|
||||
*
|
||||
* These handlers coordinate cross-context updates when social events occur.
|
||||
* They bridge the Social context with Feed, Notification, and Cache contexts.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Callback type for feed refresh requests
|
||||
*/
|
||||
export type FeedRefreshCallback = () => void
|
||||
|
||||
/**
|
||||
* Callback type for content refiltering requests
|
||||
*/
|
||||
export type RefilterCallback = () => void
|
||||
|
||||
/**
|
||||
* Callback type for profile prefetch requests
|
||||
*/
|
||||
export type PrefetchProfileCallback = (pubkey: string) => void
|
||||
|
||||
/**
|
||||
* Service callbacks that can be injected for cross-context coordination
|
||||
*/
|
||||
export interface SocialHandlerCallbacks {
|
||||
onFeedRefreshNeeded?: FeedRefreshCallback
|
||||
onRefilterNeeded?: RefilterCallback
|
||||
onPrefetchProfile?: PrefetchProfileCallback
|
||||
}
|
||||
|
||||
let callbacks: SocialHandlerCallbacks = {}
|
||||
|
||||
/**
|
||||
* Set the callbacks for cross-context coordination
|
||||
* Call this during provider initialization
|
||||
*/
|
||||
export function setSocialHandlerCallbacks(newCallbacks: SocialHandlerCallbacks): void {
|
||||
callbacks = { ...callbacks, ...newCallbacks }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all callbacks (for cleanup/testing)
|
||||
*/
|
||||
export function clearSocialHandlerCallbacks(): void {
|
||||
callbacks = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for user followed events
|
||||
* Coordinates with:
|
||||
* - Feed context: Add followed user's content to timeline
|
||||
* - Cache context: Prefetch followed user's profile and notes
|
||||
*/
|
||||
export const handleUserFollowed: EventHandler<UserFollowed> = async (event) => {
|
||||
console.debug('[SocialEventHandler] User followed:', {
|
||||
actor: event.actor.formatted,
|
||||
followed: event.followed.formatted,
|
||||
petname: event.petname
|
||||
})
|
||||
|
||||
// Prefetch the followed user's profile for better UX
|
||||
if (callbacks.onPrefetchProfile) {
|
||||
callbacks.onPrefetchProfile(event.followed.hex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for user unfollowed events
|
||||
* Can be used to:
|
||||
* - Update feed context to exclude unfollowed user's content
|
||||
* - Clean up cached data for unfollowed user
|
||||
*/
|
||||
export const handleUserUnfollowed: EventHandler<UserUnfollowed> = async (event) => {
|
||||
console.debug('[SocialEventHandler] User unfollowed:', {
|
||||
actor: event.actor.formatted,
|
||||
unfollowed: event.unfollowed.formatted
|
||||
})
|
||||
|
||||
// Future: Dispatch to feed context to update content sources
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for user muted events
|
||||
* Coordinates with:
|
||||
* - Feed context: Refilter timeline to hide muted user's content
|
||||
* - Notification context: Filter notifications from muted user
|
||||
* - DM context: Update DM filtering
|
||||
*/
|
||||
export const handleUserMuted: EventHandler<UserMuted> = async (event) => {
|
||||
console.debug('[SocialEventHandler] User muted:', {
|
||||
actor: event.actor.formatted,
|
||||
muted: event.muted.formatted,
|
||||
visibility: event.visibility
|
||||
})
|
||||
|
||||
// Trigger immediate refiltering of current timeline
|
||||
if (callbacks.onRefilterNeeded) {
|
||||
callbacks.onRefilterNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for user unmuted events
|
||||
* Coordinates with:
|
||||
* - Feed context: Refilter timeline to show unmuted user's content
|
||||
* - Notification context: Restore notifications from unmuted user
|
||||
*/
|
||||
export const handleUserUnmuted: EventHandler<UserUnmuted> = async (event) => {
|
||||
console.debug('[SocialEventHandler] User unmuted:', {
|
||||
actor: event.actor.formatted,
|
||||
unmuted: event.unmuted.formatted
|
||||
})
|
||||
|
||||
// Trigger refiltering to restore unmuted user's content
|
||||
if (callbacks.onRefilterNeeded) {
|
||||
callbacks.onRefilterNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for mute visibility changed events
|
||||
*/
|
||||
export const handleMuteVisibilityChanged: EventHandler<MuteVisibilityChanged> = async (event) => {
|
||||
console.debug('[SocialEventHandler] Mute visibility changed:', {
|
||||
actor: event.actor.formatted,
|
||||
target: event.target.formatted,
|
||||
from: event.from,
|
||||
to: event.to
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for follow list published events
|
||||
* Coordinates with:
|
||||
* - Feed context: Refresh following feed with new list
|
||||
* - Cache context: Invalidate author caches
|
||||
*/
|
||||
export const handleFollowListPublished: EventHandler<FollowListPublished> = async (event) => {
|
||||
console.debug('[SocialEventHandler] Follow list published:', {
|
||||
owner: event.owner.formatted,
|
||||
followingCount: event.followingCount
|
||||
})
|
||||
|
||||
// Trigger feed refresh to reflect new following list
|
||||
if (callbacks.onFeedRefreshNeeded) {
|
||||
callbacks.onFeedRefreshNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for mute list published events
|
||||
* Coordinates with:
|
||||
* - Feed context: Refilter timeline with new mute list
|
||||
* - Notification context: Update notification filtering
|
||||
*/
|
||||
export const handleMuteListPublished: EventHandler<MuteListPublished> = async (event) => {
|
||||
console.debug('[SocialEventHandler] Mute list published:', {
|
||||
owner: event.owner.formatted,
|
||||
publicMuteCount: event.publicMuteCount,
|
||||
privateMuteCount: event.privateMuteCount
|
||||
})
|
||||
|
||||
// Trigger refiltering with updated mute list
|
||||
if (callbacks.onRefilterNeeded) {
|
||||
callbacks.onRefilterNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all social event handlers with the event dispatcher
|
||||
*/
|
||||
export function registerSocialEventHandlers(): void {
|
||||
eventDispatcher.on('social.user_followed', handleUserFollowed)
|
||||
eventDispatcher.on('social.user_unfollowed', handleUserUnfollowed)
|
||||
eventDispatcher.on('social.user_muted', handleUserMuted)
|
||||
eventDispatcher.on('social.user_unmuted', handleUserUnmuted)
|
||||
eventDispatcher.on('social.mute_visibility_changed', handleMuteVisibilityChanged)
|
||||
eventDispatcher.on('social.follow_list_published', handleFollowListPublished)
|
||||
eventDispatcher.on('social.mute_list_published', handleMuteListPublished)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister all social event handlers
|
||||
*/
|
||||
export function unregisterSocialEventHandlers(): void {
|
||||
eventDispatcher.off('social.user_followed', handleUserFollowed)
|
||||
eventDispatcher.off('social.user_unfollowed', handleUserUnfollowed)
|
||||
eventDispatcher.off('social.user_muted', handleUserMuted)
|
||||
eventDispatcher.off('social.user_unmuted', handleUserUnmuted)
|
||||
eventDispatcher.off('social.mute_visibility_changed', handleMuteVisibilityChanged)
|
||||
eventDispatcher.off('social.follow_list_published', handleFollowListPublished)
|
||||
eventDispatcher.off('social.mute_list_published', handleMuteListPublished)
|
||||
}
|
||||
121
src/application/handlers/index.ts
Normal file
121
src/application/handlers/index.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Domain Event Handlers
|
||||
*
|
||||
* Application-level handlers that coordinate cross-context updates
|
||||
* when domain events occur.
|
||||
*/
|
||||
|
||||
// Social Event Handlers
|
||||
export {
|
||||
registerSocialEventHandlers,
|
||||
unregisterSocialEventHandlers,
|
||||
setSocialHandlerCallbacks,
|
||||
clearSocialHandlerCallbacks,
|
||||
handleUserFollowed,
|
||||
handleUserUnfollowed,
|
||||
handleUserMuted,
|
||||
handleUserUnmuted,
|
||||
handleMuteVisibilityChanged,
|
||||
handleFollowListPublished,
|
||||
handleMuteListPublished,
|
||||
type SocialHandlerCallbacks,
|
||||
type FeedRefreshCallback,
|
||||
type RefilterCallback,
|
||||
type PrefetchProfileCallback
|
||||
} from './SocialEventHandlers'
|
||||
|
||||
// Content Event Handlers
|
||||
export {
|
||||
registerContentEventHandlers,
|
||||
unregisterContentEventHandlers,
|
||||
setContentHandlerCallbacks,
|
||||
clearContentHandlerCallbacks,
|
||||
handleEventBookmarked,
|
||||
handleEventUnbookmarked,
|
||||
handleBookmarkListPublished,
|
||||
handleNotePinned,
|
||||
handleNoteUnpinned,
|
||||
handlePinsLimitExceeded,
|
||||
handlePinListPublished,
|
||||
handleReactionAdded,
|
||||
handleContentReposted,
|
||||
type ContentHandlerCallbacks,
|
||||
type UpdateReactionCountCallback,
|
||||
type UpdateRepostCountCallback,
|
||||
type CreateNotificationCallback,
|
||||
type ShowToastCallback,
|
||||
type UpdateProfilePinsCallback
|
||||
} from './ContentEventHandlers'
|
||||
|
||||
// Feed Event Handlers
|
||||
export {
|
||||
registerFeedEventHandlers,
|
||||
unregisterFeedEventHandlers,
|
||||
handleFeedSwitched,
|
||||
handleContentFilterUpdated,
|
||||
handleFeedRefreshed,
|
||||
handleNoteCreated,
|
||||
handleNoteDeleted,
|
||||
handleNoteReplied,
|
||||
handleUsersMentioned,
|
||||
handleTimelineEventsReceived,
|
||||
handleTimelineEOSED
|
||||
} from './FeedEventHandlers'
|
||||
|
||||
// Relay Event Handlers
|
||||
export {
|
||||
registerRelayEventHandlers,
|
||||
unregisterRelayEventHandlers,
|
||||
handleFavoriteRelayAdded,
|
||||
handleFavoriteRelayRemoved,
|
||||
handleFavoriteRelaysPublished,
|
||||
handleRelaySetCreated,
|
||||
handleRelaySetUpdated,
|
||||
handleRelaySetDeleted,
|
||||
handleMailboxRelayAdded,
|
||||
handleMailboxRelayRemoved,
|
||||
handleMailboxRelayScopeChanged,
|
||||
handleRelayListPublished
|
||||
} from './RelayEventHandlers'
|
||||
|
||||
/**
|
||||
* Initialize all domain event handlers
|
||||
*
|
||||
* Call this once during application startup to register all handlers
|
||||
* with the event dispatcher.
|
||||
*/
|
||||
export function initializeEventHandlers(): void {
|
||||
const { registerSocialEventHandlers } = require('./SocialEventHandlers')
|
||||
const { registerContentEventHandlers } = require('./ContentEventHandlers')
|
||||
const { registerFeedEventHandlers } = require('./FeedEventHandlers')
|
||||
const { registerRelayEventHandlers } = require('./RelayEventHandlers')
|
||||
|
||||
registerSocialEventHandlers()
|
||||
registerContentEventHandlers()
|
||||
registerFeedEventHandlers()
|
||||
registerRelayEventHandlers()
|
||||
|
||||
console.debug('[EventHandlers] All domain event handlers registered')
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup all domain event handlers
|
||||
*
|
||||
* Call this during application shutdown or for testing purposes.
|
||||
*/
|
||||
export function cleanupEventHandlers(): void {
|
||||
const { unregisterSocialEventHandlers, clearSocialHandlerCallbacks } = require('./SocialEventHandlers')
|
||||
const { unregisterContentEventHandlers, clearContentHandlerCallbacks } = require('./ContentEventHandlers')
|
||||
const { unregisterFeedEventHandlers } = require('./FeedEventHandlers')
|
||||
const { unregisterRelayEventHandlers } = require('./RelayEventHandlers')
|
||||
|
||||
unregisterSocialEventHandlers()
|
||||
unregisterContentEventHandlers()
|
||||
unregisterFeedEventHandlers()
|
||||
unregisterRelayEventHandlers()
|
||||
|
||||
clearSocialHandlerCallbacks()
|
||||
clearContentHandlerCallbacks()
|
||||
|
||||
console.debug('[EventHandlers] All domain event handlers unregistered')
|
||||
}
|
||||
@@ -10,3 +10,13 @@ export type { RelaySelectorOptions } from './RelaySelector'
|
||||
|
||||
export { PublishingService, publishingService } from './PublishingService'
|
||||
export type { DraftEvent, PublishNoteOptions } from './PublishingService'
|
||||
|
||||
// Event Handlers
|
||||
export {
|
||||
initializeEventHandlers,
|
||||
cleanupEventHandlers,
|
||||
registerSocialEventHandlers,
|
||||
unregisterSocialEventHandlers,
|
||||
registerContentEventHandlers,
|
||||
unregisterContentEventHandlers
|
||||
} from './handlers'
|
||||
|
||||
Reference in New Issue
Block a user