Fix bugs and clean noise reduction

This commit is contained in:
2026-03-06 02:22:43 +01:00
parent 0ed9ca93d3
commit 2d84fbd91a
39 changed files with 3443 additions and 1544 deletions

View File

@@ -2,6 +2,7 @@
import {
Component,
inject,
computed,
signal
} from '@angular/core';
import { CommonModule } from '@angular/common';
@@ -16,7 +17,8 @@ import {
lucideMonitor,
lucideHash,
lucideUsers,
lucidePlus
lucidePlus,
lucideVolumeX
} from '@ng-icons/lucide';
import {
selectOnlineUsers,
@@ -35,15 +37,18 @@ import { MessagesActions } from '../../../store/messages/messages.actions';
import { WebRTCService } from '../../../core/services/webrtc.service';
import { VoiceSessionService } from '../../../core/services/voice-session.service';
import { VoiceActivityService } from '../../../core/services/voice-activity.service';
import { VoicePlaybackService } from '../../voice/voice-controls/services/voice-playback.service';
import { VoiceControlsComponent } from '../../voice/voice-controls/voice-controls.component';
import {
ContextMenuComponent,
UserAvatarComponent,
ConfirmDialogComponent
ConfirmDialogComponent,
UserVolumeMenuComponent
} from '../../../shared';
import {
Channel,
ChatEvent,
RoomMember,
Room,
User
} from '../../../core/models';
@@ -54,7 +59,16 @@ type TabView = 'channels' | 'users';
@Component({
selector: 'app-rooms-side-panel',
standalone: true,
imports: [CommonModule, FormsModule, NgIcon, VoiceControlsComponent, ContextMenuComponent, UserAvatarComponent, ConfirmDialogComponent],
imports: [
CommonModule,
FormsModule,
NgIcon,
VoiceControlsComponent,
ContextMenuComponent,
UserVolumeMenuComponent,
UserAvatarComponent,
ConfirmDialogComponent
],
viewProviders: [
provideIcons({
lucideMessageSquare,
@@ -64,7 +78,8 @@ type TabView = 'channels' | 'users';
lucideMonitor,
lucideHash,
lucideUsers,
lucidePlus
lucidePlus,
lucideVolumeX
})
],
templateUrl: './rooms-side-panel.component.html'
@@ -76,6 +91,7 @@ export class RoomsSidePanelComponent {
private store = inject(Store);
private webrtc = inject(WebRTCService);
private voiceSessionService = inject(VoiceSessionService);
private voicePlayback = inject(VoicePlaybackService);
voiceActivity = inject(VoiceActivityService);
activeTab = signal<TabView>('channels');
@@ -87,6 +103,31 @@ export class RoomsSidePanelComponent {
activeChannelId = this.store.selectSignal(selectActiveChannelId);
textChannels = this.store.selectSignal(selectTextChannels);
voiceChannels = this.store.selectSignal(selectVoiceChannels);
roomMembers = computed(() => this.currentRoom()?.members ?? []);
offlineRoomMembers = computed(() => {
const current = this.currentUser();
const onlineIds = new Set(this.onlineUsers().map((user) => user.oderId || user.id));
if (current) {
onlineIds.add(current.oderId || current.id);
}
return this.roomMembers().filter((member) => !onlineIds.has(this.roomMemberKey(member)));
});
knownUserCount = computed(() => {
const memberIds = new Set(
this.roomMembers()
.map((member) => this.roomMemberKey(member))
.filter(Boolean)
);
const current = this.currentUser();
if (current) {
memberIds.add(current.oderId || current.id);
}
return memberIds.size;
});
// Channel context menu state
showChannelMenu = signal(false);
@@ -108,6 +149,13 @@ export class RoomsSidePanelComponent {
userMenuY = signal(0);
contextMenuUser = signal<User | null>(null);
// Per-user volume context menu state
showVolumeMenu = signal(false);
volumeMenuX = signal(0);
volumeMenuY = signal(0);
volumeMenuPeerId = signal('');
volumeMenuDisplayName = signal('');
/** Return online users excluding the current user. */
// Filter out current user from online users list
onlineUsersFiltered() {
@@ -118,6 +166,10 @@ export class RoomsSidePanelComponent {
return this.onlineUsers().filter((user) => user.id !== currentId && user.oderId !== currentOderId);
}
private roomMemberKey(member: RoomMember): string {
return member.oderId || member.id;
}
/** Check whether the current user has permission to manage channels. */
canManageChannels(): boolean {
const room = this.currentRoom();
@@ -287,9 +339,27 @@ export class RoomsSidePanelComponent {
this.showUserMenu.set(false);
}
/** Open the per-user volume context menu for a voice channel participant. */
openVoiceUserVolumeMenu(evt: MouseEvent, user: User) {
evt.preventDefault();
// Don't show volume menu for the local user
const me = this.currentUser();
if (user.id === me?.id || user.oderId === me?.oderId)
return;
this.volumeMenuPeerId.set(user.oderId || user.id);
this.volumeMenuDisplayName.set(user.displayName);
this.volumeMenuX.set(evt.clientX);
this.volumeMenuY.set(evt.clientY);
this.showVolumeMenu.set(true);
}
/** Change a user's role and broadcast the update to connected peers. */
changeUserRole(role: 'admin' | 'moderator' | 'member') {
const user = this.contextMenuUser();
const roomId = this.currentRoom()?.id;
this.closeUserMenu();
@@ -298,6 +368,7 @@ export class RoomsSidePanelComponent {
// Broadcast role change to peers
this.webrtc.broadcastMessage({
type: 'role-change',
roomId,
targetUserId: user.id,
role
});
@@ -377,11 +448,29 @@ export class RoomsSidePanelComponent {
private onVoiceJoinSucceeded(roomId: string, room: Room, current: User | null): void {
this.updateVoiceStateStore(roomId, room, current);
this.trackCurrentUserMic();
this.startVoiceHeartbeat(roomId, room);
this.broadcastVoiceConnected(roomId, room, current);
this.startVoiceSession(roomId, room);
}
private trackCurrentUserMic(): void {
const userId = this.currentUser()?.oderId || this.currentUser()?.id;
const micStream = this.webrtc.getRawMicStream();
if (userId && micStream) {
this.voiceActivity.trackLocalMic(userId, micStream);
}
}
private untrackCurrentUserMic(): void {
const userId = this.currentUser()?.oderId || this.currentUser()?.id;
if (userId) {
this.voiceActivity.untrackLocalMic(userId);
}
}
private updateVoiceStateStore(roomId: string, room: Room, current: User | null): void {
if (!current?.id)
return;
@@ -445,6 +534,8 @@ export class RoomsSidePanelComponent {
// Stop voice heartbeat
this.webrtc.stopVoiceHeartbeat();
this.untrackCurrentUserMic();
// Disable voice locally
this.webrtc.disableVoice();
@@ -484,11 +575,7 @@ export class RoomsSidePanelComponent {
/** Count the number of users connected to a voice channel in the current room. */
voiceOccupancy(roomId: string): number {
const users = this.onlineUsers();
const room = this.currentRoom();
return users.filter((user) => !!user.voiceState?.isConnected && user.voiceState?.roomId === roomId && user.voiceState?.serverId === room?.id)
.length;
return this.voiceUsersInRoom(roomId).length;
}
/** Dispatch a viewer:focus event to display a remote user's screen share. */
@@ -505,6 +592,13 @@ export class RoomsSidePanelComponent {
window.dispatchEvent(evt);
}
/** Check whether the local user has muted a specific voice user. */
isUserLocallyMuted(user: User): boolean {
const peerId = user.oderId || user.id;
return this.voicePlayback.isUserMuted(peerId);
}
/** Check whether a user is currently sharing their screen. */
isUserSharing(userId: string): boolean {
const me = this.currentUser();
@@ -524,13 +618,33 @@ export class RoomsSidePanelComponent {
return !!stream && stream.getVideoTracks().length > 0;
}
/** Return all users currently connected to a specific voice channel. */
/** Return all users currently connected to a specific voice channel, including the local user. */
voiceUsersInRoom(roomId: string) {
const room = this.currentRoom();
return this.onlineUsers().filter(
const me = this.currentUser();
const remoteUsers = this.onlineUsers().filter(
(user) => !!user.voiceState?.isConnected && user.voiceState?.roomId === roomId && user.voiceState?.serverId === room?.id
);
// Include the local user at the top if they are in this voice channel
if (
me?.voiceState?.isConnected &&
me.voiceState?.roomId === roomId &&
me.voiceState?.serverId === room?.id
) {
// Avoid duplicates if the current user is already in onlineUsers
const meId = me.id;
const meOderId = me.oderId;
const alreadyIncluded = remoteUsers.some(
(user) => user.id === meId || user.oderId === meOderId
);
if (!alreadyIncluded) {
return [me, ...remoteUsers];
}
}
return remoteUsers;
}
/** Check whether the current user is connected to the specified voice channel. */