diff --git a/projects/sample-app/src/app/app.component.html b/projects/sample-app/src/app/app.component.html index b2bf74aa..b4a04516 100644 --- a/projects/sample-app/src/app/app.component.html +++ b/projects/sample-app/src/app/app.component.html @@ -45,3 +45,7 @@ + + +
{{ counter }}
+
diff --git a/projects/sample-app/src/app/app.component.ts b/projects/sample-app/src/app/app.component.ts index b2883f43..bdf94353 100644 --- a/projects/sample-app/src/app/app.component.ts +++ b/projects/sample-app/src/app/app.component.ts @@ -13,6 +13,7 @@ import { EmojiPickerContext, CustomTemplatesService, ThemeService, + AvatarContext, } from 'stream-chat-angular'; import { environment } from '../environments/environment'; @@ -26,8 +27,10 @@ export class AppComponent implements AfterViewInit { isThreadOpen = false; @ViewChild('emojiPickerTemplate') emojiPickerTemplate!: TemplateRef; + @ViewChild('avatar') avatarTemplate!: TemplateRef; themeVersion: '1' | '2'; theme$: Observable; + counter = 0; constructor( private chatService: ChatClientService, @@ -44,6 +47,7 @@ export class AppComponent implements AfterViewInit { void this.channelService.init({ type: 'messaging', members: { $in: [environment.userId] }, + // id: { $eq: '1af49475-b988-479e-9444-2a10aab707f0' }, }); this.streamI18nService.setTranslation(); this.channelService.activeParentMessage$ @@ -51,12 +55,15 @@ export class AppComponent implements AfterViewInit { .subscribe((isThreadOpen) => (this.isThreadOpen = isThreadOpen)); this.themeVersion = themeService.themeVersion; this.theme$ = themeService.theme$; + + // setInterval(() => this.counter++, 1000); } ngAfterViewInit(): void { this.customTemplateService.emojiPickerTemplate$.next( this.emojiPickerTemplate ); + // this.customTemplateService.avatarTemplate$.next(this.avatarTemplate); } closeMenu() { diff --git a/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.html b/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.html index 92f88116..e656aa84 100644 --- a/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.html +++ b/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.html @@ -19,7 +19,8 @@ @@ -70,7 +71,8 @@ @@ -202,7 +204,8 @@ @@ -249,7 +252,8 @@ @@ -314,7 +318,8 @@ > @@ -384,7 +389,8 @@ diff --git a/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.ts b/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.ts index 0e2a3ed1..645e341b 100644 --- a/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.ts +++ b/projects/stream-chat-angular/src/lib/attachment-list/attachment-list.component.ts @@ -4,8 +4,6 @@ import { HostBinding, Input, OnChanges, - OnDestroy, - OnInit, Output, SimpleChanges, TemplateRef, @@ -26,7 +24,6 @@ import { ChannelService } from '../channel.service'; import { CustomTemplatesService } from '../custom-templates.service'; import { AttachmentConfigurationService } from '../attachment-configuration.service'; import { ThemeService } from '../theme.service'; -import { Subscription } from 'rxjs'; /** * The `AttachmentList` component displays the attachments of a message @@ -36,7 +33,7 @@ import { Subscription } from 'rxjs'; templateUrl: './attachment-list.component.html', styles: [], }) -export class AttachmentListComponent implements OnChanges, OnInit, OnDestroy { +export class AttachmentListComponent implements OnChanges { /** * The id of the message the attachments belong to */ @@ -60,12 +57,6 @@ export class AttachmentListComponent implements OnChanges, OnInit, OnDestroy { imagesToView: Attachment[] = []; imagesToViewCurrentIndex = 0; themeVersion: '1' | '2'; - imageAttachmentTemplate?: TemplateRef; - videoAttachmentTemplate?: TemplateRef; - galleryAttachmentTemplate?: TemplateRef; - fileAttachmentTemplate?: TemplateRef; - cardAttachmentTemplate?: TemplateRef; - attachmentActionsTemplate?: TemplateRef; @ViewChild('modalContent', { static: true }) private modalContent!: TemplateRef; private attachmentConfigurations: Map< @@ -74,7 +65,6 @@ export class AttachmentListComponent implements OnChanges, OnInit, OnDestroy { | VideoAttachmentConfiguration | ImageAttachmentConfiguration > = new Map(); - private subscriptions: Subscription[] = []; constructor( public readonly customTemplatesService: CustomTemplatesService, @@ -84,38 +74,6 @@ export class AttachmentListComponent implements OnChanges, OnInit, OnDestroy { ) { this.themeVersion = themeService.themeVersion; } - ngOnInit(): void { - this.subscriptions.push( - this.customTemplatesService.imageAttachmentTemplate$.subscribe( - (t) => (this.imageAttachmentTemplate = t) - ) - ); - this.subscriptions.push( - this.customTemplatesService.galleryAttachmentTemplate$.subscribe( - (t) => (this.galleryAttachmentTemplate = t) - ) - ); - this.subscriptions.push( - this.customTemplatesService.videoAttachmentTemplate$.subscribe( - (t) => (this.videoAttachmentTemplate = t) - ) - ); - this.subscriptions.push( - this.customTemplatesService.fileAttachmentTemplate$.subscribe( - (t) => (this.fileAttachmentTemplate = t) - ) - ); - this.subscriptions.push( - this.customTemplatesService.cardAttachmentTemplate$.subscribe( - (t) => (this.cardAttachmentTemplate = t) - ) - ); - this.subscriptions.push( - this.customTemplatesService.attachmentActionsTemplate$.subscribe( - (t) => (this.attachmentActionsTemplate = t) - ) - ); - } ngOnChanges(changes: SimpleChanges): void { if (changes.attachments) { @@ -137,10 +95,6 @@ export class AttachmentListComponent implements OnChanges, OnInit, OnDestroy { } } - ngOnDestroy(): void { - this.subscriptions.forEach((s) => s.unsubscribe()); - } - trackByUrl(_: number, attachment: Attachment) { return ( attachment.image_url || diff --git a/projects/stream-chat-angular/src/lib/channel.service.ts b/projects/stream-chat-angular/src/lib/channel.service.ts index 2f287f4a..d1f37387 100644 --- a/projects/stream-chat-angular/src/lib/channel.service.ts +++ b/projects/stream-chat-angular/src/lib/channel.service.ts @@ -1169,13 +1169,16 @@ export class ChannelService< channel.on('message.read', (e) => { this.ngZone.run(() => { let latestMessage!: StreamMessage; - this.activeChannelMessages$.pipe(first()).subscribe((messages) => { + let messages!: StreamMessage[]; + this.activeChannelMessages$.pipe(first()).subscribe((m) => { + messages = m; latestMessage = messages[messages.length - 1]; }); if (!latestMessage || !e.user) { return; } latestMessage.readBy = getReadBy(latestMessage, channel); + messages[messages.length - 1] = { ...latestMessage }; this.activeChannelMessagesSubject.next( this.activeChannelMessagesSubject.getValue() @@ -1251,14 +1254,17 @@ export class ChannelService< ) .pipe(first()) .subscribe((m) => (messages = m)); - const message = messages.find((m) => m.id === e?.message?.id); - if (!message) { + const messageIndex = messages.findIndex((m) => m.id === e?.message?.id); + if (messageIndex === -1) { return; } + const message = messages[messageIndex]; message.reaction_counts = { ...e.message?.reaction_counts }; message.reaction_scores = { ...e.message?.reaction_scores }; message.latest_reactions = [...(e.message?.latest_reactions || [])]; message.own_reactions = [...(e.message?.own_reactions || [])]; + + messages[messageIndex] = { ...message }; isThreadMessage ? this.activeThreadMessagesSubject.next([...messages]) : this.activeChannelMessagesSubject.next([...messages]); diff --git a/projects/stream-chat-angular/src/lib/chat-client.service.ts b/projects/stream-chat-angular/src/lib/chat-client.service.ts index a3f0d46d..64ef8200 100644 --- a/projects/stream-chat-angular/src/lib/chat-client.service.ts +++ b/projects/stream-chat-angular/src/lib/chat-client.service.ts @@ -118,7 +118,9 @@ export class ChatClientService< ); throw error; } - this.userSubject.next(this.chatClient.user); + this.userSubject.next( + this.chatClient.user ? { ...this.chatClient.user } : undefined + ); const sdkPrefix = 'stream-chat-angular'; if (!this.chatClient.getUserAgent().includes(sdkPrefix)) { this.chatClient.setUserAgent( diff --git a/projects/stream-chat-angular/src/lib/message-actions-box/message-actions-box.component.html b/projects/stream-chat-angular/src/lib/message-actions-box/message-actions-box.component.html index 9e84fd9f..820a77e3 100644 --- a/projects/stream-chat-angular/src/lib/message-actions-box/message-actions-box.component.html +++ b/projects/stream-chat-angular/src/lib/message-actions-box/message-actions-box.component.html @@ -10,7 +10,8 @@ > @@ -37,7 +38,7 @@ @@ -72,7 +73,7 @@ diff --git a/projects/stream-chat-angular/src/lib/message-actions-box/message-actions-box.component.ts b/projects/stream-chat-angular/src/lib/message-actions-box/message-actions-box.component.ts index 941f4349..8f2534a2 100644 --- a/projects/stream-chat-angular/src/lib/message-actions-box/message-actions-box.component.ts +++ b/projects/stream-chat-angular/src/lib/message-actions-box/message-actions-box.component.ts @@ -3,13 +3,12 @@ import { EventEmitter, Input, OnChanges, - OnDestroy, Output, SimpleChanges, TemplateRef, ViewChild, } from '@angular/core'; -import { Observable, Subject, Subscription } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; import { ChannelService } from '../channel.service'; import { ChatClientService } from '../chat-client.service'; import { CustomTemplatesService } from '../custom-templates.service'; @@ -30,7 +29,7 @@ import { templateUrl: './message-actions-box.component.html', styles: [], }) -export class MessageActionsBoxComponent implements OnChanges, OnDestroy { +export class MessageActionsBoxComponent implements OnChanges { /** * Indicates if the list should be opened or closed. Adding a UI element to open and close the list is the parent's component responsibility. * @deprecated No need for this since [theme-v2](../theming/introduction.mdx) @@ -66,7 +65,6 @@ export class MessageActionsBoxComponent implements OnChanges, OnDestroy { | TemplateRef | undefined; modalTemplate: TemplateRef | undefined; - subscriptions: Subscription[] = []; visibleMessageActionItems: (MessageActionItem | CustomMessageActionItem)[] = []; sendMessage$: Observable; @@ -78,23 +76,8 @@ export class MessageActionsBoxComponent implements OnChanges, OnDestroy { private chatClientService: ChatClientService, private notificationService: NotificationService, private channelService: ChannelService, - private customTemplatesService: CustomTemplatesService + public readonly customTemplatesService: CustomTemplatesService ) { - this.subscriptions.push( - this.customTemplatesService.messageInputTemplate$.subscribe( - (template) => (this.messageInputTemplate = template) - ) - ); - this.subscriptions.push( - this.customTemplatesService.messageActionsBoxItemTemplate$.subscribe( - (template) => (this.messageActionItemTemplate = template) - ) - ); - this.subscriptions.push( - this.customTemplatesService.modalTemplate$.subscribe( - (template) => (this.modalTemplate = template) - ) - ); this.messageActionItems = [ { actionName: 'quote', @@ -185,10 +168,6 @@ export class MessageActionsBoxComponent implements OnChanges, OnDestroy { } } - ngOnDestroy(): void { - this.subscriptions.forEach((s) => s.unsubscribe()); - } - getActionLabel( actionLabelOrTranslationKey: ((message: StreamMessage) => string) | string ) { diff --git a/projects/stream-chat-angular/src/lib/message-list/message-list.component.ts b/projects/stream-chat-angular/src/lib/message-list/message-list.component.ts index 5f0f1b4b..f25a0b45 100644 --- a/projects/stream-chat-angular/src/lib/message-list/message-list.component.ts +++ b/projects/stream-chat-angular/src/lib/message-list/message-list.component.ts @@ -54,7 +54,6 @@ export class MessageListComponent */ @Input() messageOptionsTrigger: 'message-row' | 'message-bubble' = 'message-row'; - typingIndicatorTemplate: TemplateRef | undefined; /** * You can hide the "jump to latest" button while scrolling. A potential use-case for this input would be to [workaround a known issue on iOS Safar](https://github.com/GetStream/stream-chat-angular/issues/418) */ @@ -81,6 +80,7 @@ export class MessageListComponent * You can turn on and off the loading indicator that signals to users that more messages are being loaded to the message list */ @Input() displayLoadingIndicator = true; + typingIndicatorTemplate: TemplateRef | undefined; messageTemplate: TemplateRef | undefined; customDateSeparatorTemplate: TemplateRef | undefined; customnewMessagesIndicatorTemplate: TemplateRef | undefined; @@ -358,6 +358,7 @@ export class MessageListComponent scrollToBottom(): void { this.scrollContainer.nativeElement.scrollTop = this.scrollContainer.nativeElement.scrollHeight; + this.forceRepaint(); } scrollToTop() { @@ -460,6 +461,14 @@ export class MessageListComponent this.scrollContainer.nativeElement.scrollTop = (this.prevScrollTop || 0) + (this.scrollContainer.nativeElement.scrollHeight - this.containerHeight!); + this.forceRepaint(); + } + + private forceRepaint() { + // Solves the issue of empty screen on iOS Safari when scrolling + this.scrollContainer.nativeElement.style.display = 'none'; + this.scrollContainer.nativeElement.offsetHeight; // no need to store this anywhere, the reference is enough + this.scrollContainer.nativeElement.style.display = ''; } private getScrollPosition(): 'top' | 'bottom' | 'middle' { diff --git a/projects/stream-chat-angular/src/lib/message/message.component.html b/projects/stream-chat-angular/src/lib/message/message.component.html index b4791e93..9294bfce 100644 --- a/projects/stream-chat-angular/src/lib/message/message.component.html +++ b/projects/stream-chat-angular/src/lib/message/message.component.html @@ -82,7 +82,8 @@ @@ -158,7 +159,7 @@ @@ -249,7 +250,7 @@ @@ -306,7 +307,7 @@ @@ -378,7 +379,7 @@ @@ -475,7 +476,7 @@ @@ -511,7 +512,7 @@ @@ -545,7 +546,7 @@ @@ -641,7 +642,7 @@ diff --git a/projects/stream-chat-angular/src/lib/message/message.component.spec.ts b/projects/stream-chat-angular/src/lib/message/message.component.spec.ts index c5951081..875f80d8 100644 --- a/projects/stream-chat-angular/src/lib/message/message.component.spec.ts +++ b/projects/stream-chat-angular/src/lib/message/message.component.spec.ts @@ -18,7 +18,7 @@ import { AttachmentListComponent } from '../attachment-list/attachment-list.comp import { MessageReactionsComponent } from '../message-reactions/message-reactions.component'; import { TranslateModule } from '@ngx-translate/core'; import { ChannelService } from '../channel.service'; -import { SimpleChange } from '@angular/core'; +import { ChangeDetectionStrategy, SimpleChange } from '@angular/core'; import { AvatarPlaceholderComponent } from '../avatar-placeholder/avatar-placeholder.component'; import { ThemeService } from '../theme.service'; import { of } from 'rxjs'; @@ -99,6 +99,8 @@ describe('MessageComponent', () => { useValue: { themeVersion: '2' }, }, ], + }).overrideComponent(MessageComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, }); fixture = TestBed.createComponent(MessageComponent); component = fixture.componentInstance; @@ -167,6 +169,7 @@ describe('MessageComponent', () => { 'send-reaction', 'send-reply', ]; + component.ngOnChanges({ enabledMessageActions: {} as SimpleChange }); fixture.detectChanges(); }); @@ -175,6 +178,7 @@ describe('MessageComponent', () => { ...component.message, ...{ reaction_counts: { wow: 1 } }, } as StreamMessage; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); const container = queryContainer(); let classList = container?.classList; @@ -198,6 +202,7 @@ describe('MessageComponent', () => { component.message.user = { id: 'notcurrentUser', name: 'Jane' }; component.message.reaction_counts = {}; component.message.reply_count = 3; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); classList = container?.classList; @@ -236,6 +241,7 @@ describe('MessageComponent', () => { it('if message is delivered', () => { component.isLastSentMessage = true; component.message = { ...message, ...{ readBy: [] } }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); const deliveredIndicator = queryDeliveredIndicator(); const icon = nativeElement.querySelector( @@ -275,6 +281,7 @@ describe('MessageComponent', () => { it(`should display delivered icon, if user can't receive delivered events`, () => { component.isLastSentMessage = true; component.enabledMessageActions = []; + component.ngOnChanges({ enabledMessageActions: {} as SimpleChange }); fixture.detectChanges(); const readIndicator = queryReadIndicator(); const deliveredIndicator = queryDeliveredIndicator(); @@ -295,6 +302,7 @@ describe('MessageComponent', () => { readBy, }, }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); const readByCounter = queryReadByCounter(); @@ -327,6 +335,7 @@ describe('MessageComponent', () => { ...message, ...{ user: { id: 'id', name: senderName, image: senderImage } }, }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); const avatar = queryAvatar(); @@ -348,6 +357,7 @@ describe('MessageComponent', () => { ...message, ...{ readBy: [userWithoutName] }, }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); expect(queryAvatar()?.name).toContain(currentUser.id); @@ -357,6 +367,7 @@ describe('MessageComponent', () => { ...message, ...{ user: userWithoutName }, }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); expect(queryAvatar()?.name).toContain(userWithoutName.id); @@ -385,6 +396,7 @@ describe('MessageComponent', () => { ...message, ...{ errorStatusCode: 403, status: 'failed' }, }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); const errorMessage = queryErrorMessage(); @@ -402,6 +414,7 @@ describe('MessageComponent', () => { ...message, ...{ errorStatusCode: 500, status: 'failed' }, }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); const errorMessage = queryErrorMessage(); @@ -423,6 +436,7 @@ describe('MessageComponent', () => { ...message, ...{ type: 'error' }, }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); const clientErrorMessage = queryClientErrorMessage(); @@ -434,6 +448,7 @@ describe('MessageComponent', () => { it('should display message sender and date', () => { const sender = { id: 'sender', name: 'Jack' }; component.message = { ...message, ...{ user: sender } }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); const senderElement = querySender(); const dateElement = queryDate(); @@ -453,6 +468,7 @@ describe('MessageComponent', () => { describe('should not display message options', () => { it('if message is being sent', () => { component.message = { ...message, ...{ status: 'sending' } }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); expect(component.areOptionsVisible).toBe(false); @@ -461,24 +477,28 @@ describe('MessageComponent', () => { it('if message sending failed', () => { message.status = 'failed'; + component.ngOnChanges({ message: {} as SimpleChange }); expect(component.areOptionsVisible).toBe(false); }); it('if message is unsent', () => { message.type = 'error'; + component.ngOnChanges({ message: {} as SimpleChange }); expect(component.areOptionsVisible).toBe(false); }); it('if message is system message', () => { message.type = 'system'; + component.ngOnChanges({ message: {} as SimpleChange }); expect(component.areOptionsVisible).toBe(false); }); it('if message is ephemeral message', () => { message.type = 'ephemeral'; + component.ngOnChanges({ message: {} as SimpleChange }); expect(component.areOptionsVisible).toBe(false); }); @@ -490,6 +510,7 @@ describe('MessageComponent', () => { it('should display message actions for regular messages', () => { component.enabledMessageActions = ['delete']; + component.ngOnChanges({ enabledMessageActions: {} as SimpleChange }); fixture.detectChanges(); expect(queryActionIcon()).not.toBeNull(); @@ -497,6 +518,7 @@ describe('MessageComponent', () => { it(`shouldn't display message actions if there are no enabled message actions`, () => { component.enabledMessageActions = []; + component.ngOnChanges({ enabledMessageActions: {} as SimpleChange }); fixture.detectChanges(); expect(queryActionIcon()).toBeNull(); @@ -504,6 +526,7 @@ describe('MessageComponent', () => { it(`shouldn't display message actions if there is no visible message action`, () => { component.enabledMessageActions = ['flag-message']; + component.ngOnChanges({ enabledMessageActions: {} as SimpleChange }); fixture.detectChanges(); expect(queryActionIcon()).toBeNull(); @@ -511,6 +534,7 @@ describe('MessageComponent', () => { it('should open and close message actions box', () => { component.enabledMessageActions = ['update-own-message', 'flag-message']; + component.ngOnChanges({ enabledMessageActions: {} as SimpleChange }); fixture.detectChanges(); expect(messageActionsBoxComponent.isOpen).toBeFalse(); @@ -524,6 +548,7 @@ describe('MessageComponent', () => { it('should close message actions box on mouseleave event', () => { component.enabledMessageActions = ['update-own-message', 'flag-message']; component.isActionBoxOpen = true; + component.ngOnChanges({ enabledMessageActions: {} as SimpleChange }); fixture.detectChanges(); queryContainer()?.dispatchEvent(new Event('mouseleave')); @@ -542,6 +567,7 @@ describe('MessageComponent', () => { expect(messageActionsBoxComponent.isMine).toBeTrue(); component.message = { ...message, ...{ user: { id: 'notcurrentuser' } } }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); expect(messageActionsBoxComponent.isMine).toBeFalse(); @@ -587,6 +613,7 @@ describe('MessageComponent', () => { ...{ attachments }, }; component.message.parent_id = 'parent-id'; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); const attachmentComponent = queryAttachmentComponent(); @@ -668,6 +695,7 @@ describe('MessageComponent', () => { it(`shouldn't display empty text`, () => { component.message = { ...component.message!, ...{ text: '' } }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); expect(queryText()).toBeNull(); @@ -688,6 +716,7 @@ describe('MessageComponent', () => { it('should resend message, if sending is failed', () => { component.message = { ...component.message!, status: 'failed' }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); spyOn(component, 'resendMessage'); queryMessageInner()!.click(); @@ -697,6 +726,7 @@ describe('MessageComponent', () => { it(`shouldn't resend message, if message could be sent`, () => { component.message = { ...component.message!, status: 'received' }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); spyOn(component, 'resendMessage'); queryMessageInner()!.click(); @@ -710,6 +740,7 @@ describe('MessageComponent', () => { status: 'failed', errorStatusCode: 403, }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); spyOn(component, 'resendMessage'); queryMessageInner()!.click(); @@ -727,6 +758,7 @@ describe('MessageComponent', () => { expect(queryDeletedMessageContainer()).toBeNull(); component.message = { ...message, deleted_at: new Date().toISOString() }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); expect(queryDeletedMessageContainer()).not.toBeNull(); @@ -951,6 +983,7 @@ describe('MessageComponent', () => { expect(queryReplyCountButton()).toBeNull(); component.message = { ...message, reply_count: 1 }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); expect(queryReplyCountButton()).not.toBeNull(); @@ -961,6 +994,7 @@ describe('MessageComponent', () => { expect(queryReplyCountButton()).toBeNull(); component.message = { ...message, reply_count: 1 }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); queryReplyCountButton()?.click(); fixture.detectChanges(); @@ -971,7 +1005,10 @@ describe('MessageComponent', () => { it(`shouldn't display reply count for parent messages if user doesn't have the necessary capability`, () => { component.message = { ...message, reply_count: 1 }; component.enabledMessageActions = []; - component.ngOnChanges({ enabledMessageActions: {} as SimpleChange }); + component.ngOnChanges({ + message: {} as SimpleChange, + enabledMessageActions: {} as SimpleChange, + }); fixture.detectChanges(); expect(queryReplyCountButton()).toBeNull(); @@ -1003,6 +1040,7 @@ describe('MessageComponent', () => { component.mode = 'thread'; component.enabledMessageActions = ['update-own-message', 'delete']; component.ngOnChanges({ + mode: {} as SimpleChange, enabledMessageActions: {} as SimpleChange, }); fixture.detectChanges(); @@ -1038,6 +1076,7 @@ describe('MessageComponent', () => { component.enabledMessageActions = ['send-reply']; component.message!.parent_id = 'parentMessage'; component.ngOnChanges({ + message: {} as SimpleChange, enabledMessageActions: {} as SimpleChange, }); fixture.detectChanges(); @@ -1049,6 +1088,7 @@ describe('MessageComponent', () => { component.enabledMessageActions = ['update-any-message']; component.message!.parent_id = 'parentMessage'; component.ngOnChanges({ + message: {} as SimpleChange, enabledMessageActions: {} as SimpleChange, }); fixture.detectChanges(); @@ -1060,6 +1100,7 @@ describe('MessageComponent', () => { component.enabledMessageActions = ['send-reaction']; component.message!.parent_id = 'parentMessage'; component.ngOnChanges({ + message: {} as SimpleChange, enabledMessageActions: {} as SimpleChange, }); fixture.detectChanges(); @@ -1149,6 +1190,7 @@ describe('MessageComponent', () => { expect(quotedMessageContainer?.classList).toContain('mine'); component.message = { ...component.message!, user: { id: 'otheruser' } }; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); expect(quotedMessageContainer?.classList).not.toContain('mine'); @@ -1159,6 +1201,7 @@ describe('MessageComponent', () => { { image_url: 'http://url/to/image', type: 'image' }, ]; component.message!.text = undefined; + component.ngOnChanges({ message: {} as SimpleChange }); fixture.detectChanges(); expect(queryAttachmentComponent()).toBeDefined(); diff --git a/projects/stream-chat-angular/src/lib/message/message.component.ts b/projects/stream-chat-angular/src/lib/message/message.component.ts index 718a6605..596f91ca 100644 --- a/projects/stream-chat-angular/src/lib/message/message.component.ts +++ b/projects/stream-chat-angular/src/lib/message/message.component.ts @@ -9,6 +9,7 @@ import { OnDestroy, OnInit, ChangeDetectorRef, + ChangeDetectionStrategy, } from '@angular/core'; import { Attachment, UserResponse } from 'stream-chat'; import { ChannelService } from '../channel.service'; @@ -27,7 +28,7 @@ import { SystemMessageContext, } from '../types'; import emojiRegex from 'emoji-regex'; -import { Subscription } from 'rxjs'; +import { Observable, Subscription } from 'rxjs'; import { CustomTemplatesService } from '../custom-templates.service'; import { listUsers } from '../list-users'; import { ThemeService } from '../theme.service'; @@ -47,6 +48,7 @@ type MessagePart = { selector: 'stream-message', templateUrl: './message.component.html', styles: [], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class MessageComponent implements OnInit, OnChanges, OnDestroy { /** @@ -81,24 +83,51 @@ export class MessageComponent implements OnInit, OnChanges, OnDestroy { isReactionSelectorOpen = false; visibleMessageActionsCount = 0; messageTextParts: MessagePart[] = []; - mentionTemplate: TemplateRef | undefined; - customDeliveredStatusTemplate: - | TemplateRef - | undefined; - customSendingStatusTemplate: TemplateRef | undefined; - customReadStatusTemplate: TemplateRef | undefined; - attachmentListTemplate: TemplateRef | undefined; - messageActionsBoxTemplate: TemplateRef | undefined; - messageReactionsTemplate: TemplateRef | undefined; - systemMessageTemplate: TemplateRef | undefined; + mentionTemplate$?: Observable< + TemplateRef | undefined + >; + deliveredStatusTemplate$?: Observable< + TemplateRef | undefined + >; + sendingStatusTemplate$?: Observable< + TemplateRef | undefined + >; + readStatusTemplate$?: Observable | undefined>; + attachmentListTemplate$?: Observable< + TemplateRef | undefined + >; + messageActionsBoxTemplate$?: Observable< + TemplateRef | undefined + >; + messageReactionsTemplate$?: Observable< + TemplateRef | undefined + >; + systemMessageTemplate$?: Observable< + TemplateRef | undefined + >; popperTriggerClick = NgxPopperjsTriggers.click; popperTriggerHover = NgxPopperjsTriggers.hover; popperPlacementAuto = NgxPopperjsPlacements.AUTO; shouldDisplayTranslationNotice = false; displayedMessageTextContent: 'original' | 'translation' = 'original'; imageAttachmentModalState: 'opened' | 'closed' = 'closed'; + shouldDisplayThreadLink = false; + isSentByCurrentUser = false; + readByText = ''; + lastReadUser: UserResponse | undefined = undefined; + isOnlyReadByMe = false; + isReadByMultipleUsers = false; + isMessageDeliveredAndRead = false; + parsedDate = ''; + areOptionsVisible = false; + hasAttachment = false; + hasReactions = false; + replyCountParam: { replyCount: number | undefined } = { + replyCount: undefined, + }; + canDisplayReadStatus = false; private quotedMessageAttachments: Attachment[] | undefined; - private user: UserResponse | undefined; + user: UserResponse | undefined; private subscriptions: Subscription[] = []; @ViewChild('container') private container: | ElementRef @@ -118,49 +147,28 @@ export class MessageComponent implements OnInit, OnChanges, OnDestroy { ngOnInit(): void { this.subscriptions.push( this.chatClientService.user$.subscribe((u) => { - this.user = u; + if (u !== this.user) { + this.user = u; + this.setIsSentByCurrentUser(); + this.setLastReadUser(); + this.cdRef.detectChanges(); + } }) ); - this.subscriptions.push( - this.customTemplatesService.mentionTemplate$.subscribe( - (template) => (this.mentionTemplate = template) - ) - ); - this.subscriptions.push( - this.customTemplatesService.attachmentListTemplate$.subscribe( - (template) => (this.attachmentListTemplate = template) - ) - ); - this.subscriptions.push( - this.customTemplatesService.messageActionsBoxTemplate$.subscribe( - (template) => (this.messageActionsBoxTemplate = template) - ) - ); - this.subscriptions.push( - this.customTemplatesService.messageReactionsTemplate$.subscribe( - (template) => (this.messageReactionsTemplate = template) - ) - ); - this.subscriptions.push( - this.customTemplatesService.deliveredStatusTemplate$.subscribe( - (template) => (this.customDeliveredStatusTemplate = template) - ) - ); - this.subscriptions.push( - this.customTemplatesService.sendingStatusTemplate$.subscribe( - (template) => (this.customSendingStatusTemplate = template) - ) - ); - this.subscriptions.push( - this.customTemplatesService.readStatusTemplate$.subscribe( - (template) => (this.customReadStatusTemplate = template) - ) - ); - this.subscriptions.push( - this.customTemplatesService.systemMessageTemplate$.subscribe( - (template) => (this.systemMessageTemplate = template) - ) - ); + this.mentionTemplate$ = this.customTemplatesService.mentionTemplate$; + this.attachmentListTemplate$ = + this.customTemplatesService.attachmentListTemplate$; + this.messageActionsBoxTemplate$ = + this.customTemplatesService.messageActionsBoxTemplate$; + this.messageReactionsTemplate$ = + this.customTemplatesService.messageReactionsTemplate$; + this.deliveredStatusTemplate$ = + this.customTemplatesService.deliveredStatusTemplate$; + this.sendingStatusTemplate$ = + this.customTemplatesService.sendingStatusTemplate$; + this.readStatusTemplate$ = this.customTemplatesService.readStatusTemplate$; + this.systemMessageTemplate$ = + this.customTemplatesService.systemMessageTemplate$; } ngOnChanges(changes: SimpleChanges): void { @@ -173,98 +181,71 @@ export class MessageComponent implements OnInit, OnChanges, OnDestroy { originalAttachments && originalAttachments.length ? [originalAttachments[0]] : []; + this.setIsSentByCurrentUser(); + this.setLastReadUser(); + this.readByText = this.message?.readBy + ? listUsers(this.message.readBy) + : ''; + this.isOnlyReadByMe = !!( + this.message && + this.message.readBy && + this.message.readBy.length === 0 + ); + this.isReadByMultipleUsers = !!( + this.message && + this.message.readBy && + this.message.readBy.length > 1 + ); + this.isMessageDeliveredAndRead = !!( + this.message && + this.message.readBy && + this.message.status === 'received' && + this.message.readBy.length > 0 + ); + this.parsedDate = + (this.message && + this.message.created_at && + this.dateParser.parseDateTime(this.message.created_at)) || + ''; + this.hasAttachment = + !!this.message?.attachments && !!this.message.attachments.length; + this.hasReactions = + !!this.message?.reaction_counts && + Object.keys(this.message.reaction_counts).length > 0; + this.replyCountParam = { replyCount: this.message?.reply_count }; } if (changes.enabledMessageActions) { this.canReactToMessage = this.enabledMessageActions.indexOf('send-reaction') !== -1; this.canReceiveReadEvents = this.enabledMessageActions.indexOf('read-events') !== -1; + this.canDisplayReadStatus = + this.canReceiveReadEvents !== false && + this.enabledMessageActions.indexOf('read-events') !== -1; } - } - - ngOnDestroy(): void { - this.subscriptions.forEach((s) => s.unsubscribe()); - } - - get shouldDisplayThreadLink() { - return ( - !!this.message?.reply_count && - this.mode !== 'thread' && - this.enabledMessageActions.indexOf('send-reply') !== -1 - ); - } - - get isSentByCurrentUser() { - return this.message?.user?.id === this.user?.id; - } - - get readByText() { - return listUsers(this.message!.readBy); - } - - get lastReadUser() { - return this.message?.readBy.filter((u) => u.id !== this.user?.id)[0]; - } - - get isOnlyReadByMe() { - return this.message && this.message.readBy.length === 0; - } - - get isReadByMultipleUsers() { - return this.message && this.message.readBy.length > 1; - } - - get isMessageDeliveredAndRead() { - return ( - this.message && - this.message.readBy && - this.message.status === 'received' && - this.message.readBy.length > 0 - ); - } - - get parsedDate() { - if (!this.message || !this.message?.created_at) { - return; + if (changes.message || changes.enabledMessageActions || changes.mode) { + this.shouldDisplayThreadLink = + !!this.message?.reply_count && + this.mode !== 'thread' && + this.enabledMessageActions.indexOf('send-reply') !== -1; } - return this.dateParser.parseDateTime(this.message.created_at); - } - - get areOptionsVisible() { - if (!this.message) { - return false; + if (changes.message || changes.mode) { + this.areOptionsVisible = this.message + ? !( + !this.message.type || + this.message.type === 'error' || + this.message.type === 'system' || + this.message.type === 'ephemeral' || + this.message.status === 'failed' || + this.message.status === 'sending' || + (this.mode === 'thread' && !this.message.parent_id) + ) + : false; } - return !( - !this.message.type || - this.message.type === 'error' || - this.message.type === 'system' || - this.message.type === 'ephemeral' || - this.message.status === 'failed' || - this.message.status === 'sending' || - (this.mode === 'thread' && !this.message.parent_id) - ); } - get hasAttachment() { - return !!this.message?.attachments && !!this.message.attachments.length; - } - - get hasReactions() { - return ( - !!this.message?.reaction_counts && - Object.keys(this.message.reaction_counts).length > 0 - ); - } - - get replyCountParam() { - return { replyCount: this.message?.reply_count }; - } - - get canDisplayReadStatus() { - return ( - this.canReceiveReadEvents !== false && - this.enabledMessageActions.indexOf('read-events') !== -1 - ); + ngOnDestroy(): void { + this.subscriptions.forEach((s) => s.unsubscribe()); } getAttachmentListContext(): AttachmentListContext { @@ -479,4 +460,14 @@ export class MessageComponent implements OnInit, OnChanges, OnDestroy { return content; } + + private setIsSentByCurrentUser() { + this.isSentByCurrentUser = this.message?.user?.id === this.user?.id; + } + + private setLastReadUser() { + this.lastReadUser = this.message?.readBy?.filter( + (u) => u.id !== this.user?.id + )[0]; + } }