Skip to content

Commit

Permalink
feat: empty message list placeholder
Browse files Browse the repository at this point in the history
  • Loading branch information
szuperaz committed Nov 9, 2023
1 parent 01507da commit 92845a2
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 92 deletions.
13 changes: 11 additions & 2 deletions projects/stream-chat-angular/src/lib/custom-templates.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,17 @@ export class CustomTemplatesService {
newMessagesIndicatorTemplate$ = new BehaviorSubject<
TemplateRef<void> | undefined
>(undefined);
emptyChannelPlaceholder$ = new BehaviorSubject<
TemplateRef<{ mode: 'main' | 'thread' }> | undefined
/**
* The template to show if the main message list is empty
*/
emptyMainMessageListPlaceholder$ = new BehaviorSubject<
TemplateRef<void> | undefined
>(undefined);
/**
* The template to show if the thread message list is empty
*/
emptyThreadMessageListPlaceholder$ = new BehaviorSubject<
TemplateRef<void> | undefined
>(undefined);

constructor() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
class="str-chat__list"
style="overscroll-behavior: none"
>
<ng-container *ngIf="mode === 'main' && isEmpty && emptyListTemplate">
<ng-container *ngTemplateOutlet="emptyListTemplate"></ng-container>
</ng-container>
<div class="str-chat__reverse-infinite-scroll str-chat__message-list-scroll">
<ul
class="str-chat__ul"
Expand All @@ -28,6 +31,9 @@
replies' | translate:replyCountParam)}}
</div>
</li>
<ng-container *ngIf="mode === 'thread' && isEmpty && emptyListTemplate">
<ng-container *ngTemplateOutlet="emptyListTemplate"></ng-container>
</ng-container>
<stream-loading-indicator
data-testid="top-loading-indicator"
*ngIf="
Expand All @@ -36,94 +42,76 @@
></stream-loading-indicator>
<ng-container *ngIf="messages$ | async as messages">
<ng-container
*ngIf="
messages.length > 0 || !emptyChannelPlaceholderTemplate;
else noMessages
*ngFor="
let message of messages;
let i = index;
let isFirst = first;
let isLast = last;
trackBy: trackByMessageId
"
>
<ng-container *ngIf="isFirst">
<ng-container
*ngTemplateOutlet="
dateSeparator;
context: {
date: message.created_at,
parsedDate: parseDate(message.created_at),
isNewMessage: false
}
"
></ng-container>
</ng-container>
<li
tabindex="0"
data-testclass="message"
class="str-chat__li str-chat__li--{{ groupStyles[i] }}"
id="{{ message.id }}"
>
<ng-container
*ngTemplateOutlet="
messageTemplateContainer;
context: { message: message, index: i }
"
></ng-container>
</li>
<ng-container
*ngFor="
let message of messages;
let i = index;
let isFirst = first;
let isLast = last;
trackBy: trackByMessageId
*ngIf="
(lastReadMessageId === message.id &&
!isLast &&
direction === 'bottom-to-top' &&
!isSentByCurrentUser(messages[i + 1]) &&
(!displayDateSeparator || !isNextMessageOnSeparateDate[i])) ||
(direction === 'top-to-bottom' &&
!isLast &&
!isSentByCurrentUser(message) &&
lastReadMessageId === messages[i + 1].id)
"
>
<ng-container *ngIf="isFirst">
<ng-container
*ngTemplateOutlet="
dateSeparator;
context: {
date: message.created_at,
parsedDate: parseDate(message.created_at),
isNewMessage: false
}
"
></ng-container>
</ng-container>
<li
tabindex="0"
data-testclass="message"
class="str-chat__li str-chat__li--{{ groupStyles[i] }}"
id="{{ message.id }}"
>
<ng-container
*ngTemplateOutlet="
messageTemplateContainer;
context: { message: message, index: i }
"
></ng-container>
</li>
<ng-container
*ngIf="
(lastReadMessageId === message.id &&
!isLast &&
direction === 'bottom-to-top' &&
!isSentByCurrentUser(messages[i + 1]) &&
(!displayDateSeparator || !isNextMessageOnSeparateDate[i])) ||
(direction === 'top-to-bottom' &&
!isLast &&
!isSentByCurrentUser(message) &&
lastReadMessageId === messages[i + 1].id)
*ngTemplateOutlet="
customnewMessagesIndicatorTemplate ||
defaultNewMessagesIndicator
"
></ng-container>
</ng-container>
<ng-container *ngIf="isNextMessageOnSeparateDate[i]">
<ng-container
*ngTemplateOutlet="
dateSeparator;
context: {
date: messages[i + 1].created_at,
parsedDate: parseDate(messages[i + 1].created_at),
isNewMessage:
(direction === 'bottom-to-top' &&
message.id === lastReadMessageId &&
!isSentByCurrentUser(messages[i + 1])) ||
(direction === 'top-to-bottom' && false)
}
"
>
<ng-container
*ngTemplateOutlet="
customnewMessagesIndicatorTemplate ||
defaultNewMessagesIndicator
"
></ng-container>
</ng-container>
<ng-container *ngIf="isNextMessageOnSeparateDate[i]">
<ng-container
*ngTemplateOutlet="
dateSeparator;
context: {
date: messages[i + 1].created_at,
parsedDate: parseDate(messages[i + 1].created_at),
isNewMessage:
(direction === 'bottom-to-top' &&
message.id === lastReadMessageId &&
!isSentByCurrentUser(messages[i + 1])) ||
(direction === 'top-to-bottom' && false)
}
"
></ng-container>
</ng-container>
></ng-container>
</ng-container>
</ng-container>
<ng-template #noMessages>
<ng-container
*ngTemplateOutlet="
emptyChannelPlaceholderTemplate;
context: { mode: mode }
"
></ng-container>
<ng-template #defaultTemplate>
<div>There are no messages</div>
</ng-template>
</ng-template>
</ng-container>
<stream-loading-indicator
data-testid="bottom-loading-indicator"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
AfterViewChecked,
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
HostBinding,
Expand Down Expand Up @@ -87,13 +88,11 @@ export class MessageListComponent
messageTemplate: TemplateRef<MessageContext> | undefined;
customDateSeparatorTemplate: TemplateRef<DateSeparatorContext> | undefined;
customnewMessagesIndicatorTemplate: TemplateRef<void> | undefined;
emptyChannelPlaceholderTemplate: TemplateRef<{
mode: 'main' | 'thread';
}> | null = null;
emptyMainMessageListTemplate: TemplateRef<void> | null = null;
emptyThreadMessageListTemplate: TemplateRef<void> | null = null;
messages$!: Observable<StreamMessage[]>;
enabledMessageActions: string[] = [];
@HostBinding('class') private class =
'str-chat-angular__main-panel-inner str-chat-angular__message-list-host str-chat__main-panel-inner';
isEmpty = true;
unreadMessageCount = 0;
isUserScrolled: boolean | undefined;
groupStyles: GroupStyle[] = [];
Expand Down Expand Up @@ -128,12 +127,20 @@ export class MessageListComponent
private channelId?: string;
private parsedDates = new Map<Date, string>();

@HostBinding('class')
private get class() {
return `str-chat-angular__main-panel-inner str-chat-angular__message-list-host str-chat__main-panel-inner ${
this.isEmpty ? 'str-chat-angular__message-list-host--empty' : ''
}`;
}

constructor(
private channelService: ChannelService,
private chatClientService: ChatClientService,
private customTemplatesService: CustomTemplatesService,
private dateParser: DateParserService,
private ngZone: NgZone
private ngZone: NgZone,
private cdRef: ChangeDetectorRef
) {
this.subscriptions.push(
this.channelService.activeChannel$.subscribe((channel) => {
Expand Down Expand Up @@ -222,11 +229,6 @@ export class MessageListComponent
(template) => (this.typingIndicatorTemplate = template)
)
);
this.subscriptions.push(
this.customTemplatesService.emptyChannelPlaceholder$.subscribe(
(template) => (this.emptyChannelPlaceholderTemplate = template || null)
)
);
this.usersTypingInChannel$ = this.channelService.usersTypingInChannel$;
this.usersTypingInThread$ = this.channelService.usersTypingInThread$;
}
Expand Down Expand Up @@ -277,6 +279,28 @@ export class MessageListComponent
}
})
);
this.subscriptions.push(
this.customTemplatesService.emptyMainMessageListPlaceholder$.subscribe(
(template) => {
const isChanged = this.emptyMainMessageListTemplate !== template;
this.emptyMainMessageListTemplate = template || null;
if (isChanged) {
this.cdRef.detectChanges();
}
}
)
);
this.subscriptions.push(
this.customTemplatesService.emptyThreadMessageListPlaceholder$.subscribe(
(template) => {
const isChanged = this.emptyThreadMessageListTemplate !== template;
this.emptyThreadMessageListTemplate = template || null;
if (isChanged) {
this.cdRef.detectChanges();
}
}
)
);
}

ngAfterViewChecked() {
Expand Down Expand Up @@ -459,6 +483,12 @@ export class MessageListComponent
return { replyCount: this.parentMessage?.reply_count };
}

get emptyListTemplate() {
return this.mode === 'main'
? this.emptyMainMessageListTemplate
: this.emptyThreadMessageListTemplate;
}

private preserveScrollbarPosition() {
this.scrollContainer.nativeElement.scrollTop =
(this.prevScrollTop || 0) +
Expand Down Expand Up @@ -516,6 +546,10 @@ export class MessageListComponent
this.resetScrollState();
return;
}
if (this.isEmpty) {
// cdRef.detectChanges() isn't enough here, test will fail
setTimeout(() => (this.isEmpty = false), 0);
}
this.chatClientService.chatClient?.logger?.(
'info',
`Received one or more messages`,
Expand Down Expand Up @@ -582,6 +616,7 @@ export class MessageListComponent
}

private resetScrollState() {
this.isEmpty = true;
this.latestMessage = undefined;
this.hasNewMessages = true;
this.isUserScrolled = false;
Expand Down

0 comments on commit 92845a2

Please sign in to comment.