Add ServerMessageService for broadcasting player state updates, implement PlayerListState model, and update ChatService to synchronize active player states across servers.
This commit is contained in:
@@ -21,7 +21,16 @@
|
||||
(keyup.enter)="sendMessage()"
|
||||
placeholder="Type a message..."
|
||||
maxlength="256"
|
||||
[disabled]="hiddenMode()"
|
||||
/>
|
||||
<button (click)="sendMessage()">Send</button>
|
||||
<button (click)="sendMessage()" [disabled]="hiddenMode()">Send</button>
|
||||
<button
|
||||
class="visibility-toggle"
|
||||
[class.active]="hiddenMode()"
|
||||
[disabled]="!visibilityConnected() || visibilityUpdating()"
|
||||
(click)="toggleHiddenMode()"
|
||||
>
|
||||
{{ hiddenMode() ? 'Show Online' : 'Hide Online' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -89,5 +89,14 @@
|
||||
&:active {
|
||||
background: #2a6ae0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
&.visibility-toggle.active {
|
||||
background: #b34747;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,18 +26,22 @@ export class ChatBoxComponent {
|
||||
private readonly matSnackBar = inject(MatSnackBar);
|
||||
|
||||
readonly messages = input.required<ChatMessage[]>();
|
||||
readonly hiddenMode = this.chatService.hiddenMode;
|
||||
readonly visibilityConnected = this.chatService.connected;
|
||||
readonly visibilityUpdating = this.chatService.visibilityUpdating;
|
||||
chatText = '';
|
||||
|
||||
readonly canChat = computed(() => {
|
||||
const hasPrivilege = this.authService.hasAccess(['SCOPE_head_mod']);
|
||||
const isServerChannel = this.chatService.selectedChannel()?.type === 'SERVER';
|
||||
if (!hasPrivilege || !isServerChannel) {
|
||||
console.log(`User has privilege ${hasPrivilege}, user in server channel ${isServerChannel}`);
|
||||
}
|
||||
return hasPrivilege && isServerChannel;
|
||||
const channelType = this.chatService.selectedChannel()?.type;
|
||||
const isValidChannel = this.chatService.selectedChannel()?.type === 'SERVER' || this.chatService.selectedChannel()?.type === 'DM';
|
||||
return hasPrivilege && isValidChannel;
|
||||
});
|
||||
|
||||
sendMessage() {
|
||||
if (this.hiddenMode()) {
|
||||
return;
|
||||
}
|
||||
const message = this.chatText.trim();
|
||||
if (!message) {
|
||||
return;
|
||||
@@ -68,4 +72,8 @@ export class ChatBoxComponent {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggleHiddenMode(): void {
|
||||
this.chatService.toggleHiddenMode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {computed, inject, Injectable, OnDestroy, signal} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {AuthService} from '@services/auth.service';
|
||||
import {EventSourcePolyfill} from 'event-source-polyfill';
|
||||
import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object';
|
||||
@@ -9,6 +10,7 @@ import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
|
||||
import {NotificationService} from '@pages/altitude/chat/service/chat-notification.service';
|
||||
import {ChatInfoService} from '@api';
|
||||
import {ServerState} from '@pages/altitude/chat/objects/server-state.object';
|
||||
import {CookieService} from 'ngx-cookie-service';
|
||||
|
||||
interface SsePayloadEvent {
|
||||
data: string;
|
||||
@@ -16,10 +18,15 @@ interface SsePayloadEvent {
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class ChatService implements OnDestroy {
|
||||
private readonly HIDDEN_MODE_COOKIE = 'chat-hidden-mode';
|
||||
private eventSource?: EventSourcePolyfill;
|
||||
private visibilityPollId?: number;
|
||||
private visibilityRevision = 0;
|
||||
private readonly notificationService: NotificationService = inject(NotificationService);
|
||||
private readonly authService: AuthService = inject(AuthService)
|
||||
private readonly chatInfoService: ChatInfoService = inject(ChatInfoService);
|
||||
private readonly httpClient = inject(HttpClient);
|
||||
private readonly cookieService = inject(CookieService);
|
||||
private readonly _messages = signal<ChatMessage[]>([])
|
||||
private readonly _channels = signal<ChatChannel[]>([])
|
||||
public readonly channels = computed(() => this._channels().sort((a, b) => a.name.localeCompare(b.name)));
|
||||
@@ -30,6 +37,12 @@ export class ChatService implements OnDestroy {
|
||||
public readonly partieMap = signal<Map<string, string>>(new Map());
|
||||
public readonly userNameMap = signal<Map<string, string>>(new Map());
|
||||
public readonly userListVisible = signal(true);
|
||||
private readonly _connected = signal(false);
|
||||
public readonly connected = this._connected.asReadonly();
|
||||
private readonly _hiddenMode = signal(this.cookieService.get(this.HIDDEN_MODE_COOKIE) === 'true');
|
||||
public readonly hiddenMode = this._hiddenMode.asReadonly();
|
||||
private readonly _visibilityUpdating = signal(false);
|
||||
public readonly visibilityUpdating = this._visibilityUpdating.asReadonly();
|
||||
private readonly pendingPartyNameRequests = new Set<string>();
|
||||
private readonly pendingUserNameRequests = new Set<string>();
|
||||
|
||||
@@ -67,6 +80,9 @@ export class ChatService implements OnDestroy {
|
||||
|
||||
this.on(source, 'connected', (event) => {
|
||||
this.processChatMessages(event);
|
||||
this._connected.set(true);
|
||||
this.restoreVisibilityAfterConnection();
|
||||
this.startVisibilityPolling();
|
||||
});
|
||||
|
||||
this.on(source, 'chat', (event) => {
|
||||
@@ -92,10 +108,77 @@ export class ChatService implements OnDestroy {
|
||||
});
|
||||
|
||||
source.onerror = (err) => {
|
||||
this._connected.set(false);
|
||||
console.error('SSE error, polyfill will auto-reconnect:', err);
|
||||
};
|
||||
}
|
||||
|
||||
public toggleHiddenMode(): void {
|
||||
if (!this._connected() || this._visibilityUpdating()) {
|
||||
return;
|
||||
}
|
||||
this.setBackendHiddenMode(!this._hiddenMode());
|
||||
}
|
||||
|
||||
private restoreVisibilityAfterConnection(): void {
|
||||
if (this._hiddenMode()) {
|
||||
this.setBackendHiddenMode(true);
|
||||
} else {
|
||||
this.validateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private setBackendHiddenMode(hidden: boolean): void {
|
||||
if (!this._connected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const revision = ++this.visibilityRevision;
|
||||
this._visibilityUpdating.set(true);
|
||||
const path = hidden ? '/api/chat/hide' : '/api/chat/show';
|
||||
this.httpClient.post<void>(path, null).subscribe({
|
||||
next: () => {
|
||||
if (revision === this.visibilityRevision) {
|
||||
this.applyHiddenMode(hidden);
|
||||
}
|
||||
this._visibilityUpdating.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(`Failed to ${hidden ? 'hide' : 'show'} chat session:`, error);
|
||||
this._visibilityUpdating.set(false);
|
||||
this.validateVisibility();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startVisibilityPolling(): void {
|
||||
if (this.visibilityPollId !== undefined) {
|
||||
return;
|
||||
}
|
||||
this.visibilityPollId = window.setInterval(() => this.validateVisibility(), 60_000);
|
||||
}
|
||||
|
||||
private validateVisibility(): void {
|
||||
if (!this._connected() || this._visibilityUpdating()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const revision = this.visibilityRevision;
|
||||
this.httpClient.get<{hidden: boolean}>('/api/chat/visibility').subscribe({
|
||||
next: (visibility) => {
|
||||
if (revision === this.visibilityRevision) {
|
||||
this.applyHiddenMode(visibility.hidden);
|
||||
}
|
||||
},
|
||||
error: (error) => console.error('Failed to validate chat visibility:', error)
|
||||
});
|
||||
}
|
||||
|
||||
private applyHiddenMode(hidden: boolean): void {
|
||||
this._hiddenMode.set(hidden);
|
||||
this.cookieService.set(this.HIDDEN_MODE_COOKIE, String(hidden), 365, '/');
|
||||
}
|
||||
|
||||
private processChatMessages(event: SsePayloadEvent): ChatMessage[] {
|
||||
const raw = JSON.parse(event.data) as RawChatMessage[];
|
||||
const messages: ChatMessage[] = raw.map((m) => {
|
||||
@@ -222,6 +305,11 @@ export class ChatService implements OnDestroy {
|
||||
disconnect(): void {
|
||||
this.eventSource?.close();
|
||||
this.eventSource = undefined;
|
||||
this._connected.set(false);
|
||||
if (this.visibilityPollId !== undefined) {
|
||||
window.clearInterval(this.visibilityPollId);
|
||||
this.visibilityPollId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
Reference in New Issue
Block a user