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:
woikos
2026-01-04 07:29:07 +01:00
parent 158f3d77d3
commit 4c3e8d5cc7
167 changed files with 13451 additions and 1903 deletions

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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')
}

View File

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