style: Now uses template files

This commit is contained in:
2026-03-01 14:53:47 +01:00
parent 8c551a90f4
commit d88a476f15
36 changed files with 2089 additions and 2103 deletions

View File

@@ -0,0 +1,199 @@
import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideMessageSquare, lucideMic, lucideMicOff, lucideChevronLeft, lucideMonitor, lucideHash, lucideUsers } from '@ng-icons/lucide';
import { selectOnlineUsers, selectCurrentUser } from '../../../store/users/users.selectors';
import { selectCurrentRoom } from '../../../store/rooms/rooms.selectors';
import * as UsersActions from '../../../store/users/users.actions';
import { WebRTCService } from '../../../core/services/webrtc.service';
import { VoiceSessionService } from '../../../core/services/voice-session.service';
import { VoiceControlsComponent } from '../../voice/voice-controls/voice-controls.component';
type TabView = 'channels' | 'users';
@Component({
selector: 'app-rooms-side-panel',
standalone: true,
imports: [CommonModule, NgIcon, VoiceControlsComponent],
viewProviders: [
provideIcons({ lucideMessageSquare, lucideMic, lucideMicOff, lucideChevronLeft, lucideMonitor, lucideHash, lucideUsers })
],
templateUrl: './rooms-side-panel.component.html',
})
export class RoomsSidePanelComponent {
private store = inject(Store);
private webrtc = inject(WebRTCService);
private voiceSessionService = inject(VoiceSessionService);
activeTab = signal<TabView>('channels');
showFloatingControls = this.voiceSessionService.showFloatingControls;
onlineUsers = this.store.selectSignal(selectOnlineUsers);
currentUser = this.store.selectSignal(selectCurrentUser);
currentRoom = this.store.selectSignal(selectCurrentRoom);
// Filter out current user from online users list
onlineUsersFiltered() {
const current = this.currentUser();
const currentId = current?.id;
const currentOderId = current?.oderId;
return this.onlineUsers().filter(u => u.id !== currentId && u.oderId !== currentOderId);
}
joinVoice(roomId: string) {
// Gate by room permissions
const room = this.currentRoom();
if (room && room.permissions && room.permissions.allowVoice === false) {
console.warn('Voice is disabled by room permissions');
return;
}
const current = this.currentUser();
// Check if already connected to voice in a DIFFERENT server - must disconnect first
if (current?.voiceState?.isConnected && current.voiceState.serverId !== room?.id) {
// Connected to voice in a different server - user must disconnect first
console.warn('Already connected to voice in another server. Disconnect first before joining.');
return;
}
// If switching channels within the same server, just update the room
const isSwitchingChannels = current?.voiceState?.isConnected &&
current.voiceState.serverId === room?.id &&
current.voiceState.roomId !== roomId;
// Enable microphone and broadcast voice-state
const enableVoicePromise = isSwitchingChannels ? Promise.resolve() : this.webrtc.enableVoice();
enableVoicePromise.then(() => {
if (current?.id && room) {
this.store.dispatch(UsersActions.updateVoiceState({
userId: current.id,
voiceState: { isConnected: true, isMuted: current.voiceState?.isMuted ?? false, isDeafened: current.voiceState?.isDeafened ?? false, roomId: roomId, serverId: room.id }
}));
}
// Start voice heartbeat to broadcast presence every 5 seconds
this.webrtc.startVoiceHeartbeat(roomId);
this.webrtc.broadcastMessage({
type: 'voice-state',
oderId: current?.oderId || current?.id,
displayName: current?.displayName || 'User',
voiceState: { isConnected: true, isMuted: current?.voiceState?.isMuted ?? false, isDeafened: current?.voiceState?.isDeafened ?? false, roomId: roomId, serverId: room?.id }
});
// Update voice session for floating controls
if (room) {
const voiceRoomName = roomId === 'general' ? '🔊 General' : roomId === 'afk' ? '🔕 AFK' : roomId;
this.voiceSessionService.startSession({
serverId: room.id,
serverName: room.name,
roomId: roomId,
roomName: voiceRoomName,
serverIcon: room.icon,
serverDescription: room.description,
serverRoute: `/room/${room.id}`,
});
}
}).catch((e) => console.error('Failed to join voice room', roomId, e));
}
leaveVoice(roomId: string) {
const current = this.currentUser();
// Only leave if currently in this room
if (!(current?.voiceState?.isConnected && current.voiceState.roomId === roomId)) return;
// Stop voice heartbeat
this.webrtc.stopVoiceHeartbeat();
// Disable voice locally
this.webrtc.disableVoice();
// Update store voice state
if (current?.id) {
this.store.dispatch(UsersActions.updateVoiceState({
userId: current.id,
voiceState: { isConnected: false, isMuted: false, isDeafened: false, roomId: undefined, serverId: undefined }
}));
}
// Broadcast disconnect
this.webrtc.broadcastMessage({
type: 'voice-state',
oderId: current?.oderId || current?.id,
displayName: current?.displayName || 'User',
voiceState: { isConnected: false, isMuted: false, isDeafened: false, roomId: undefined, serverId: undefined }
});
// End voice session
this.voiceSessionService.endSession();
}
voiceOccupancy(roomId: string): number {
const users = this.onlineUsers();
const room = this.currentRoom();
// Only count users connected to voice in this specific server and room
return users.filter(u =>
!!u.voiceState?.isConnected &&
u.voiceState?.roomId === roomId &&
u.voiceState?.serverId === room?.id
).length;
}
viewShare(userId: string) {
// Focus viewer on a user's stream if present
// Requires WebRTCService to expose a remote streams registry.
const evt = new CustomEvent('viewer:focus', { detail: { userId } });
window.dispatchEvent(evt);
}
viewStream(userId: string) {
// Focus viewer on a user's stream - dispatches event to screen-share-viewer
const evt = new CustomEvent('viewer:focus', { detail: { userId } });
window.dispatchEvent(evt);
}
isUserSharing(userId: string): boolean {
const me = this.currentUser();
if (me?.id === userId) {
// Local user: use signal
return this.webrtc.isScreenSharing();
}
// For remote users, check the store state first (authoritative)
const user = this.onlineUsers().find(u => u.id === userId || u.oderId === userId);
if (user?.screenShareState?.isSharing === false) {
// Store says not sharing - trust this over stream presence
return false;
}
// Fall back to checking stream if store state is undefined
const stream = this.webrtc.getRemoteStream(userId);
return !!stream && stream.getVideoTracks().length > 0;
}
voiceUsersInRoom(roomId: string) {
const room = this.currentRoom();
// Only show users connected to voice in this specific server and room
return this.onlineUsers().filter(u =>
!!u.voiceState?.isConnected &&
u.voiceState?.roomId === roomId &&
u.voiceState?.serverId === room?.id
);
}
isCurrentRoom(roomId: string): boolean {
const me = this.currentUser();
const room = this.currentRoom();
// Check that voice is connected AND both the server AND room match
return !!(
me?.voiceState?.isConnected &&
me.voiceState?.roomId === roomId &&
me.voiceState?.serverId === room?.id
);
}
voiceEnabled(): boolean {
const room = this.currentRoom();
return room?.permissions?.allowVoice !== false;
}
}