Modularize chat page with new components (chat-box, server-list, chat-info). Introduce server filtering, real-time updates, and redesigned layout for enhanced usability.
This commit is contained in:
parent
73132fae13
commit
d61f2ed938
|
|
@ -1,5 +1,5 @@
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<app-header [current_page]="'chat'" height="460px" background_image="/public/img/backgrounds/staff.png"
|
<app-header [current_page]="'chat'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||||
[overlay_gradient]="0.5">
|
[overlay_gradient]="0.5">
|
||||||
<div class="title" header-content>
|
<div class="title" header-content>
|
||||||
<h1>Chat</h1>
|
<h1>Chat</h1>
|
||||||
|
|
@ -7,18 +7,25 @@
|
||||||
</app-header>
|
</app-header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<section class="darkmodeSection">
|
<app-full-size [hideFooter]="true">
|
||||||
@for (message of messages(); track message.timestamp) {
|
<section class="darkmodeSection full-height">
|
||||||
<p>
|
<div class="page-layout">
|
||||||
<span class="date-time">[{{ message.timestamp | date: 'mediumTime' }}] </span>
|
<div class="server-list">
|
||||||
@if (message.notBlocked) {
|
<app-server-list [servers]="servers()" [selectedServer]="selectedServer()"
|
||||||
<span [style.color]=getServerColor(message.server)>[{{ message.server }}] </span>
|
(selectedServerChange)="onSelectedServerChange($event)">
|
||||||
} @else {
|
|
||||||
<span class="blocked">[{{ message.server }}] </span>
|
</app-server-list>
|
||||||
}
|
</div>
|
||||||
<mini-message [node]="message.messageJson"></mini-message>
|
<div class="chat">
|
||||||
</p>
|
<div class="chat-info">
|
||||||
}
|
<app-chat-info [selectedServer]="selectedServer()"></app-chat-info>
|
||||||
|
</div>
|
||||||
|
<div class="chat-box">
|
||||||
|
<app-chat-box [messages]="messages()"></app-chat-box>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</app-full-size>
|
||||||
</main>
|
</main>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,32 @@
|
||||||
.blocked {
|
.page-layout {
|
||||||
color: red;
|
display: flex;
|
||||||
}
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
.date-time {
|
.server-list {
|
||||||
color: gray;
|
flex: 0 0 auto;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
.chat-info {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 5px 5px 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-box {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 5px 0 5px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,20 @@ import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
||||||
import {Subscription} from 'rxjs';
|
import {Subscription} from 'rxjs';
|
||||||
import {ChatService} from '@pages/altitude/chat/service/chat.service';
|
import {ChatService} from '@pages/altitude/chat/service/chat.service';
|
||||||
import {HeaderComponent} from '@header/header.component';
|
import {HeaderComponent} from '@header/header.component';
|
||||||
import {MiniMessageComponent} from '@pages/altitude/chat/mini-message/mini-message.component';
|
|
||||||
import {MiniMessageInteractionService} from '@pages/altitude/chat/mini-message/mini-message-interaction.service';
|
import {MiniMessageInteractionService} from '@pages/altitude/chat/mini-message/mini-message-interaction.service';
|
||||||
import {DatePipe} from '@angular/common';
|
import {FullSizeComponent} from '@shared-components/full-size/full-size.component';
|
||||||
|
import {ChatBoxComponent} from '@pages/altitude/chat/components/chat/chat-box.component';
|
||||||
|
import {ServerListComponent} from '@pages/altitude/chat/components/server-list/server-list.component';
|
||||||
|
import {ChatInfoComponent} from '@pages/altitude/chat/components/chat-info/chat-info.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-chat',
|
selector: 'app-chat',
|
||||||
imports: [
|
imports: [
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
MiniMessageComponent,
|
FullSizeComponent,
|
||||||
DatePipe
|
ChatBoxComponent,
|
||||||
|
ServerListComponent,
|
||||||
|
ChatInfoComponent
|
||||||
],
|
],
|
||||||
templateUrl: './chat.component.html',
|
templateUrl: './chat.component.html',
|
||||||
styleUrl: './chat.component.scss',
|
styleUrl: './chat.component.scss',
|
||||||
|
|
@ -21,6 +25,8 @@ export class ChatComponent implements OnInit, OnDestroy {
|
||||||
private sub?: Subscription;
|
private sub?: Subscription;
|
||||||
private readonly chatService: ChatService = inject(ChatService)
|
private readonly chatService: ChatService = inject(ChatService)
|
||||||
protected readonly messages = this.chatService.messages;
|
protected readonly messages = this.chatService.messages;
|
||||||
|
protected readonly servers = this.chatService.servers;
|
||||||
|
protected readonly selectedServer = this.chatService.selectedServer;
|
||||||
|
|
||||||
constructor(private interaction: MiniMessageInteractionService) {
|
constructor(private interaction: MiniMessageInteractionService) {
|
||||||
interaction.clicks$.subscribe(({action, value}) => {
|
interaction.clicks$.subscribe(({action, value}) => {
|
||||||
|
|
@ -30,6 +36,10 @@ export class ChatComponent implements OnInit, OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onSelectedServerChange(server: string) {
|
||||||
|
this.chatService.selectServer(server);
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.chatService.connect();
|
this.chatService.connect();
|
||||||
|
|
||||||
|
|
@ -39,15 +49,4 @@ export class ChatComponent implements OnInit, OnDestroy {
|
||||||
this.sub?.unsubscribe();
|
this.sub?.unsubscribe();
|
||||||
this.chatService.disconnect(); // triggers onCompletion server-side, cleans up the emitter
|
this.chatService.disconnect(); // triggers onCompletion server-side, cleans up the emitter
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getServerColor(server: string) {
|
|
||||||
if (server === 'bayou') {
|
|
||||||
return "#007FFF";
|
|
||||||
} else if (server === 'lobby') {
|
|
||||||
return "white";
|
|
||||||
} else if (server === 'creative') {
|
|
||||||
return "#FFA500";
|
|
||||||
}
|
|
||||||
return "white";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div class="full-width"
|
||||||
|
[style.background-color]="getServerColor(selectedServer())">
|
||||||
|
<span class="server" [style.color]="getServerTextColor(selectedServer())">
|
||||||
|
{{ selectedServer() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
.server {
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: 25px;
|
||||||
|
font-family: minecraft-text, 'opensans-bold', sans-serif;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import {Component, input} from '@angular/core';
|
||||||
|
import {getServerColor, getServerTextColor} from '@pages/altitude/chat/util/server-color';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chat-info',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './chat-info.component.html',
|
||||||
|
styleUrl: './chat-info.component.scss'
|
||||||
|
})
|
||||||
|
export class ChatInfoComponent {
|
||||||
|
public readonly selectedServer = input.required<string>();
|
||||||
|
protected readonly getServerColor = getServerColor;
|
||||||
|
protected readonly getServerTextColor = getServerTextColor;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
@for (message of messages(); track message.timestamp) {
|
||||||
|
<p>
|
||||||
|
<span class="date-time" [class.blocked]="!message.notBlocked">
|
||||||
|
[{{ message.timestamp * 1000 | date: 'mediumTime' }}]
|
||||||
|
</span>
|
||||||
|
<mini-message [node]="message.messageJson"></mini-message>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
.blocked {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-time {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import {Component, input} from '@angular/core';
|
||||||
|
import {DatePipe} from '@angular/common';
|
||||||
|
import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object';
|
||||||
|
import {MiniMessageComponent} from '@pages/altitude/chat/components/mini-message/mini-message.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chat-box',
|
||||||
|
imports: [
|
||||||
|
MiniMessageComponent,
|
||||||
|
DatePipe
|
||||||
|
],
|
||||||
|
templateUrl: './chat-box.component.html',
|
||||||
|
styleUrl: './chat-box.component.scss'
|
||||||
|
})
|
||||||
|
export class ChatBoxComponent {
|
||||||
|
readonly messages = input.required<ChatMessage[]>();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
@if (node) {
|
||||||
|
<ng-container>
|
||||||
|
<a
|
||||||
|
*ngIf="isUrlLink; else plain"
|
||||||
|
[ngStyle]="ngStyle"
|
||||||
|
[mmObfuscated]="style.obfuscated"
|
||||||
|
[attr.title]="hoverTitle"
|
||||||
|
[attr.href]="node.clickEvent?.value"
|
||||||
|
[attr.data-insertion]="node.insertion || null"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="mm-node mm-link"
|
||||||
|
>{{ text }}</a>
|
||||||
|
|
||||||
|
<ng-template #plain>
|
||||||
|
<span
|
||||||
|
[ngStyle]="ngStyle"
|
||||||
|
[mmObfuscated]="style.obfuscated"
|
||||||
|
[attr.title]="hoverTitle"
|
||||||
|
[attr.data-insertion]="node.insertion || null"
|
||||||
|
[class.mm-clickable]="hasNonUrlClick"
|
||||||
|
class="mm-node"
|
||||||
|
(click)="onClick()"
|
||||||
|
>{{ text }}</span
|
||||||
|
>
|
||||||
|
</ng-template>
|
||||||
|
@for (child of node.extra; track child) {
|
||||||
|
<mini-message [node]="child" [parentStyle]="style"></mini-message>
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
.mm-node {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mm-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mm-link {
|
||||||
|
text-decoration: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {Component, Input, Optional} from '@angular/core';
|
import {Component, Input, Optional} from '@angular/core';
|
||||||
import {MiniMessageComponent as MiniMessageNode, ResolvedStyle} from './mini-message.types';
|
import {MiniMessageComponent as MiniMessageNode, ResolvedStyle} from '../../mini-message/mini-message.types';
|
||||||
import {hoverEventToTitle, mergeStyle, ownText, styleToNgStyle} from './mini-message.util';
|
import {hoverEventToTitle, mergeStyle, ownText, styleToNgStyle} from '../../mini-message/mini-message.util';
|
||||||
import {ObfuscatedDirective} from './obfuscated.directive';
|
import {ObfuscatedDirective} from '../../mini-message/obfuscated.directive';
|
||||||
import {MiniMessageInteractionService} from './mini-message-interaction.service';
|
import {MiniMessageInteractionService} from '../../mini-message/mini-message-interaction.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a Paper/Adventure component-JSON tree as styled HTML.
|
* Renders a Paper/Adventure component-JSON tree as styled HTML.
|
||||||
|
|
@ -20,51 +20,8 @@ import {MiniMessageInteractionService} from './mini-message-interaction.service'
|
||||||
selector: 'mini-message',
|
selector: 'mini-message',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, ObfuscatedDirective],
|
imports: [CommonModule, ObfuscatedDirective],
|
||||||
template: `
|
templateUrl: './mini-message.component.html',
|
||||||
<ng-container *ngIf="node">
|
styleUrl: './mini-message.component.scss',
|
||||||
<a
|
|
||||||
*ngIf="isUrlLink; else plain"
|
|
||||||
[ngStyle]="ngStyle"
|
|
||||||
[mmObfuscated]="style.obfuscated"
|
|
||||||
[attr.title]="hoverTitle"
|
|
||||||
[attr.href]="node.clickEvent?.value"
|
|
||||||
[attr.data-insertion]="node.insertion || null"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="mm-node mm-link"
|
|
||||||
>{{ text }}</a
|
|
||||||
>
|
|
||||||
<ng-template #plain>
|
|
||||||
<span
|
|
||||||
[ngStyle]="ngStyle"
|
|
||||||
[mmObfuscated]="style.obfuscated"
|
|
||||||
[attr.title]="hoverTitle"
|
|
||||||
[attr.data-insertion]="node.insertion || null"
|
|
||||||
[class.mm-clickable]="hasNonUrlClick"
|
|
||||||
class="mm-node"
|
|
||||||
(click)="onClick()"
|
|
||||||
>{{ text }}</span
|
|
||||||
>
|
|
||||||
</ng-template>
|
|
||||||
<mini-message *ngFor="let child of node.extra" [node]="child" [parentStyle]="style"></mini-message>
|
|
||||||
</ng-container>
|
|
||||||
`,
|
|
||||||
styles: [
|
|
||||||
`
|
|
||||||
.mm-node {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mm-clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mm-link {
|
|
||||||
text-decoration: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class MiniMessageComponent {
|
export class MiniMessageComponent {
|
||||||
@Input() node!: MiniMessageNode;
|
@Input() node!: MiniMessageNode;
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<div class="server-list">
|
||||||
|
<p>Servers</p>
|
||||||
|
@for (server of servers(); track server.server) {
|
||||||
|
<div class="server" (click)="selectedServerChange.emit(server.server)">
|
||||||
|
<span class="server-name">{{ server.server }}</span>
|
||||||
|
@if (server.unreadMessages > 0) {
|
||||||
|
<div class="notification">
|
||||||
|
<svg-circle></svg-circle>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
.server-list {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #172133;
|
||||||
|
|
||||||
|
.server {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 2px 2px 2px 10px;
|
||||||
|
border-bottom: 1px solid #222;
|
||||||
|
|
||||||
|
.server-name {
|
||||||
|
color: white
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import {Component, effect, input, output} from '@angular/core';
|
||||||
|
import {ChatServer} from '@pages/altitude/chat/objects/chat-server.object';
|
||||||
|
import {CircleSvgComponent} from '@pages/altitude/chat/svg/circle.svg';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-server-list',
|
||||||
|
imports: [
|
||||||
|
CircleSvgComponent
|
||||||
|
],
|
||||||
|
templateUrl: './server-list.component.html',
|
||||||
|
styleUrl: './server-list.component.scss'
|
||||||
|
})
|
||||||
|
export class ServerListComponent {
|
||||||
|
public readonly servers = input.required<ChatServer[]>();
|
||||||
|
public readonly selectedServer = input.required<string>();
|
||||||
|
public readonly selectedServerChange = output<string>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
effect(() => {
|
||||||
|
const chatServers = this.servers();
|
||||||
|
if (this.selectedServer() === '' && chatServers.length > 0) {
|
||||||
|
this.selectedServerChange.emit(chatServers[0].server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface ChatServer {
|
||||||
|
server: string;
|
||||||
|
unreadMessages: number;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import {inject, Injectable, OnDestroy, signal} from '@angular/core';
|
import {computed, inject, Injectable, OnDestroy, signal} from '@angular/core';
|
||||||
import {AuthService} from '@services/auth.service';
|
import {AuthService} from '@services/auth.service';
|
||||||
import {EventSourcePolyfill} from 'event-source-polyfill';
|
import {EventSourcePolyfill} from 'event-source-polyfill';
|
||||||
import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object';
|
import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object';
|
||||||
import {MiniMessageComponent} from '@pages/altitude/chat/mini-message/mini-message.types';
|
|
||||||
import {RawChatMessage} from '@pages/altitude/chat/objects/raw-chat-message.object';
|
import {RawChatMessage} from '@pages/altitude/chat/objects/raw-chat-message.object';
|
||||||
import {normalizeComponent} from '@pages/altitude/chat/mini-message/normalize.util';
|
import {normalizeComponent} from '@pages/altitude/chat/mini-message/normalize.util';
|
||||||
|
import {ChatServer} from '@pages/altitude/chat/objects/chat-server.object';
|
||||||
|
|
||||||
export interface ChatEvent {
|
export interface ChatEvent {
|
||||||
type: string;
|
type: string;
|
||||||
|
|
@ -20,7 +20,11 @@ export class ChatService implements OnDestroy {
|
||||||
private eventSource?: EventSourcePolyfill;
|
private eventSource?: EventSourcePolyfill;
|
||||||
private readonly authService: AuthService = inject(AuthService)
|
private readonly authService: AuthService = inject(AuthService)
|
||||||
private readonly _messages = signal<ChatMessage[]>([])
|
private readonly _messages = signal<ChatMessage[]>([])
|
||||||
public readonly messages = this._messages.asReadonly()
|
public readonly messages = computed(() => this._messages().filter((message) => message.server === this._selectedServer()));
|
||||||
|
private readonly _servers = signal<ChatServer[]>([])
|
||||||
|
public readonly servers = computed(() => this._servers().sort((a, b) => a.server.localeCompare(b.server)));
|
||||||
|
private readonly _selectedServer = signal<string>('');
|
||||||
|
public readonly selectedServer = this._selectedServer.asReadonly()
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
if (this.eventSource) {
|
if (this.eventSource) {
|
||||||
|
|
@ -43,21 +47,17 @@ export class ChatService implements OnDestroy {
|
||||||
this.eventSource = source;
|
this.eventSource = source;
|
||||||
|
|
||||||
this.on(source, 'connected', (event) => {
|
this.on(source, 'connected', (event) => {
|
||||||
const raw = JSON.parse(event.data) as RawChatMessage[];
|
this.processChatMessages(event);
|
||||||
const messages: ChatMessage[] = raw.map((m) => ({
|
|
||||||
...m,
|
|
||||||
messageJson: normalizeComponent(JSON.parse(m.messageJson)),
|
|
||||||
}));
|
|
||||||
this._messages.update((old) => [...old, ...messages]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on(source, 'chat', (event) => {
|
this.on(source, 'chat', (event) => {
|
||||||
const raw = JSON.parse(event.data) as RawChatMessage[];
|
const chatMessages = this.processChatMessages(event);
|
||||||
const messages: ChatMessage[] = raw.map((m) => ({
|
chatMessages.forEach((message) => {
|
||||||
...m,
|
const find = this.servers().find(server => message.server === server.server);
|
||||||
messageJson: JSON.parse(m.messageJson) as MiniMessageComponent,
|
if (find) {
|
||||||
}));
|
find.unreadMessages++;
|
||||||
this._messages.update((old) => [...old, ...messages]);
|
}
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
source.onerror = (err) => {
|
source.onerror = (err) => {
|
||||||
|
|
@ -65,7 +65,30 @@ export class ChatService implements OnDestroy {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// single, deliberate escape hatch from the broken DOM/polyfill type overlap —
|
private processChatMessages(event: SsePayloadEvent): ChatMessage[] {
|
||||||
|
const raw = JSON.parse(event.data) as RawChatMessage[];
|
||||||
|
const messages: ChatMessage[] = raw.map((m) => ({
|
||||||
|
...m,
|
||||||
|
messageJson: normalizeComponent(JSON.parse(m.messageJson)),
|
||||||
|
}));
|
||||||
|
this._messages.update((old) => [...old, ...messages]);
|
||||||
|
|
||||||
|
//TODO [Stijn] [2026-07-19]: Handle servers by getting them from backend
|
||||||
|
messages.map((message) => message.server).forEach((server) => {
|
||||||
|
const found = this._servers().find(existing => existing.server === server);
|
||||||
|
if (!found) {
|
||||||
|
this._servers.update((old) => [...old, {server: server, unreadMessages: 0}]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectServer(server: string) {
|
||||||
|
this._selectedServer.set(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
// single, deliberate escape hatch from the broken DOM/polyfill type overlap —
|
||||||
// everything else in this file stays fully typed
|
// everything else in this file stays fully typed
|
||||||
private on(source: EventSourcePolyfill, eventName: string, handler: (event: SsePayloadEvent) => void): void {
|
private on(source: EventSourcePolyfill, eventName: string, handler: (event: SsePayloadEvent) => void): void {
|
||||||
(source as unknown as { addEventListener: (type: string, listener: (event: SsePayloadEvent) => void) => void })
|
(source as unknown as { addEventListener: (type: string, listener: (event: SsePayloadEvent) => void) => void })
|
||||||
|
|
|
||||||
19
frontend/src/app/pages/altitude/chat/svg/circle.svg.ts
Normal file
19
frontend/src/app/pages/altitude/chat/svg/circle.svg.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'svg-circle',
|
||||||
|
imports: [],
|
||||||
|
template: `
|
||||||
|
<svg
|
||||||
|
width="10px"
|
||||||
|
height="10px" viewBox="0 0 6 6" id="meteor-icon-kit__solid-circle-xxxs" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M3 6C1.3431 6 0 4.6569 0 3C0 1.3431 1.3431 0 3 0C4.6569 0 6 1.3431 6 3C6 4.6569 4.6569 6 3 6z"
|
||||||
|
fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class CircleSvgComponent {
|
||||||
|
|
||||||
|
}
|
||||||
21
frontend/src/app/pages/altitude/chat/util/server-color.ts
Normal file
21
frontend/src/app/pages/altitude/chat/util/server-color.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
export function getServerColor(server: string) {
|
||||||
|
if (server === 'bayou') {
|
||||||
|
return "#0290e8";
|
||||||
|
} else if (server === 'lobby') {
|
||||||
|
return "white";
|
||||||
|
} else if (server === 'creative') {
|
||||||
|
return "#FFA500";
|
||||||
|
}
|
||||||
|
return "white";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServerTextColor(server: string) {
|
||||||
|
if (server === 'bayou') {
|
||||||
|
return "white";
|
||||||
|
} else if (server === 'lobby') {
|
||||||
|
return "black";
|
||||||
|
} else if (server === 'creative') {
|
||||||
|
return "black";
|
||||||
|
}
|
||||||
|
return "white";
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user