From ebbaf6716d92a80ecc3818ffd948f2358f9199aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?BENEDEK=20L=C3=A1szl=C3=B3?= Date: Sat, 7 Jun 2025 02:42:59 +0200 Subject: [PATCH] feat: implement message bar and enhance chat functionality - Added MessageBarComponent for sending messages. - Integrated message sending logic in ChatService. - Updated FeedComponent to scroll to the bottom on new messages. - Improved layout and styling for chat components. - Adjusted environment configuration for API base URL. --- src/app/auth/login/login.component.ts | 4 +- src/app/chat/chat.module.ts | 12 ++++- src/app/chat/chat/chat.component.html | 7 ++- src/app/chat/chat/chat.component.scss | 34 +++++++++++++- src/app/chat/chat/feed/feed.component.html | 7 ++- src/app/chat/chat/feed/feed.component.scss | 6 --- src/app/chat/chat/feed/feed.component.ts | 25 +++++++++-- .../message-bar/message-bar.component.html | 12 +++++ .../message-bar/message-bar.component.scss | 23 ++++++++++ .../chat/message-bar/message-bar.component.ts | 44 +++++++++++++++++++ src/app/services/chat.service.ts | 25 ++++++++++- src/app/services/responses/chat.ts | 4 +- src/environment/environment.ts | 2 +- src/styles.scss | 2 +- 14 files changed, 181 insertions(+), 26 deletions(-) create mode 100644 src/app/chat/chat/message-bar/message-bar.component.html create mode 100644 src/app/chat/chat/message-bar/message-bar.component.scss create mode 100644 src/app/chat/chat/message-bar/message-bar.component.ts diff --git a/src/app/auth/login/login.component.ts b/src/app/auth/login/login.component.ts index 631fb46..ac90846 100644 --- a/src/app/auth/login/login.component.ts +++ b/src/app/auth/login/login.component.ts @@ -11,7 +11,7 @@ import { Router } from '@angular/router'; styleUrls: ['../auth.module.scss', './login.component.scss'] }) export class LoginComponent implements OnInit { - loginForm!: FormGroup; + public loginForm!: FormGroup; constructor( private authService: AuthService, @@ -41,7 +41,7 @@ export class LoginComponent implements OnInit { this.router.navigateByUrl('chat'); } }, - error: _ => { + error: () => { this.toastrService.error("API error", "Error"); } }) diff --git a/src/app/chat/chat.module.ts b/src/app/chat/chat.module.ts index d85d4a2..fe4fc1e 100644 --- a/src/app/chat/chat.module.ts +++ b/src/app/chat/chat.module.ts @@ -13,6 +13,10 @@ import { MatBadgeModule } from '@angular/material/badge'; import { MatButtonModule } from '@angular/material/button'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatIconModule } from '@angular/material/icon'; +import { MessageBarComponent } from './chat/message-bar/message-bar.component'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ declarations: [ @@ -21,7 +25,8 @@ import { MatIconModule } from '@angular/material/icon'; FeedComponent, ChannelEntryComponent, MessageComponent, - ProfilePictureComponent + ProfilePictureComponent, + MessageBarComponent ], imports: [ CommonModule, @@ -30,7 +35,10 @@ import { MatIconModule } from '@angular/material/icon'; MatBadgeModule, MatButtonModule, MatTooltipModule, - MatIconModule + MatIconModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule ] }) export class ChatModule { } diff --git a/src/app/chat/chat/chat.component.html b/src/app/chat/chat/chat.component.html index faa7a10..13e9e6f 100644 --- a/src/app/chat/chat/chat.component.html +++ b/src/app/chat/chat/chat.component.html @@ -1,6 +1,9 @@ - -
+ + + + +
\ No newline at end of file diff --git a/src/app/chat/chat/chat.component.scss b/src/app/chat/chat/chat.component.scss index d9c4120..4ea2f6f 100644 --- a/src/app/chat/chat/chat.component.scss +++ b/src/app/chat/chat/chat.component.scss @@ -1,5 +1,35 @@ #container { - display: grid; - grid-template-columns: minmax(auto, 150px) 1fr; height: 100%; + + display: grid; + grid-template-columns: 150px 1fr; + grid-template-rows: 65px 1fr auto; + gap: 0px; + + app-toolbar { + grid-column: span 2 / span 2; + + } + + app-channel-list { + grid-row: span 2 / span 2; + grid-row-start: 2; + } + + app-feed { + grid-row-start: 2; + + border-left: 2px solid var(--mat-sys-surface-variant); + border-top: 2px solid var(--mat-sys-surface-variant); + border-radius: 10px 0 0 0; + + background-color: var(--mat-sys-background); + } + + app-message-bar { + grid-column-start: 2; + grid-row-start: 3; + + border-left: 2px solid var(--mat-sys-surface-variant); + } } \ No newline at end of file diff --git a/src/app/chat/chat/feed/feed.component.html b/src/app/chat/chat/feed/feed.component.html index 575a4d5..8ee4812 100644 --- a/src/app/chat/chat/feed/feed.component.html +++ b/src/app/chat/chat/feed/feed.component.html @@ -1,4 +1,3 @@ - \ No newline at end of file +
+ +
\ No newline at end of file diff --git a/src/app/chat/chat/feed/feed.component.scss b/src/app/chat/chat/feed/feed.component.scss index ef17fef..3c1fe79 100644 --- a/src/app/chat/chat/feed/feed.component.scss +++ b/src/app/chat/chat/feed/feed.component.scss @@ -1,9 +1,3 @@ :host { - border-left: 2px solid var(--mat-sys-surface-variant); - border-top: 2px solid var(--mat-sys-surface-variant); - border-radius: 10px 0; - - background-color: var(--mat-sys-background); - overflow-y: scroll; } \ No newline at end of file diff --git a/src/app/chat/chat/feed/feed.component.ts b/src/app/chat/chat/feed/feed.component.ts index 8731944..ce468b8 100644 --- a/src/app/chat/chat/feed/feed.component.ts +++ b/src/app/chat/chat/feed/feed.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnDestroy } from '@angular/core'; +import { AfterContentInit, AfterViewChecked, AfterViewInit, Component, ElementRef, Input, OnChanges, OnDestroy, ViewChild } from '@angular/core'; import { Message } from '../../../models/message'; import { ChatService } from '../../../services/chat.service'; import { Channel } from '../../../models/channel'; @@ -10,7 +10,7 @@ import { Subscription } from 'rxjs'; templateUrl: './feed.component.html', styleUrl: './feed.component.scss' }) -export class FeedComponent implements OnChanges, OnDestroy { +export class FeedComponent implements OnChanges, OnDestroy, AfterViewInit { private readonly DEFAULT_CHANNEL_ID: number = 1; @Input('channel') public channel?: Channel; @@ -18,7 +18,11 @@ export class FeedComponent implements OnChanges, OnDestroy { public subscription?: Subscription; - constructor(private chatService: ChatService) { } + constructor(private chatService: ChatService, private element: ElementRef) { } + + ngAfterViewInit(): void { + this.scrollToBottom(); + } ngOnChanges(): void { if (this.subscription) { @@ -28,7 +32,11 @@ export class FeedComponent implements OnChanges, OnDestroy { this.subscription = this.chatService.GetMessages(this.channel?.id ?? this.DEFAULT_CHANNEL_ID) - .subscribe(message => this.messages.push(message)); + .subscribe(message => { + this.messages.push(message); + + if (this.isAtBottom()) this.scrollToBottom(); + }); } ngOnDestroy(): void { @@ -36,4 +44,13 @@ export class FeedComponent implements OnChanges, OnDestroy { this.subscription.unsubscribe(); } } + + private isAtBottom(): boolean { + let element = this.element.nativeElement; + return Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) <= 100; + } + + private scrollToBottom(): void { + setTimeout(() => this.element.nativeElement.scrollTop = this.element.nativeElement.scrollHeight, 100); + } } diff --git a/src/app/chat/chat/message-bar/message-bar.component.html b/src/app/chat/chat/message-bar/message-bar.component.html new file mode 100644 index 0000000..bb89746 --- /dev/null +++ b/src/app/chat/chat/message-bar/message-bar.component.html @@ -0,0 +1,12 @@ +
+ + + Message + + + + + +
\ No newline at end of file diff --git a/src/app/chat/chat/message-bar/message-bar.component.scss b/src/app/chat/chat/message-bar/message-bar.component.scss new file mode 100644 index 0000000..282de69 --- /dev/null +++ b/src/app/chat/chat/message-bar/message-bar.component.scss @@ -0,0 +1,23 @@ +form { + align-items: center; + + padding: 10px; + + + display: grid; + grid-template-columns: 1fr auto; + + mat-form-field { + padding-top: 5px; + } +} + +button { + margin: 0 5px; + transform: translateY(-10px); + + mat-icon { + margin: auto; + vertical-align: middle; + } +} \ No newline at end of file diff --git a/src/app/chat/chat/message-bar/message-bar.component.ts b/src/app/chat/chat/message-bar/message-bar.component.ts new file mode 100644 index 0000000..2a470cc --- /dev/null +++ b/src/app/chat/chat/message-bar/message-bar.component.ts @@ -0,0 +1,44 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { ChatService } from '../../../services/chat.service'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { ToastrService } from 'ngx-toastr'; + +@Component({ + selector: 'app-message-bar', + standalone: false, + templateUrl: './message-bar.component.html', + styleUrl: './message-bar.component.scss' +}) +export class MessageBarComponent implements OnInit { + private readonly MAX_MESSAGE_LENGTH: number = 120; + + @Input('channel') channel!: number; + + public messageForm!: FormGroup; + + constructor(private chatService: ChatService, + private formBuilder: FormBuilder, + private toastrService: ToastrService) { } + + ngOnInit(): void { + this.messageForm = this.formBuilder.group({ + content: new FormControl('', [ + Validators.required, + Validators.minLength(1), + Validators.maxLength(this.MAX_MESSAGE_LENGTH) + ]) + }); + } + + public Send(): void { + if (this.messageForm.valid) { + this.chatService.SendMessage( + this.channel, + this.messageForm.get("content")!.value).subscribe( + { + next: () => this.messageForm.setValue({ "content": "" }), + error: () => this.toastrService.error("Failed to send message.", "Error") + }); + } + } +} diff --git a/src/app/services/chat.service.ts b/src/app/services/chat.service.ts index de15f61..08a11f4 100644 --- a/src/app/services/chat.service.ts +++ b/src/app/services/chat.service.ts @@ -5,13 +5,16 @@ import { Channel } from '../models/channel'; import { Message } from '../models/message'; import { HttpClient } from '@angular/common/http'; import { environment } from '../../environment/environment'; -import { GetMessagesResponse, ListAvailableChannelsResponse } from './responses/chat'; +import { GetMessagesResponse, ListAvailableChannelsResponse, SendMessageResponse } from './responses/chat'; @Injectable({ providedIn: 'root' }) export class ChatService { + private readonly CHANNEL: string = "channel_id"; + private readonly CONTENT: string = "content"; + constructor(private http: HttpClient) { } public ListAvailableChannels(): Observable { @@ -59,4 +62,24 @@ export class ChatService { messages.pipe(mergeMap(msgArray => from(msgArray))), socket.asObservable()); } + + public SendMessage(channel: number, content: string): Observable { + let url = `${environment.apiBase}/chat/send`; + + let data = new FormData(); + data.append(this.CHANNEL, channel.toString()); + data.append(this.CONTENT, content); + + return this.http.post(url, data, { withCredentials: true }) + .pipe( + map(response => { + if (response.error) { + throw new Error(response.error); + } + + return response; + }), + catchError(error => throwError(() => new Error(error.error.message))) + ); + } } diff --git a/src/app/services/responses/chat.ts b/src/app/services/responses/chat.ts index 24923f6..2f48cb9 100644 --- a/src/app/services/responses/chat.ts +++ b/src/app/services/responses/chat.ts @@ -8,4 +8,6 @@ export class ListAvailableChannelsResponse extends APIResponse { export class GetMessagesResponse extends APIResponse { public messages?: Message[]; -} \ No newline at end of file +} + +export class SendMessageResponse extends APIResponse { } \ No newline at end of file diff --git a/src/environment/environment.ts b/src/environment/environment.ts index 3d9c8e8..0ba156e 100644 --- a/src/environment/environment.ts +++ b/src/environment/environment.ts @@ -1,4 +1,4 @@ export const environment = { production: false, - apiBase: "http://localhost:5000/api" + apiBase: "/api" } \ No newline at end of file diff --git a/src/styles.scss b/src/styles.scss index ffe6595..2d6b8c0 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -2,7 +2,7 @@ html, body { - height: 100%; + height: 100dvh; margin: 0; font-family: var(--mat-sys-body-medium-font);