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 @@
0">
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];
+ }
}