enhance chat functionality with infinite scrolling and message retrieval improvements
This commit is contained in:
parent
4f94192857
commit
066e5463b2
@ -1,5 +1,5 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideRouter, withInMemoryScrolling } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
@ -10,7 +10,10 @@ import { provideMarkdown } from 'ngx-markdown';
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(routes),
|
||||
provideRouter(routes, withInMemoryScrolling({
|
||||
anchorScrolling: "enabled",
|
||||
scrollPositionRestoration: 'top'
|
||||
})),
|
||||
provideHttpClient(),
|
||||
provideAnimations(),
|
||||
provideToastr({
|
||||
|
@ -18,7 +18,7 @@
|
||||
<mat-drawer-content>
|
||||
<app-toolbar (sidebar)="drawer.toggle()" />
|
||||
|
||||
<app-feed #feed
|
||||
<app-feed #feed (outOfMessages)="this.GetMoreMessages()"
|
||||
[messages]="(this.channels && this.channels.length) != 0 ? this.channels[this.selectedChannel].messages : []"></app-feed>
|
||||
|
||||
<app-message-bar
|
||||
|
@ -41,7 +41,7 @@ export class ChatComponent implements OnInit {
|
||||
|
||||
private getMessages(): void {
|
||||
this.channels.forEach((channelObj, i) => {
|
||||
this.chatService.GetMessages(channelObj.channel.id).subscribe({
|
||||
this.chatService.GetMessageStream(channelObj.channel.id).subscribe({
|
||||
next: message => {
|
||||
channelObj.messages.push(message);
|
||||
this.feed.ScrollEventHandler();
|
||||
@ -56,6 +56,18 @@ export class ChatComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public GetMoreMessages(): void {
|
||||
this.chatService.GetMessages(this.channels[this.selectedChannel].channel.id, this.channels[this.selectedChannel].messages[0]?.time ?? new Date())
|
||||
.subscribe({
|
||||
next: messages => {
|
||||
let lastMessage = this.channels[this.selectedChannel].messages[0];
|
||||
this.channels[this.selectedChannel].messages.unshift(...messages);
|
||||
this.feed.ScrollToMessage(lastMessage.channel_id, lastMessage.id);
|
||||
},
|
||||
error: () => this.toastrService.error("Failed to fetch older messages.", "Error")
|
||||
});
|
||||
}
|
||||
|
||||
public GetChannelList(): Channel[] {
|
||||
return this.channels.map(c => c.channel);
|
||||
}
|
||||
|
@ -1,3 +1 @@
|
||||
<div #scrollable id="scrollable">
|
||||
<app-message *ngFor="let message of this.messages" [message]="message" />
|
||||
</div>
|
||||
<app-message *ngFor="let message of this.messages" [message]="message" />
|
@ -1,4 +1,4 @@
|
||||
import { AfterViewInit, Component, DoCheck, ElementRef, EventEmitter, Input, IterableDiffer, IterableDiffers, Output } from '@angular/core';
|
||||
import { AfterViewInit, Component, DoCheck, ElementRef, EventEmitter, HostListener, Input, IterableDiffer, IterableDiffers, Output, QueryList, ViewChildren } from '@angular/core';
|
||||
import { Message } from '../../../models/message';
|
||||
|
||||
@Component({
|
||||
@ -9,6 +9,7 @@ import { Message } from '../../../models/message';
|
||||
})
|
||||
export class FeedComponent implements AfterViewInit {
|
||||
@Input('messages') public messages?: Message[];
|
||||
@Output('outOfMessages') public outOfMessages: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
constructor(private element: ElementRef<HTMLElement>) { }
|
||||
|
||||
@ -21,11 +22,38 @@ export class FeedComponent implements AfterViewInit {
|
||||
return Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) <= 100;
|
||||
}
|
||||
|
||||
private isAtTop(): boolean {
|
||||
let element = this.element.nativeElement;
|
||||
return element.scrollTop == 0;
|
||||
}
|
||||
|
||||
private scrollToBottom(): void {
|
||||
setTimeout(() => this.element.nativeElement.scrollTop = this.element.nativeElement.scrollHeight, 100);
|
||||
}
|
||||
|
||||
// handles scroll to bottom events from the chat component when a new message arrives
|
||||
public ScrollEventHandler(): void {
|
||||
if (this.isAtBottom()) this.scrollToBottom();
|
||||
}
|
||||
|
||||
@HostListener('scroll')
|
||||
private detectOutOfMessages(): void {
|
||||
if (this.isAtTop()) {
|
||||
this.outOfMessages.emit();
|
||||
}
|
||||
}
|
||||
|
||||
public ScrollToMessage(channelID: number, messageID: number): void {
|
||||
setTimeout(() => {
|
||||
let id = `#c${channelID}m${messageID}`;
|
||||
|
||||
let target = this.element.nativeElement.querySelector(id) as HTMLDivElement | null;
|
||||
if (!target) return;
|
||||
|
||||
this.element.nativeElement.scrollTo({
|
||||
top: target.offsetTop - target.offsetHeight,
|
||||
behavior: "instant"
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="message-container">
|
||||
<div [id]="`c${message.channel_id}m${message.id}`" class="message-container">
|
||||
<app-profile-picture [username]="this.message.sender_name" />
|
||||
|
||||
<div class="message-inner-container">
|
||||
|
@ -37,36 +37,52 @@ export class ChatService {
|
||||
);
|
||||
}
|
||||
|
||||
public GetMessages(channelID: number): Observable<Message> {
|
||||
let url = `${environment.apiBase}/chat/messages/${channelID}`;
|
||||
let messages: Observable<Message[]> =
|
||||
this.http.get<GetMessagesResponse>(url, { withCredentials: true })
|
||||
.pipe(
|
||||
map(response => {
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
public GetMessages(channelID: number, from: Date, limit: number = 10): Observable<Message[]> {
|
||||
let url = `${environment.apiBase}/chat/messages/${channelID}?limit=${limit}&from=${from.toISOString()}`;
|
||||
return this.http.get<GetMessagesResponse>(url, { withCredentials: true })
|
||||
.pipe(
|
||||
map(response => {
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
if (response.messages) {
|
||||
return response.messages;
|
||||
}
|
||||
if (response.messages) {
|
||||
return response.messages;
|
||||
}
|
||||
|
||||
throw new Error("bad API response, missing messages with no error");
|
||||
}),
|
||||
catchError(error => throwError(() => new Error(error.error.message)))
|
||||
);
|
||||
throw new Error("bad API response, missing messages with no error");
|
||||
}),
|
||||
// HttpClient doesn't parse Date objects by default
|
||||
map(msgs => {
|
||||
return msgs.map(msg => {
|
||||
if (typeof msg.time == 'string')
|
||||
msg.time = new Date(msg.time);
|
||||
|
||||
url = `${environment.apiBase}/chat/subscribe/${channelID}`;
|
||||
return msg;
|
||||
})
|
||||
}),
|
||||
catchError(error => throwError(() => new Error(error.error.message)))
|
||||
);
|
||||
}
|
||||
|
||||
public GetMessageStream(channelID: number, historyLimit: number = 10): Observable<Message> {
|
||||
let messages: Observable<Message[]> = this.GetMessages(channelID, new Date(), historyLimit);
|
||||
|
||||
let url = `${environment.apiBase}/chat/subscribe/${channelID}`;
|
||||
let socket = webSocket<Message>(url);
|
||||
|
||||
return merge(
|
||||
messages.pipe(mergeMap(msgArray => from(msgArray))),
|
||||
socket.asObservable())
|
||||
.pipe(map(msg => {
|
||||
// HttpClient doesn't parse Date objects by default
|
||||
msg.time = new Date(msg.time);
|
||||
return msg;
|
||||
}));
|
||||
messages
|
||||
.pipe(mergeMap(msgArray => from(msgArray))),
|
||||
socket.asObservable()
|
||||
.pipe(map(msg => {
|
||||
// HttpClient doesn't parse Date objects by default
|
||||
if (typeof msg.time == 'string')
|
||||
msg.time = new Date(msg.time);
|
||||
|
||||
return msg;
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
public SendMessage(channel: number, content: string): Observable<SendMessageResponse> {
|
||||
|
Loading…
Reference in New Issue
Block a user