import { AfterViewInit , ChangeDetectorRef , Component , ElementRef , Inject , Input , OnChanges , OnDestroy , OnInit , Optional , QueryList , SimpleChanges , ViewChild , ViewChildren , } from '@angular/core' ; import { Observable , Subject } from 'rxjs' ; import { debounceTime , filter , takeUntil } from 'rxjs/operators' ; import { Direction , Message } from '../../core/message' ; import { Recipient } from '../../core/recipient' ; import { BlockPlugin } from '../../services/adapters/xmpp/plugins/block.plugin' ; import { MessageArchivePlugin } from '../../services/adapters/xmpp/plugins/message-archive.plugin' ; import { RoomMessage } from '../../services/adapters/xmpp/plugins/multi-user-chat.plugin' ; import { ChatListStateService } from '../../services/chat-list-state.service' ; import { ChatMessageListRegistryService } from '../../services/chat-message-list-registry.service' ; import { CHAT_SERVICE_TOKEN , ChatService } from '../../services/chat-service' ; import { ContactFactoryService } from '../../services/contact-factory.service' ; import { REPORT_USER_INJECTION_TOKEN , ReportUserService } from '../../services/report-user-service' ; import { ChatMessageComponent } from '../chat-message/chat-message.component' ; enum SubscriptionAction { PENDING_REQUEST , SHOW_BLOCK_ACTIONS , NO_PENDING_REQUEST } @ Component ( { selector : 'ngx-chat-message-list' , templateUrl : './chat-message-list.component.html' , styleUrls : [ './chat-message-list.component.less' ] , } ) export class ChatMessageListComponent implements OnInit , OnDestroy , OnChanges , AfterViewInit { @ Input ( ) recipient : Recipient ; @ Input ( ) showAvatars : boolean ; @ ViewChild ( 'messageArea' ) chatMessageAreaElement : ElementRef < HTMLElement > ; @ ViewChildren ( ChatMessageComponent ) chatMessageViewChildrenList : QueryList < ChatMessageComponent > ; Direction = Direction ; SubscriptionAction = SubscriptionAction ; blockPlugin : BlockPlugin ; subscriptionAction = SubscriptionAction . NO_PENDING_REQUEST ; onTop$ = new Subject < IntersectionObserverEntry > ( ) ; private ngDestroy = new Subject < void > ( ) ; private isAtBottom = true ; private bottomLeftAt = 0 ; private oldestVisibleMessageBeforeLoading : Message = null ; constructor ( public chatListService , @ Inject ( CHAT_SERVICE_TOKEN ) public chatService , private chatMessageListRegistry , @ Optional ( ) @ Inject ( REPORT_USER_INJECTION_TOKEN ) public reportUserService , private changeDetectorRef , private contactFactory ) { this . blockPlugin = this . chatService . getPlugin ( BlockPlugin ) ; } async ngOnInit ( ) { this . onTop$ . pipe ( filter ( event => event . isIntersecting ) , debounceTime ( 1000 ) ) . subscribe ( ( ) => this . loadOlderMessagesBeforeViewport ( ) ) ; if ( this . recipient . recipientType === 'contact' ) { this . recipient . pendingIn$ . pipe ( filter ( pendingIn => pendingIn === true ) , takeUntil ( this . ngDestroy ) ) . subscribe ( ( ) => { this . subscriptionAction = SubscriptionAction . PENDING_REQUEST ; this . scheduleScrollToLastMessage ( ) ; } ) ; } this . chatMessageListRegistry . incrementOpenWindowCount ( this . recipient ) ; } async ngAfterViewInit ( ) { this . chatMessageViewChildrenList . changes . subscribe ( ( ) => { if ( this . oldestVisibleMessageBeforeLoading ) { this . scrollToMessage ( this . oldestVisibleMessageBeforeLoading ) ; } this . oldestVisibleMessageBeforeLoading = null ; } ) ; const messages$ = this . recipient . messages$ ; messages$ . pipe ( debounceTime ( 10 ) , filter ( ( ) => this . isNearBottom ( ) ) , takeUntil ( this . ngDestroy ) ) . subscribe ( ( _ ) => this . scheduleScrollToLastMessage ( ) ) ; if ( this . recipient . messages . length < 10 ) { await this . loadMessages ( ) ; } this . scheduleScrollToLastMessage ( ) ; } ngOnChanges ( changes ) { const contact = changes . contact ; if ( contact && contact . previousValue && contact . currentValue ) { this . chatMessageListRegistry . decrementOpenWindowCount ( contact . previousValue ) ; this . chatMessageListRegistry . incrementOpenWindowCount ( contact . currentValue ) ; } if ( contact && contact . currentValue ) { this . scheduleScrollToLastMessage ( ) ; } } ngOnDestroy ( ) { this . ngDestroy . next ( ) ; this . chatMessageListRegistry . decrementOpenWindowCount ( this . recipient ) ; } acceptSubscriptionRequest ( event ) { event . preventDefault ( ) ; if ( this . subscriptionAction === SubscriptionAction . PENDING_REQUEST ) { this . chatService . addContact ( this . recipient . jidBare . toString ( ) ) ; this . subscriptionAction = SubscriptionAction . NO_PENDING_REQUEST ; this . scheduleScrollToLastMessage ( ) ; } } denySubscriptionRequest ( event ) { event . preventDefault ( ) ; if ( this . subscriptionAction === SubscriptionAction . PENDING_REQUEST ) { this . chatService . removeContact ( this . recipient . jidBare . toString ( ) ) ; this . subscriptionAction = SubscriptionAction . SHOW_BLOCK_ACTIONS ; this . scheduleScrollToLastMessage ( ) ; } } scheduleScrollToLastMessage ( ) { setTimeout ( ( ) => this . scrollToLastMessage ( ) , 0 ) ; } private scrollToLastMessage ( ) { if ( this . chatMessageAreaElement ) { this . chatMessageAreaElement . nativeElement . scrollTop = this . chatMessageAreaElement . nativeElement . scrollHeight ; this . isAtBottom = true ; } } private scrollToMessage ( message ) { if ( this . chatMessageAreaElement ) { const htmlIdAttribute = 'message-' + message . id ; const messageElement = document . getElementById ( htmlIdAttribute ) ; messageElement . scrollIntoView ( false ) ; } } blockContact ( $event ) { $event . preventDefault ( ) ; this . blockPlugin . blockJid ( this . recipient . jidBare . toString ( ) ) ; this . chatListService . closeChat ( this . recipient ) ; this . subscriptionAction = SubscriptionAction . NO_PENDING_REQUEST ; } blockContactAndReport ( $event ) { if ( this . recipient . recipientType !== 'contact' ) { return ; } $event . preventDefault ( ) ; this . reportUserService . reportUser ( this . recipient ) ; this . blockContact ( $event ) ; } dismissBlockOptions ( $event ) { $event . preventDefault ( ) ; this . subscriptionAction = SubscriptionAction . NO_PENDING_REQUEST ; } subscriptionActionShown ( ) { if ( this . recipient . recipientType !== 'contact' ) { return false ; } return this . subscriptionAction === SubscriptionAction . PENDING_REQUEST || ( this . blockPlugin . supportsBlock$ . getValue ( ) === true && this . subscriptionAction === SubscriptionAction . SHOW_BLOCK_ACTIONS ) ; } async loadOlderMessagesBeforeViewport ( ) { if ( this . isLoadingHistory ( ) || this . isNearBottom ( ) ) { return ; } try { this . oldestVisibleMessageBeforeLoading = this . recipient . oldestMessage ; await this . loadMessages ( ) ; } catch ( e ) { this . oldestVisibleMessageBeforeLoading = null ; } } private async loadMessages ( ) { try { this . changeDetectorRef . detach ( ) ; await this . chatService . getPlugin ( MessageArchivePlugin ) . loadMostRecentUnloadedMessages ( this . recipient ) ; } finally { this . changeDetectorRef . reattach ( ) ; } } onBottom ( event ) { this . isAtBottom = event . isIntersecting ; if ( event . isIntersecting ) { this . isAtBottom = true ; } else { this . isAtBottom = false ; this . bottomLeftAt = Date . now ( ) ; } } private isNearBottom ( ) { return this . isAtBottom || Date . now ( ) - this . bottomLeftAt < 1000 ; } private isLoadingHistory ( ) { return ! ! this . oldestVisibleMessageBeforeLoading ; } getOrCreateContactWithFullJid ( message ) { if ( this . recipient . recipientType === 'contact' ) { return this . recipient ; } const roomMessage = message as RoomMessage ; let matchingContact = this . chatService . contacts$ . getValue ( ) . find ( contact => contact . jidFull . equals ( roomMessage . from ) ) ; if ( ! matchingContact ) { matchingContact = this . contactFactory . createContact ( roomMessage . from . toString ( ) , roomMessage . from . resource ) ; this . chatService . contacts$ . next ( [ matchingContact ] . concat ( this . chatService . contacts$ . getValue ( ) ) ) ; } return matchingContact ; } }