This commit is contained in:
BENEDEK László 2025-06-06 16:17:10 +02:00
parent 543dc2b9fb
commit 757e593373
14 changed files with 98 additions and 166 deletions

View File

@ -74,7 +74,8 @@
"buildTarget": "ui:build:production" "buildTarget": "ui:build:production"
}, },
"development": { "development": {
"buildTarget": "ui:build:development" "buildTarget": "ui:build:development",
"proxyConfig": "proxy.conf.json"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"

8
proxy.conf.json Normal file
View File

@ -0,0 +1,8 @@
{
"/api": {
"target": "http://localhost:5000",
"secure": false,
"ws": true,
"changeOrigin": true
}
}

View File

@ -13,8 +13,7 @@
button { button {
display: block; display: block;
margin-bottom: 10px 0;
} }
} }
} }

View File

@ -12,6 +12,7 @@ import { ToolbarComponent } from '../common/toolbar/toolbar.component';
import { MatBadgeModule } from '@angular/material/badge'; 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';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -28,7 +29,8 @@ import { MatTooltipModule } from '@angular/material/tooltip';
ToolbarComponent, ToolbarComponent,
MatBadgeModule, MatBadgeModule,
MatButtonModule, MatButtonModule,
MatTooltipModule MatTooltipModule,
MatIconModule
] ]
}) })
export class ChatModule { } export class ChatModule { }

View File

@ -4,4 +4,6 @@
border-radius: 10px 0; border-radius: 10px 0;
background-color: var(--mat-sys-background); background-color: var(--mat-sys-background);
overflow-y: scroll;
} }

View File

@ -1,7 +1,6 @@
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnChanges, OnDestroy } 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 { UserService } from '../../../services/user.service';
import { Channel } from '../../../models/channel'; import { Channel } from '../../../models/channel';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@ -21,7 +20,7 @@ export class FeedComponent implements OnChanges, OnDestroy {
constructor(private chatService: ChatService) { } constructor(private chatService: ChatService) { }
ngOnChanges() { ngOnChanges(): void {
if (this.subscription) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
this.messages = []; this.messages = [];

View File

@ -1,8 +1,8 @@
<div class="message-container"> <div class="message-container">
<app-profile-picture [username]="this.message.sender" /> <app-profile-picture [username]="this.message.sender_name" />
<div class="message-inner-container"> <div class="message-inner-container">
<div class="message-sender">{{this.message.sender}}</div> <div class="message-sender">{{this.message.sender_name}}</div>
<div class="message-content">{{this.message.content}}</div> <div class="message-content">{{this.message.content}}</div>
</div> </div>

View File

@ -1 +1,2 @@
<img [src]="this.url" [alt]="this.username"> <img *ngIf="this.url != ''" [src]="this.url" [alt]="this.username">
<mat-icon *ngIf="this.url == ''">face</mat-icon>

View File

@ -1,7 +1,23 @@
img { :host {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
aspect-ratio: 1;
border-radius: 50%; border-radius: 50%;
box-shadow: 0px 0px 5px var(--mat-sys-on-background); box-shadow: 0px 0px 2px var(--mat-sys-on-background);
}
// style alt for missing images
img {
display: flex;
justify-content: center;
padding-top: 15px;
overflow: hidden;
}
mat-icon {
width: 100% !important;
height: 100% !important;
text-align: center;
font-size: 50px;
} }

View File

@ -1,9 +1,7 @@
import { Timestamp } from "rxjs"
export class Message { export class Message {
public id!: number public id!: number
public sender!: string public sender_name!: string
public channel!: number public channel_id!: number
public time!: Date public time!: Date
public content!: string public content!: string
} }

View File

@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { catchError, map, Observable, of } from 'rxjs'; import { catchError, map, Observable, of, tap } from 'rxjs';
import { LoginResponse, RegisterResponse } from './responses/auth'; import { LoginResponse, RegisterResponse } from './responses/auth';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router';
import { environment } from '../../environment/environment'; import { environment } from '../../environment/environment';
@ -14,6 +14,8 @@ export class AuthService {
private readonly PASSWORD_FIELD: string = "password"; private readonly PASSWORD_FIELD: string = "password";
private readonly REPEAT_PASSWORD_FIELD: string = "repeatPassword"; private readonly REPEAT_PASSWORD_FIELD: string = "repeatPassword";
private loggedIn: boolean = false;
constructor(private http: HttpClient) { } constructor(private http: HttpClient) { }
public Login(username: string, password: string): Observable<LoginResponse> { public Login(username: string, password: string): Observable<LoginResponse> {
@ -51,29 +53,40 @@ export class AuthService {
return this.http.get(url, { withCredentials: true }).pipe( return this.http.get(url, { withCredentials: true }).pipe(
map(() => true), map(() => true),
catchError(() => of(false)) catchError(() => of(false)),
tap(result => this.loggedIn = result)
); );
} }
public IsLoggedIn(): boolean { public HasToken(): boolean {
let cookies = document.cookie.split(';'); let cookies = document.cookie.split(';');
let found = false; let found = false;
cookies.forEach(cookie => { cookies.forEach(cookie => {
cookie = cookie.trim(); cookie = cookie.trim();
found = found || cookie.startsWith(this.SESSION_COOKIE + "=") && cookie.split('=', 2)[1] != ''; found = found || cookie.startsWith(this.SESSION_COOKIE + "=") && cookie.split('=', 2)[1] != '';
}); });
return found; return found;
} }
public IsLoggedIn(): boolean { return this.loggedIn; }
} }
export const IsLoggedInCanActivate: CanActivateFn = ( export const IsLoggedInCanActivate: CanActivateFn = (
_: ActivatedRouteSnapshot, _: ActivatedRouteSnapshot,
__: RouterStateSnapshot __: RouterStateSnapshot
) => { ): Observable<boolean> => {
if (inject(AuthService).IsLoggedIn()) { const authService = inject(AuthService);
return true; const router = inject(Router);
} else {
inject(Router).navigateByUrl("auth/login"); return authService.Bump().pipe(
return false; map(isValid => {
} if (isValid) {
} return true;
} else {
router.navigate(['auth/login']);
return false;
}
})
);
};

View File

@ -1,10 +1,11 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { catchError, from, map, Observable, throwError } from 'rxjs'; import { catchError, from, map, merge, mergeMap, Observable, throwError } from 'rxjs';
import { webSocket } from 'rxjs/webSocket';
import { Channel } from '../models/channel'; 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 { ListAvailableChannelsResponse } from './responses/chat'; import { GetMessagesResponse, ListAvailableChannelsResponse } from './responses/chat';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -32,143 +33,30 @@ export class ChatService {
); );
} }
// TODO: implement
public GetMessages(channelID: number): Observable<Message> { 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);
}
return from([ if (response.messages) {
{ return response.messages;
id: 1, }
sender: 'admin',
channel: 1, throw new Error("bad API response, missing messages with no error");
time: new Date(), }),
content: 'this is my first message' catchError(error => throwError(() => new Error(error.error.message)))
}, );
{
id: 1, url = `${environment.apiBase}/chat/subscribe/${channelID}`;
sender: 'alice', let socket = webSocket<Message>(url);
channel: 1,
time: new Date(), return merge(
content: 'this is my first message' messages.pipe(mergeMap(msgArray => from(msgArray))),
}, socket.asObservable());
{
id: 1,
sender: 'bob',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'charlie',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 1,
sender: 'admin',
channel: 1,
time: new Date(),
content: 'this is my first message'
},
{
id: 2,
sender: 'admin',
channel: 2,
time: new Date(),
content: 'this is my second message'
}
]);
} }
} }

View File

@ -1,6 +1,11 @@
import { Channel } from "../../models/channel"; import { Channel } from "../../models/channel";
import { Message } from "../../models/message";
import { APIResponse } from "./basic"; import { APIResponse } from "./basic";
export class ListAvailableChannelsResponse extends APIResponse { export class ListAvailableChannelsResponse extends APIResponse {
public channels?: Channel[]; public channels?: Channel[];
}
export class GetMessagesResponse extends APIResponse {
public messages?: Message[];
} }

View File

@ -1,4 +1,4 @@
export const environment = { export const environment = {
production: false, production: false,
apiBase: "http://localhost:5000" apiBase: "http://localhost:5000/api"
} }