import { Injectable, inject } from '@angular/core'; import { createEffect } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { EMPTY, from, merge, Observable } from 'rxjs'; import { distinctUntilChanged, map, mergeMap } from 'rxjs/operators'; import { selectCurrentUser } from '../../../store/users/users.selectors'; import { ChatEvent, CustomEmoji, CustomEmojiSummaryItem, CustomEmojiTransferManifest } from '../../../shared-kernel'; import { RealtimeSessionFacade } from '../../../core/realtime'; import { CustomEmojiService } from './custom-emoji.service'; @Injectable() export class CustomEmojiSyncEffects { private readonly customEmoji = inject(CustomEmojiService); private readonly store = inject(Store); private readonly webrtc = inject(RealtimeSessionFacade); private readonly incomingEvents$ = merge( this.webrtc.onMessageReceived, this.webrtc.onSignalingMessage as Observable ); currentUserLoad$ = createEffect( () => this.store.select(selectCurrentUser).pipe( map((user) => user?.id ?? null), distinctUntilChanged(), mergeMap((userId) => from(this.customEmoji.loadForUser(userId)).pipe(mergeMap(() => EMPTY))) ), { dispatch: false } ); peerConnectedSummary$ = createEffect( () => this.webrtc.onPeerConnected.pipe( mergeMap((peerId) => from(this.customEmoji.sendSummaryToPeer(peerId)).pipe(mergeMap(() => EMPTY))) ), { dispatch: false } ); incomingCustomEmojiEvents$ = createEffect( () => this.incomingEvents$.pipe( mergeMap((event) => { if (event.type === 'message' || event.type === 'chat-message') { if (!event.fromPeerId || typeof event.message?.content !== 'string') { return EMPTY; } return from(this.customEmoji.requestMissingFromMessageContent(event.fromPeerId, event.message.content)).pipe(mergeMap(() => EMPTY)); } if (event.type === 'chat-sync-batch' || event.type === 'chat-sync-full') { if (!event.fromPeerId || !Array.isArray(event.messages)) { return EMPTY; } return from(Promise.all(event.messages.map((message) => this.customEmoji.requestMissingFromMessageContent( event.fromPeerId as string, message.content )))).pipe(mergeMap(() => EMPTY)); } switch (event.type) { case 'custom-emoji-summary': { if (!event.fromPeerId || !Array.isArray(event.customEmojiSummaries)) { return EMPTY; } const ids = this.customEmoji.missingFromSummary(event.customEmojiSummaries as CustomEmojiSummaryItem[]); if (ids.length === 0) { return EMPTY; } this.webrtc.sendToPeer(event.fromPeerId, { type: 'custom-emoji-request', ids }); return EMPTY; } case 'custom-emoji-request': if (!event.fromPeerId || !Array.isArray(event.ids)) { return EMPTY; } return from(this.customEmoji.sendRequestedToPeer(event.fromPeerId, event.ids)).pipe(mergeMap(() => EMPTY)); case 'custom-emoji-full': if (event.customEmoji) { return from(this.customEmoji.saveRemoteEmoji(event.customEmoji as CustomEmoji)).pipe(mergeMap(() => EMPTY)); } if (!event.customEmojiTransfer || typeof event.total !== 'number') { return EMPTY; } this.customEmoji.receiveTransferStart(event.customEmojiTransfer as CustomEmojiTransferManifest, event.total); return EMPTY; case 'custom-emoji-chunk': if (!event.customEmojiId || typeof event.index !== 'number' || typeof event.total !== 'number' || typeof event.data !== 'string') { return EMPTY; } return from(this.customEmoji.receiveTransferChunk(event.customEmojiId, event.index, event.total, event.data)).pipe(mergeMap(() => EMPTY)); default: return EMPTY; } }) ), { dispatch: false } ); }