Move toju-app into own its folder
This commit is contained in:
@@ -0,0 +1,663 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering */
|
||||
import {
|
||||
Component,
|
||||
inject,
|
||||
computed,
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
lucideMessageSquare,
|
||||
lucideMic,
|
||||
lucideMicOff,
|
||||
lucideChevronLeft,
|
||||
lucideMonitor,
|
||||
lucideHash,
|
||||
lucideUsers,
|
||||
lucidePlus,
|
||||
lucideVolumeX
|
||||
} from '@ng-icons/lucide';
|
||||
import {
|
||||
selectOnlineUsers,
|
||||
selectCurrentUser,
|
||||
selectIsCurrentUserAdmin
|
||||
} from '../../../store/users/users.selectors';
|
||||
import {
|
||||
selectCurrentRoom,
|
||||
selectActiveChannelId,
|
||||
selectTextChannels,
|
||||
selectVoiceChannels
|
||||
} from '../../../store/rooms/rooms.selectors';
|
||||
import { UsersActions } from '../../../store/users/users.actions';
|
||||
import { RoomsActions } from '../../../store/rooms/rooms.actions';
|
||||
import { MessagesActions } from '../../../store/messages/messages.actions';
|
||||
import { RealtimeSessionFacade } from '../../../core/realtime';
|
||||
import { ScreenShareFacade } from '../../../domains/screen-share';
|
||||
import { VoiceActivityService, VoiceConnectionFacade } from '../../../domains/voice-connection';
|
||||
import { VoiceSessionFacade, VoiceWorkspaceService } from '../../../domains/voice-session';
|
||||
import { VoicePlaybackService } from '../../../domains/voice-connection/application/voice-playback.service';
|
||||
import { VoiceControlsComponent } from '../../../domains/voice-session/feature/voice-controls/voice-controls.component';
|
||||
import {
|
||||
ContextMenuComponent,
|
||||
UserAvatarComponent,
|
||||
ConfirmDialogComponent,
|
||||
UserVolumeMenuComponent
|
||||
} from '../../../shared';
|
||||
import {
|
||||
Channel,
|
||||
ChatEvent,
|
||||
RoomMember,
|
||||
Room,
|
||||
User
|
||||
} from '../../../shared-kernel';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
type TabView = 'channels' | 'users';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rooms-side-panel',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
NgIcon,
|
||||
VoiceControlsComponent,
|
||||
ContextMenuComponent,
|
||||
UserVolumeMenuComponent,
|
||||
UserAvatarComponent,
|
||||
ConfirmDialogComponent
|
||||
],
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
lucideMessageSquare,
|
||||
lucideMic,
|
||||
lucideMicOff,
|
||||
lucideChevronLeft,
|
||||
lucideMonitor,
|
||||
lucideHash,
|
||||
lucideUsers,
|
||||
lucidePlus,
|
||||
lucideVolumeX
|
||||
})
|
||||
],
|
||||
templateUrl: './rooms-side-panel.component.html'
|
||||
})
|
||||
export class RoomsSidePanelComponent {
|
||||
private store = inject(Store);
|
||||
private realtime = inject(RealtimeSessionFacade);
|
||||
private voiceConnection = inject(VoiceConnectionFacade);
|
||||
private screenShare = inject(ScreenShareFacade);
|
||||
private voiceSessionService = inject(VoiceSessionFacade);
|
||||
private voiceWorkspace = inject(VoiceWorkspaceService);
|
||||
private voicePlayback = inject(VoicePlaybackService);
|
||||
voiceActivity = inject(VoiceActivityService);
|
||||
|
||||
activeTab = signal<TabView>('channels');
|
||||
showFloatingControls = this.voiceSessionService.showFloatingControls;
|
||||
isVoiceWorkspaceExpanded = this.voiceWorkspace.isExpanded;
|
||||
onlineUsers = this.store.selectSignal(selectOnlineUsers);
|
||||
currentUser = this.store.selectSignal(selectCurrentUser);
|
||||
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
||||
isAdmin = this.store.selectSignal(selectIsCurrentUserAdmin);
|
||||
activeChannelId = this.store.selectSignal(selectActiveChannelId);
|
||||
textChannels = this.store.selectSignal(selectTextChannels);
|
||||
voiceChannels = this.store.selectSignal(selectVoiceChannels);
|
||||
roomMembers = computed(() => this.currentRoom()?.members ?? []);
|
||||
roomMemberIdentifiers = computed(() => {
|
||||
const identifiers = new Set<string>();
|
||||
|
||||
for (const member of this.roomMembers()) {
|
||||
this.addIdentifiers(identifiers, member);
|
||||
}
|
||||
|
||||
return identifiers;
|
||||
});
|
||||
onlineRoomUsers = computed(() => {
|
||||
const memberIdentifiers = this.roomMemberIdentifiers();
|
||||
|
||||
return this.onlineUsers().filter((user) => !this.isCurrentUserIdentity(user) && this.matchesIdentifiers(memberIdentifiers, user));
|
||||
});
|
||||
offlineRoomMembers = computed(() => {
|
||||
const onlineIdentifiers = new Set<string>();
|
||||
|
||||
for (const user of this.onlineRoomUsers()) {
|
||||
this.addIdentifiers(onlineIdentifiers, user);
|
||||
}
|
||||
|
||||
this.addIdentifiers(onlineIdentifiers, this.currentUser());
|
||||
|
||||
return this.roomMembers().filter((member) => !this.matchesIdentifiers(onlineIdentifiers, 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;
|
||||
});
|
||||
|
||||
showChannelMenu = signal(false);
|
||||
channelMenuX = signal(0);
|
||||
channelMenuY = signal(0);
|
||||
contextChannel = signal<Channel | null>(null);
|
||||
|
||||
renamingChannelId = signal<string | null>(null);
|
||||
|
||||
showCreateChannelDialog = signal(false);
|
||||
createChannelType = signal<'text' | 'voice'>('text');
|
||||
newChannelName = '';
|
||||
|
||||
showUserMenu = signal(false);
|
||||
userMenuX = signal(0);
|
||||
userMenuY = signal(0);
|
||||
contextMenuUser = signal<User | null>(null);
|
||||
|
||||
showVolumeMenu = signal(false);
|
||||
volumeMenuX = signal(0);
|
||||
volumeMenuY = signal(0);
|
||||
volumeMenuPeerId = signal('');
|
||||
volumeMenuDisplayName = signal('');
|
||||
|
||||
private roomMemberKey(member: RoomMember): string {
|
||||
return member.oderId || member.id;
|
||||
}
|
||||
|
||||
private addIdentifiers(identifiers: Set<string>, entity: { id?: string; oderId?: string } | null | undefined): void {
|
||||
if (!entity)
|
||||
return;
|
||||
|
||||
if (entity.id) {
|
||||
identifiers.add(entity.id);
|
||||
}
|
||||
|
||||
if (entity.oderId) {
|
||||
identifiers.add(entity.oderId);
|
||||
}
|
||||
}
|
||||
|
||||
private matchesIdentifiers(identifiers: Set<string>, entity: { id?: string; oderId?: string }): boolean {
|
||||
return !!((entity.id && identifiers.has(entity.id)) || (entity.oderId && identifiers.has(entity.oderId)));
|
||||
}
|
||||
|
||||
private isCurrentUserIdentity(entity: { id?: string; oderId?: string }): boolean {
|
||||
const current = this.currentUser();
|
||||
|
||||
return !!current && (
|
||||
(typeof entity.id === 'string' && entity.id === current.id)
|
||||
|| (typeof entity.oderId === 'string' && entity.oderId === current.oderId)
|
||||
);
|
||||
}
|
||||
|
||||
canManageChannels(): boolean {
|
||||
const room = this.currentRoom();
|
||||
const user = this.currentUser();
|
||||
|
||||
if (!room || !user)
|
||||
return false;
|
||||
|
||||
if (room.hostId === user.id)
|
||||
return true;
|
||||
|
||||
const perms = room.permissions || {};
|
||||
|
||||
if (user.role === 'admin' && perms.adminsManageRooms)
|
||||
return true;
|
||||
|
||||
if (user.role === 'moderator' && perms.moderatorsManageRooms)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
selectTextChannel(channelId: string) {
|
||||
if (this.renamingChannelId())
|
||||
return;
|
||||
|
||||
this.voiceWorkspace.showChat();
|
||||
this.store.dispatch(RoomsActions.selectChannel({ channelId }));
|
||||
}
|
||||
|
||||
openChannelContextMenu(evt: MouseEvent, channel: Channel) {
|
||||
evt.preventDefault();
|
||||
this.contextChannel.set(channel);
|
||||
this.channelMenuX.set(evt.clientX);
|
||||
this.channelMenuY.set(evt.clientY);
|
||||
this.showChannelMenu.set(true);
|
||||
}
|
||||
|
||||
closeChannelMenu() {
|
||||
this.showChannelMenu.set(false);
|
||||
}
|
||||
|
||||
startRename() {
|
||||
const ch = this.contextChannel();
|
||||
|
||||
this.closeChannelMenu();
|
||||
|
||||
if (ch) {
|
||||
this.renamingChannelId.set(ch.id);
|
||||
}
|
||||
}
|
||||
|
||||
confirmRename(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const name = input.value.trim();
|
||||
const channelId = this.renamingChannelId();
|
||||
|
||||
if (channelId && name) {
|
||||
this.store.dispatch(RoomsActions.renameChannel({ channelId, name }));
|
||||
}
|
||||
|
||||
this.renamingChannelId.set(null);
|
||||
}
|
||||
|
||||
cancelRename() {
|
||||
this.renamingChannelId.set(null);
|
||||
}
|
||||
|
||||
deleteChannel() {
|
||||
const ch = this.contextChannel();
|
||||
|
||||
this.closeChannelMenu();
|
||||
|
||||
if (ch) {
|
||||
this.store.dispatch(RoomsActions.removeChannel({ channelId: ch.id }));
|
||||
}
|
||||
}
|
||||
|
||||
resyncMessages() {
|
||||
this.closeChannelMenu();
|
||||
const room = this.currentRoom();
|
||||
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.store.dispatch(MessagesActions.startSync());
|
||||
|
||||
const peers = this.realtime.getConnectedPeers();
|
||||
const inventoryRequest: ChatEvent = { type: 'chat-inventory-request', roomId: room.id };
|
||||
|
||||
peers.forEach((pid) => {
|
||||
try {
|
||||
this.realtime.sendToPeer(pid, inventoryRequest);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createChannel(type: 'text' | 'voice') {
|
||||
this.createChannelType.set(type);
|
||||
this.newChannelName = '';
|
||||
this.showCreateChannelDialog.set(true);
|
||||
}
|
||||
|
||||
confirmCreateChannel() {
|
||||
const name = this.newChannelName.trim();
|
||||
|
||||
if (!name)
|
||||
return;
|
||||
|
||||
const type = this.createChannelType();
|
||||
const existing = type === 'text' ? this.textChannels() : this.voiceChannels();
|
||||
const channel: Channel = {
|
||||
id: type === 'voice' ? `vc-${uuidv4().slice(0, 8)}` : uuidv4().slice(0, 8),
|
||||
name,
|
||||
type,
|
||||
position: existing.length
|
||||
};
|
||||
|
||||
this.store.dispatch(RoomsActions.addChannel({ channel }));
|
||||
this.showCreateChannelDialog.set(false);
|
||||
}
|
||||
|
||||
cancelCreateChannel() {
|
||||
this.showCreateChannelDialog.set(false);
|
||||
}
|
||||
|
||||
openUserContextMenu(evt: MouseEvent, user: User) {
|
||||
evt.preventDefault();
|
||||
|
||||
if (!this.isAdmin())
|
||||
return;
|
||||
|
||||
this.contextMenuUser.set(user);
|
||||
this.userMenuX.set(evt.clientX);
|
||||
this.userMenuY.set(evt.clientY);
|
||||
this.showUserMenu.set(true);
|
||||
}
|
||||
|
||||
closeUserMenu() {
|
||||
this.showUserMenu.set(false);
|
||||
}
|
||||
|
||||
openVoiceUserVolumeMenu(evt: MouseEvent, user: User) {
|
||||
evt.preventDefault();
|
||||
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);
|
||||
}
|
||||
|
||||
changeUserRole(role: 'admin' | 'moderator' | 'member') {
|
||||
const user = this.contextMenuUser();
|
||||
const roomId = this.currentRoom()?.id;
|
||||
|
||||
this.closeUserMenu();
|
||||
|
||||
if (user) {
|
||||
this.store.dispatch(UsersActions.updateUserRole({ userId: user.id, role }));
|
||||
this.realtime.broadcastMessage({
|
||||
type: 'role-change',
|
||||
roomId,
|
||||
targetUserId: user.id,
|
||||
role
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
kickUserAction() {
|
||||
const user = this.contextMenuUser();
|
||||
|
||||
this.closeUserMenu();
|
||||
|
||||
if (user) {
|
||||
this.store.dispatch(UsersActions.kickUser({ userId: user.id }));
|
||||
}
|
||||
}
|
||||
|
||||
joinVoice(roomId: string) {
|
||||
const room = this.currentRoom();
|
||||
const current = this.currentUser();
|
||||
|
||||
if (
|
||||
room
|
||||
&& current?.voiceState?.isConnected
|
||||
&& current.voiceState.roomId === roomId
|
||||
&& current.voiceState.serverId === room.id
|
||||
) {
|
||||
this.voiceWorkspace.open(null, { connectRemoteShares: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (room && room.permissions && room.permissions.allowVoice === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!room)
|
||||
return;
|
||||
|
||||
if (current?.voiceState?.isConnected && current.voiceState.serverId !== room?.id) {
|
||||
if (!this.voiceConnection.isVoiceConnected()) {
|
||||
if (current.id) {
|
||||
this.store.dispatch(
|
||||
UsersActions.updateVoiceState({
|
||||
userId: current.id,
|
||||
voiceState: {
|
||||
isConnected: false,
|
||||
isMuted: false,
|
||||
isDeafened: false,
|
||||
roomId: undefined,
|
||||
serverId: undefined
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const isSwitchingChannels = current?.voiceState?.isConnected && current.voiceState.serverId === room?.id && current.voiceState.roomId !== roomId;
|
||||
const enableVoicePromise = isSwitchingChannels ? Promise.resolve() : this.voiceConnection.enableVoice();
|
||||
|
||||
enableVoicePromise
|
||||
.then(() => this.onVoiceJoinSucceeded(roomId, room, current ?? null))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
|
||||
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.voiceConnection.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;
|
||||
|
||||
this.store.dispatch(
|
||||
UsersActions.updateVoiceState({
|
||||
userId: current.id,
|
||||
voiceState: {
|
||||
isConnected: true,
|
||||
isMuted: current.voiceState?.isMuted ?? false,
|
||||
isDeafened: current.voiceState?.isDeafened ?? false,
|
||||
roomId,
|
||||
serverId: room.id
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private startVoiceHeartbeat(roomId: string, room: Room): void {
|
||||
this.voiceConnection.startVoiceHeartbeat(roomId, room.id);
|
||||
}
|
||||
|
||||
private broadcastVoiceConnected(roomId: string, room: Room, current: User | null): void {
|
||||
this.voiceConnection.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,
|
||||
serverId: room.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startVoiceSession(roomId: string, room: Room): void {
|
||||
const voiceChannel = this.voiceChannels().find((channel) => channel.id === roomId);
|
||||
const voiceRoomName = voiceChannel ? `🔊 ${voiceChannel.name}` : roomId;
|
||||
|
||||
this.voiceSessionService.startSession({
|
||||
serverId: room.id,
|
||||
serverName: room.name,
|
||||
roomId,
|
||||
roomName: voiceRoomName,
|
||||
serverIcon: room.icon,
|
||||
serverDescription: room.description,
|
||||
serverRoute: `/room/${room.id}`
|
||||
});
|
||||
}
|
||||
|
||||
leaveVoice(roomId: string) {
|
||||
const current = this.currentUser();
|
||||
|
||||
if (!(current?.voiceState?.isConnected && current.voiceState.roomId === roomId))
|
||||
return;
|
||||
|
||||
this.voiceConnection.stopVoiceHeartbeat();
|
||||
|
||||
this.untrackCurrentUserMic();
|
||||
|
||||
this.voiceConnection.disableVoice();
|
||||
|
||||
if (current?.id) {
|
||||
this.store.dispatch(
|
||||
UsersActions.updateVoiceState({
|
||||
userId: current.id,
|
||||
voiceState: {
|
||||
isConnected: false,
|
||||
isMuted: false,
|
||||
isDeafened: false,
|
||||
roomId: undefined,
|
||||
serverId: undefined
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.voiceConnection.broadcastMessage({
|
||||
type: 'voice-state',
|
||||
oderId: current?.oderId || current?.id,
|
||||
displayName: current?.displayName || 'User',
|
||||
voiceState: {
|
||||
isConnected: false,
|
||||
isMuted: false,
|
||||
isDeafened: false,
|
||||
roomId: undefined,
|
||||
serverId: undefined
|
||||
}
|
||||
});
|
||||
|
||||
this.voiceSessionService.endSession();
|
||||
}
|
||||
|
||||
voiceOccupancy(roomId: string): number {
|
||||
return this.voiceUsersInRoom(roomId).length;
|
||||
}
|
||||
|
||||
viewShare(userId: string) {
|
||||
this.voiceWorkspace.focusStream(userId, { connectRemoteShares: true });
|
||||
}
|
||||
|
||||
viewStream(userId: string) {
|
||||
this.voiceWorkspace.focusStream(userId, { connectRemoteShares: true });
|
||||
}
|
||||
|
||||
isUserLocallyMuted(user: User): boolean {
|
||||
const peerId = user.oderId || user.id;
|
||||
|
||||
return this.voicePlayback.isUserMuted(peerId);
|
||||
}
|
||||
|
||||
isUserSharing(userId: string): boolean {
|
||||
const me = this.currentUser();
|
||||
|
||||
if (me?.id === userId) {
|
||||
return this.screenShare.isScreenSharing();
|
||||
}
|
||||
|
||||
const user = this.onlineUsers().find((onlineUser) => onlineUser.id === userId || onlineUser.oderId === userId);
|
||||
|
||||
if (user?.screenShareState?.isSharing === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const peerKeys = [
|
||||
user?.oderId,
|
||||
user?.id,
|
||||
userId
|
||||
].filter(
|
||||
(candidate): candidate is string => !!candidate
|
||||
);
|
||||
const stream = peerKeys
|
||||
.map((peerKey) => this.screenShare.getRemoteScreenShareStream(peerKey))
|
||||
.find((candidate) => !!candidate && candidate.getVideoTracks().length > 0) || null;
|
||||
|
||||
return !!stream && stream.getVideoTracks().length > 0;
|
||||
}
|
||||
|
||||
voiceUsersInRoom(roomId: string) {
|
||||
const room = this.currentRoom();
|
||||
const me = this.currentUser();
|
||||
const remoteUsers = this.onlineUsers().filter(
|
||||
(user) => !!user.voiceState?.isConnected && user.voiceState?.roomId === roomId && user.voiceState?.serverId === room?.id
|
||||
);
|
||||
|
||||
if (
|
||||
me?.voiceState?.isConnected &&
|
||||
me.voiceState?.roomId === roomId &&
|
||||
me.voiceState?.serverId === room?.id
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
isCurrentRoom(roomId: string): boolean {
|
||||
const me = this.currentUser();
|
||||
const room = this.currentRoom();
|
||||
|
||||
return !!(me?.voiceState?.isConnected && me.voiceState?.roomId === roomId && me.voiceState?.serverId === room?.id);
|
||||
}
|
||||
|
||||
voiceEnabled(): boolean {
|
||||
const room = this.currentRoom();
|
||||
|
||||
return room?.permissions?.allowVoice !== false;
|
||||
}
|
||||
|
||||
getPeerLatency(user: User): number | null {
|
||||
const latencies = this.voiceConnection.peerLatencies();
|
||||
|
||||
return latencies.get(user.oderId ?? '') ?? latencies.get(user.id) ?? null;
|
||||
}
|
||||
|
||||
getPingColorClass(user: User): string {
|
||||
const ms = this.getPeerLatency(user);
|
||||
|
||||
if (ms === null)
|
||||
return 'bg-gray-500';
|
||||
|
||||
if (ms < 100)
|
||||
return 'bg-green-500';
|
||||
|
||||
if (ms < 200)
|
||||
return 'bg-yellow-500';
|
||||
|
||||
if (ms < 350)
|
||||
return 'bg-orange-500';
|
||||
|
||||
return 'bg-red-500';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user