diff --git a/frontend/package.json b/frontend/package.json index 188c75f..5fa54f6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "@angular/router": "20.2.2", "@auth0/angular-jwt": "^5.2.0", "@types/three": "^0.177.0", + "event-source-polyfill": "^1.0.31", "ngx-cookie-service": "^20.0.1", "rxjs": "~7.8.0", "three": "^0.177.0", @@ -34,6 +35,7 @@ "@angular/build": "20.2.2", "@angular/cli": "20.2.2", "@angular/compiler-cli": "20.2.2", + "@types/event-source-polyfill": "^1.0.5", "@types/jasmine": "~5.1.0", "jasmine-core": "~5.6.0", "karma": "~6.4.0", diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json index 975a708..d681a87 100644 --- a/frontend/proxy.conf.json +++ b/frontend/proxy.conf.json @@ -1,6 +1,6 @@ { "/api": { - "target": "http://localhost:8080", + "target": "http://10.0.0.121:8080", "secure": false, "changeOrigin": true } diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 0b298b7..a75b7ac 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -198,6 +198,10 @@ export const routes: Routes = [ path: 'community', loadComponent: () => import('./pages/altitude/community/community.component').then(m => m.CommunityComponent) }, + { + path: 'chat', + loadComponent: () => import('./pages/altitude/chat/chat.component').then(m => m.ChatComponent) + }, { path: 'nicknames', loadComponent: () => import('./pages/reference/nicknames/nicknames.component').then(m => m.NicknamesComponent) diff --git a/frontend/src/app/pages/altitude/chat/chat.component.html b/frontend/src/app/pages/altitude/chat/chat.component.html new file mode 100644 index 0000000..cc26fb3 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/chat.component.html @@ -0,0 +1 @@ +

chat works!

diff --git a/frontend/src/app/pages/altitude/chat/chat.component.scss b/frontend/src/app/pages/altitude/chat/chat.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/pages/altitude/chat/chat.component.ts b/frontend/src/app/pages/altitude/chat/chat.component.ts new file mode 100644 index 0000000..8163e1b --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/chat.component.ts @@ -0,0 +1,32 @@ +import {Component, inject, OnDestroy, OnInit} from '@angular/core'; +import {Subscription} from 'rxjs'; +import {ChatService} from '@pages/altitude/chat/service/chat.service'; + +@Component({ + selector: 'app-chat', + imports: [], + templateUrl: './chat.component.html', + styleUrl: './chat.component.scss' +}) +export class ChatComponent implements OnInit, OnDestroy { + private sub?: Subscription; + private readonly liveEvents: ChatService = inject(ChatService) + + ngOnInit(): void { + this.liveEvents.connect(); + + this.sub = this.liveEvents.onEvent().subscribe(event => { + //TODO enums for event types + if (event.type === 'chat') { + console.log(event.data) + } else if (event.type === 'connect') { + console.log(event.data) + } + }); + } + + ngOnDestroy(): void { + this.sub?.unsubscribe(); + this.liveEvents.disconnect(); // triggers onCompletion server-side, cleans up the emitter + } +} diff --git a/frontend/src/app/pages/altitude/chat/service/chat.service.ts b/frontend/src/app/pages/altitude/chat/service/chat.service.ts new file mode 100644 index 0000000..0cb8f4c --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/service/chat.service.ts @@ -0,0 +1,74 @@ +import {inject, Injectable, OnDestroy} from '@angular/core'; +import {Subject} from 'rxjs'; +import {AuthService} from '@services/auth.service'; +import {EventSourcePolyfill} from 'event-source-polyfill'; + +export interface ChatEvent { + type: string; + data: any; +} + +interface SsePayloadEvent { + data: string; +} + +@Injectable({providedIn: 'root'}) +export class ChatService implements OnDestroy { + private eventSource?: EventSourcePolyfill; + private events$ = new Subject(); + private readonly authService: AuthService = inject(AuthService) + + connect() { + if (this.eventSource) { + return; // already connected + } + + const jwt = this.authService.getJwt(); + if (!jwt) { + //TODO [Stijn] [2026-07-18]: Error when no JWT available (log in?) + return; + } + + const source = new EventSourcePolyfill('/api/chat/read/subscribe', { + headers: { + Authorization: `Bearer ${jwt}` + }, + heartbeatTimeout: 60000 + }); + + this.eventSource = source; + + this.on(source, 'connected', (event) => { + console.log('SSE connected:', event.data); + }); + + this.on(source, 'chat-message', (event) => { + this.events$.next({type: 'chat-message', data: JSON.parse(event.data)}); + }); + + source.onerror = (err) => { + console.error('SSE error, polyfill will auto-reconnect:', err); + }; + } + + // single, deliberate escape hatch from the broken DOM/polyfill type overlap — + // everything else in this file stays fully typed + private on(source: EventSourcePolyfill, eventName: string, handler: (event: SsePayloadEvent) => void): void { + (source as unknown as { addEventListener: (type: string, listener: (event: SsePayloadEvent) => void) => void }) + .addEventListener(eventName, handler); + } + + + onEvent() { + return this.events$.asObservable(); + } + + disconnect(): void { + this.eventSource?.close(); + this.eventSource = undefined; + } + + ngOnDestroy(): void { + this.disconnect(); + } +} diff --git a/frontend/src/app/pages/header/header/header.component.html b/frontend/src/app/pages/header/header/header.component.html index 8aa2d16..1f67824 100644 --- a/frontend/src/app/pages/header/header/header.component.html +++ b/frontend/src/app/pages/header/header/header.component.html @@ -130,6 +130,11 @@ + @if (isAuthenticated()) { + @if (hasAccess([PermissionClaim.HEAD_MOD])) { + + } + }