ContentEventHandlers.ts raw

   1  import {
   2    EventBookmarked,
   3    EventUnbookmarked,
   4    BookmarkListPublished,
   5    NotePinned,
   6    NoteUnpinned,
   7    PinsLimitExceeded,
   8    PinListPublished,
   9    ReactionAdded,
  10    ContentReposted
  11  } from '@/domain/content'
  12  import { EventHandler, eventDispatcher } from '@/domain/shared'
  13  
  14  /**
  15   * Handlers for content domain events
  16   *
  17   * These handlers coordinate cross-context updates when content events occur.
  18   * They enable real-time UI updates and cross-context coordination.
  19   */
  20  
  21  /**
  22   * Callback for updating reaction counts in UI
  23   */
  24  export type UpdateReactionCountCallback = (eventId: string, emoji: string, delta: number) => void
  25  
  26  /**
  27   * Callback for updating repost counts in UI
  28   */
  29  export type UpdateRepostCountCallback = (eventId: string, delta: number) => void
  30  
  31  /**
  32   * Callback for creating notifications
  33   */
  34  export type CreateNotificationCallback = (
  35    type: 'reaction' | 'repost' | 'mention' | 'reply',
  36    actorPubkey: string,
  37    targetEventId: string
  38  ) => void
  39  
  40  /**
  41   * Callback for showing toast messages
  42   */
  43  export type ShowToastCallback = (message: string, type: 'info' | 'warning' | 'error') => void
  44  
  45  /**
  46   * Callback for updating profile pinned notes
  47   */
  48  export type UpdateProfilePinsCallback = (pubkey: string) => void
  49  
  50  /**
  51   * Service callbacks for cross-context coordination
  52   */
  53  export interface ContentHandlerCallbacks {
  54    onUpdateReactionCount?: UpdateReactionCountCallback
  55    onUpdateRepostCount?: UpdateRepostCountCallback
  56    onCreateNotification?: CreateNotificationCallback
  57    onShowToast?: ShowToastCallback
  58    onUpdateProfilePins?: UpdateProfilePinsCallback
  59  }
  60  
  61  let callbacks: ContentHandlerCallbacks = {}
  62  
  63  /**
  64   * Set the callbacks for cross-context coordination
  65   * Call this during provider initialization
  66   */
  67  export function setContentHandlerCallbacks(newCallbacks: ContentHandlerCallbacks): void {
  68    callbacks = { ...callbacks, ...newCallbacks }
  69  }
  70  
  71  /**
  72   * Clear all callbacks (for cleanup/testing)
  73   */
  74  export function clearContentHandlerCallbacks(): void {
  75    callbacks = {}
  76  }
  77  
  78  /**
  79   * Handler for event bookmarked
  80   * Can be used to:
  81   * - Update bookmark count displays
  82   * - Prefetch bookmarked content for offline access
  83   */
  84  export const handleEventBookmarked: EventHandler<EventBookmarked> = async (event) => {
  85    console.debug('[ContentEventHandler] Event bookmarked:', {
  86      actor: event.actor.formatted,
  87      bookmarkedEventId: event.bookmarkedEventId,
  88      type: event.bookmarkType
  89    })
  90  
  91    // Future: Trigger prefetch of bookmarked content
  92  }
  93  
  94  /**
  95   * Handler for event unbookmarked
  96   */
  97  export const handleEventUnbookmarked: EventHandler<EventUnbookmarked> = async (event) => {
  98    console.debug('[ContentEventHandler] Event unbookmarked:', {
  99      actor: event.actor.formatted,
 100      unbookmarkedEventId: event.unbookmarkedEventId
 101    })
 102  }
 103  
 104  /**
 105   * Handler for bookmark list published
 106   */
 107  export const handleBookmarkListPublished: EventHandler<BookmarkListPublished> = async (event) => {
 108    console.debug('[ContentEventHandler] Bookmark list published:', {
 109      owner: event.owner.formatted,
 110      bookmarkCount: event.bookmarkCount
 111    })
 112  }
 113  
 114  /**
 115   * Handler for note pinned
 116   * Coordinates with:
 117   * - Profile context: Update pinned notes display
 118   * - Cache context: Ensure pinned content is cached
 119   */
 120  export const handleNotePinned: EventHandler<NotePinned> = async (event) => {
 121    console.debug('[ContentEventHandler] Note pinned:', {
 122      actor: event.actor.formatted,
 123      pinnedEventId: event.pinnedEventId.hex
 124    })
 125  
 126    // Update profile display to show new pinned note
 127    if (callbacks.onUpdateProfilePins) {
 128      callbacks.onUpdateProfilePins(event.actor.hex)
 129    }
 130  }
 131  
 132  /**
 133   * Handler for note unpinned
 134   * Coordinates with:
 135   * - Profile context: Update pinned notes display
 136   */
 137  export const handleNoteUnpinned: EventHandler<NoteUnpinned> = async (event) => {
 138    console.debug('[ContentEventHandler] Note unpinned:', {
 139      actor: event.actor.formatted,
 140      unpinnedEventId: event.unpinnedEventId
 141    })
 142  
 143    // Update profile display to remove unpinned note
 144    if (callbacks.onUpdateProfilePins) {
 145      callbacks.onUpdateProfilePins(event.actor.hex)
 146    }
 147  }
 148  
 149  /**
 150   * Handler for pins limit exceeded
 151   * Coordinates with:
 152   * - UI context: Show toast notification about removed pins
 153   */
 154  export const handlePinsLimitExceeded: EventHandler<PinsLimitExceeded> = async (event) => {
 155    console.debug('[ContentEventHandler] Pins limit exceeded:', {
 156      actor: event.actor.formatted,
 157      removedCount: event.removedEventIds.length
 158    })
 159  
 160    // Show toast notification about removed pins
 161    if (callbacks.onShowToast) {
 162      callbacks.onShowToast(
 163        `Pin limit reached. ${event.removedEventIds.length} older pin(s) were removed.`,
 164        'warning'
 165      )
 166    }
 167  }
 168  
 169  /**
 170   * Handler for pin list published
 171   */
 172  export const handlePinListPublished: EventHandler<PinListPublished> = async (event) => {
 173    console.debug('[ContentEventHandler] Pin list published:', {
 174      owner: event.owner.formatted,
 175      pinCount: event.pinCount
 176    })
 177  }
 178  
 179  /**
 180   * Handler for reaction added
 181   * Coordinates with:
 182   * - UI context: Update reaction counts in real-time
 183   * - Notification context: Create notification for content author
 184   */
 185  export const handleReactionAdded: EventHandler<ReactionAdded> = async (event) => {
 186    console.debug('[ContentEventHandler] Reaction added:', {
 187      actor: event.actor.formatted,
 188      targetEventId: event.targetEventId.hex,
 189      targetAuthor: event.targetAuthor.formatted,
 190      emoji: event.emoji,
 191      isLike: event.isLike
 192    })
 193  
 194    // Update reaction count in UI
 195    if (callbacks.onUpdateReactionCount) {
 196      callbacks.onUpdateReactionCount(event.targetEventId.hex, event.emoji, 1)
 197    }
 198  
 199    // Create notification for the content author (if not self)
 200    if (callbacks.onCreateNotification && event.actor.hex !== event.targetAuthor.hex) {
 201      callbacks.onCreateNotification('reaction', event.actor.hex, event.targetEventId.hex)
 202    }
 203  }
 204  
 205  /**
 206   * Handler for content reposted
 207   * Coordinates with:
 208   * - UI context: Update repost counts in real-time
 209   * - Notification context: Create notification for original author
 210   */
 211  export const handleContentReposted: EventHandler<ContentReposted> = async (event) => {
 212    console.debug('[ContentEventHandler] Content reposted:', {
 213      actor: event.actor.formatted,
 214      originalEventId: event.originalEventId.hex,
 215      originalAuthor: event.originalAuthor.formatted
 216    })
 217  
 218    // Update repost count in UI
 219    if (callbacks.onUpdateRepostCount) {
 220      callbacks.onUpdateRepostCount(event.originalEventId.hex, 1)
 221    }
 222  
 223    // Create notification for the original author (if not self)
 224    if (callbacks.onCreateNotification && event.actor.hex !== event.originalAuthor.hex) {
 225      callbacks.onCreateNotification('repost', event.actor.hex, event.originalEventId.hex)
 226    }
 227  }
 228  
 229  /**
 230   * Register all content event handlers with the event dispatcher
 231   */
 232  export function registerContentEventHandlers(): void {
 233    eventDispatcher.on('content.event_bookmarked', handleEventBookmarked)
 234    eventDispatcher.on('content.event_unbookmarked', handleEventUnbookmarked)
 235    eventDispatcher.on('content.bookmark_list_published', handleBookmarkListPublished)
 236    eventDispatcher.on('content.note_pinned', handleNotePinned)
 237    eventDispatcher.on('content.note_unpinned', handleNoteUnpinned)
 238    eventDispatcher.on('content.pins_limit_exceeded', handlePinsLimitExceeded)
 239    eventDispatcher.on('content.pin_list_published', handlePinListPublished)
 240    eventDispatcher.on('content.reaction_added', handleReactionAdded)
 241    eventDispatcher.on('content.reposted', handleContentReposted)
 242  }
 243  
 244  /**
 245   * Unregister all content event handlers
 246   */
 247  export function unregisterContentEventHandlers(): void {
 248    eventDispatcher.off('content.event_bookmarked', handleEventBookmarked)
 249    eventDispatcher.off('content.event_unbookmarked', handleEventUnbookmarked)
 250    eventDispatcher.off('content.bookmark_list_published', handleBookmarkListPublished)
 251    eventDispatcher.off('content.note_pinned', handleNotePinned)
 252    eventDispatcher.off('content.note_unpinned', handleNoteUnpinned)
 253    eventDispatcher.off('content.pins_limit_exceeded', handlePinsLimitExceeded)
 254    eventDispatcher.off('content.pin_list_published', handlePinListPublished)
 255    eventDispatcher.off('content.reaction_added', handleReactionAdded)
 256    eventDispatcher.off('content.reposted', handleContentReposted)
 257  }
 258