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.
This commit is contained in:
parent
757e593373
commit
ebbaf6716d
@ -11,7 +11,7 @@ import { Router } from '@angular/router';
|
|||||||
styleUrls: ['../auth.module.scss', './login.component.scss']
|
styleUrls: ['../auth.module.scss', './login.component.scss']
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit {
|
export class LoginComponent implements OnInit {
|
||||||
loginForm!: FormGroup;
|
public loginForm!: FormGroup;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
@ -41,7 +41,7 @@ export class LoginComponent implements OnInit {
|
|||||||
this.router.navigateByUrl('chat');
|
this.router.navigateByUrl('chat');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: _ => {
|
error: () => {
|
||||||
this.toastrService.error("API error", "Error");
|
this.toastrService.error("API error", "Error");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -13,6 +13,10 @@ import { MatBadgeModule } from '@angular/material/badge';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -21,7 +25,8 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
FeedComponent,
|
FeedComponent,
|
||||||
ChannelEntryComponent,
|
ChannelEntryComponent,
|
||||||
MessageComponent,
|
MessageComponent,
|
||||||
ProfilePictureComponent
|
ProfilePictureComponent,
|
||||||
|
MessageBarComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -30,7 +35,10 @@ import { MatIconModule } from '@angular/material/icon';
|
|||||||
MatBadgeModule,
|
MatBadgeModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatTooltipModule,
|
MatTooltipModule,
|
||||||
MatIconModule
|
MatIconModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
ReactiveFormsModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ChatModule { }
|
export class ChatModule { }
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<app-toolbar />
|
|
||||||
|
|
||||||
<div id="container">
|
<div id="container">
|
||||||
|
<app-toolbar />
|
||||||
|
|
||||||
<app-channel-list (select)="Select($event)"></app-channel-list>
|
<app-channel-list (select)="Select($event)"></app-channel-list>
|
||||||
|
|
||||||
<app-feed [channel]="this.selectedChannel"></app-feed>
|
<app-feed [channel]="this.selectedChannel"></app-feed>
|
||||||
|
|
||||||
|
<app-message-bar [channel]="this.selectedChannel?.id ?? 1"></app-message-bar>
|
||||||
</div>
|
</div>
|
@ -1,5 +1,35 @@
|
|||||||
#container {
|
#container {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(auto, 150px) 1fr;
|
|
||||||
height: 100%;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,4 +1,3 @@
|
|||||||
<app-message
|
<div #scrollable id="scrollable">
|
||||||
*ngFor="let message of this.messages"
|
<app-message *ngFor="let message of this.messages" [message]="message" />
|
||||||
[message]="message"
|
</div>
|
||||||
/>
|
|
@ -1,9 +1,3 @@
|
|||||||
:host {
|
: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;
|
overflow-y: scroll;
|
||||||
}
|
}
|
@ -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 { Message } from '../../../models/message';
|
||||||
import { ChatService } from '../../../services/chat.service';
|
import { ChatService } from '../../../services/chat.service';
|
||||||
import { Channel } from '../../../models/channel';
|
import { Channel } from '../../../models/channel';
|
||||||
@ -10,7 +10,7 @@ import { Subscription } from 'rxjs';
|
|||||||
templateUrl: './feed.component.html',
|
templateUrl: './feed.component.html',
|
||||||
styleUrl: './feed.component.scss'
|
styleUrl: './feed.component.scss'
|
||||||
})
|
})
|
||||||
export class FeedComponent implements OnChanges, OnDestroy {
|
export class FeedComponent implements OnChanges, OnDestroy, AfterViewInit {
|
||||||
private readonly DEFAULT_CHANNEL_ID: number = 1;
|
private readonly DEFAULT_CHANNEL_ID: number = 1;
|
||||||
|
|
||||||
@Input('channel') public channel?: Channel;
|
@Input('channel') public channel?: Channel;
|
||||||
@ -18,7 +18,11 @@ export class FeedComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
public subscription?: Subscription;
|
public subscription?: Subscription;
|
||||||
|
|
||||||
constructor(private chatService: ChatService) { }
|
constructor(private chatService: ChatService, private element: ElementRef<HTMLElement>) { }
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
if (this.subscription) {
|
if (this.subscription) {
|
||||||
@ -28,7 +32,11 @@ export class FeedComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
this.subscription =
|
this.subscription =
|
||||||
this.chatService.GetMessages(this.channel?.id ?? this.DEFAULT_CHANNEL_ID)
|
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 {
|
ngOnDestroy(): void {
|
||||||
@ -36,4 +44,13 @@ export class FeedComponent implements OnChanges, OnDestroy {
|
|||||||
this.subscription.unsubscribe();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
12
src/app/chat/chat/message-bar/message-bar.component.html
Normal file
12
src/app/chat/chat/message-bar/message-bar.component.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<form [formGroup]="messageForm" (submit)="Send()">
|
||||||
|
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Message</mat-label>
|
||||||
|
<textarea type="textarea" required matInput formControlName="content"></textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button mat-button type="button" (click)="Send()">
|
||||||
|
<mat-icon>send</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
23
src/app/chat/chat/message-bar/message-bar.component.scss
Normal file
23
src/app/chat/chat/message-bar/message-bar.component.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
44
src/app/chat/chat/message-bar/message-bar.component.ts
Normal file
44
src/app/chat/chat/message-bar/message-bar.component.ts
Normal file
@ -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")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,13 +5,16 @@ import { Channel } from '../models/channel';
|
|||||||
import { Message } from '../models/message';
|
import { Message } from '../models/message';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { environment } from '../../environment/environment';
|
import { environment } from '../../environment/environment';
|
||||||
import { GetMessagesResponse, ListAvailableChannelsResponse } from './responses/chat';
|
import { GetMessagesResponse, ListAvailableChannelsResponse, SendMessageResponse } from './responses/chat';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ChatService {
|
export class ChatService {
|
||||||
|
|
||||||
|
private readonly CHANNEL: string = "channel_id";
|
||||||
|
private readonly CONTENT: string = "content";
|
||||||
|
|
||||||
constructor(private http: HttpClient) { }
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
public ListAvailableChannels(): Observable<Channel[]> {
|
public ListAvailableChannels(): Observable<Channel[]> {
|
||||||
@ -59,4 +62,24 @@ export class ChatService {
|
|||||||
messages.pipe(mergeMap(msgArray => from(msgArray))),
|
messages.pipe(mergeMap(msgArray => from(msgArray))),
|
||||||
socket.asObservable());
|
socket.asObservable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SendMessage(channel: number, content: string): Observable<SendMessageResponse> {
|
||||||
|
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<SendMessageResponse>(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)))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,4 +8,6 @@ export class ListAvailableChannelsResponse extends APIResponse {
|
|||||||
|
|
||||||
export class GetMessagesResponse extends APIResponse {
|
export class GetMessagesResponse extends APIResponse {
|
||||||
public messages?: Message[];
|
public messages?: Message[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SendMessageResponse extends APIResponse { }
|
@ -1,4 +1,4 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
apiBase: "http://localhost:5000/api"
|
apiBase: "/api"
|
||||||
}
|
}
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100dvh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
font-family: var(--mat-sys-body-medium-font);
|
font-family: var(--mat-sys-body-medium-font);
|
||||||
|
Loading…
Reference in New Issue
Block a user