wip: optimizations

This commit is contained in:
2026-05-23 15:28:40 +02:00
parent 5bf506af03
commit 155fe20862
89 changed files with 7431 additions and 392 deletions

View File

@@ -1,4 +1,8 @@
import { Component, input, output } from '@angular/core';
import {
Component,
input,
output
} from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucideMic,

View File

@@ -64,6 +64,16 @@
}
</div>
<div class="space-y-1">
@if (showTextChannelSkeleton()) {
<app-skeleton-list
[rows]="3"
rowHeight="0.875rem"
primaryWidth="70%"
[showAvatar]="true"
avatarSize="1rem"
/>
}
@for (ch of textChannels(); track ch.id) {
<button
appThemeNode="roomTextChannelItem"
@@ -132,6 +142,16 @@
<p class="px-2 py-2 text-sm text-muted-foreground">Voice is disabled by host</p>
}
<div class="space-y-1">
@if (showVoiceChannelSkeleton()) {
<app-skeleton-list
[rows]="2"
rowHeight="0.875rem"
primaryWidth="65%"
[showAvatar]="true"
avatarSize="1rem"
/>
}
@for (ch of voiceChannels(); track ch.id) {
<div
class="rounded-md transition-colors"
@@ -259,7 +279,7 @@
</div>
</section>
@if (pluginChannelSections().length > 0 || pluginMenuActions().length > 0 || pluginSidePanels().length > 0) {
@if (pluginChannelSections().length > 0 || pluginMenuActions().length > 0 || pluginSidePanels().length > 0 || showPluginSkeleton()) {
<section
class="border-t border-border px-2 py-3"
data-testid="plugin-room-side-panel"
@@ -281,6 +301,28 @@
</button>
</div>
@if (showPluginSkeleton()) {
<div class="space-y-2 px-1 py-1">
<div class="flex items-center gap-2 rounded-md px-1 py-1.5">
<app-skeleton
width="1rem"
height="1rem"
rounded="md"
[block]="false"
/>
<app-skeleton
width="62%"
height="0.875rem"
/>
</div>
<app-skeleton
width="100%"
height="3.25rem"
rounded="md"
/>
</div>
}
@if (pluginChannelSections().length > 0) {
<div class="space-y-1">
@for (record of pluginChannelSections(); track record.id) {
@@ -496,7 +538,7 @@
<div class="space-y-1">
@for (member of offlineRoomMembers(); track member.oderId || member.id) {
<div
class="group/user flex items-center gap-2 rounded-md px-3 py-2 opacity-80 hover:bg-secondary/30 transition-colors cursor-pointer"
class="group/user flex items-center gap-2 rounded-md px-3 py-2 opacity-80 hover:bg-secondary/30 transition-colors cursor-pointer [content-visibility:auto] [contain-intrinsic-size:auto_48px]"
[attr.data-testid]="'room-user-card-' + (member.oderId || member.id)"
role="button"
tabindex="0"

View File

@@ -1,13 +1,16 @@
/* eslint-disable @typescript-eslint/member-ordering */
import {
Component,
DestroyRef,
inject,
computed,
effect,
input,
OnDestroy,
output,
signal
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
@@ -33,8 +36,10 @@ import {
selectCurrentRoom,
selectActiveChannelId,
selectTextChannels,
selectVoiceChannels
selectVoiceChannels,
selectIsConnecting
} from '../../../store/rooms/rooms.selectors';
import { selectMessagesLoading } from '../../../store/messages/messages.selectors';
import { UsersActions } from '../../../store/users/users.actions';
import { RoomsActions } from '../../../store/rooms/rooms.actions';
import { MessagesActions } from '../../../store/messages/messages.actions';
@@ -68,7 +73,9 @@ import {
UserAvatarComponent,
ConfirmDialogComponent,
UserVolumeMenuComponent,
ProfileCardService
ProfileCardService,
SkeletonComponent,
SkeletonListComponent
} from '../../../shared';
import {
Channel,
@@ -79,9 +86,12 @@ import {
User
} from '../../../shared-kernel';
import { v4 as uuidv4 } from 'uuid';
import { visibilityAwareInterval$ } from '../../../shared/rxjs';
type PanelMode = 'channels' | 'users';
const SKELETON_REVEAL_DELAY_MS = 180;
@Component({
selector: 'app-rooms-side-panel',
standalone: true,
@@ -95,7 +105,9 @@ type PanelMode = 'channels' | 'users';
UserAvatarComponent,
ConfirmDialogComponent,
PluginRenderHostComponent,
ThemeNodeDirective
ThemeNodeDirective,
SkeletonComponent,
SkeletonListComponent
],
viewProviders: [
provideIcons({
@@ -135,7 +147,8 @@ export class RoomsSidePanelComponent implements OnDestroy {
private readonly voiceConnectivity = inject(VoiceConnectivityHealthService);
private readonly pluginUi = inject(PluginUiRegistryService);
private profileCardOpenTimer: ReturnType<typeof setTimeout> | null = null;
private readonly activityTimer = setInterval(() => this.activityNow.set(Date.now()), 1_000);
private skeletonRevealTimer: ReturnType<typeof setTimeout> | null = null;
private readonly destroyRef = inject(DestroyRef);
readonly panelMode = input<PanelMode>('channels');
readonly showVoiceControls = input(true);
@@ -145,12 +158,19 @@ export class RoomsSidePanelComponent implements OnDestroy {
onlineUsers = this.store.selectSignal(selectOnlineUsers);
currentUser = this.store.selectSignal(selectCurrentUser);
currentRoom = this.store.selectSignal(selectCurrentRoom);
isConnecting = this.store.selectSignal(selectIsConnecting);
messagesLoading = this.store.selectSignal(selectMessagesLoading);
activeChannelId = this.store.selectSignal(selectActiveChannelId);
textChannels = this.store.selectSignal(selectTextChannels);
voiceChannels = this.store.selectSignal(selectVoiceChannels);
pluginChannelSections = this.pluginUi.channelSectionRecords;
pluginMenuActions = this.pluginUi.toolbarActionRecords;
pluginSidePanels = this.pluginUi.sidePanelRecords;
panelHydrating = computed(() => this.panelMode() === 'channels' && (this.isConnecting() || this.messagesLoading()));
delayedPanelHydrating = signal(false);
showTextChannelSkeleton = computed(() => this.delayedPanelHydrating() && this.textChannels().length === 0);
showVoiceChannelSkeleton = computed(() => this.delayedPanelHydrating() && this.voiceEnabled() && this.voiceChannels().length === 0);
showPluginSkeleton = computed(() => this.delayedPanelHydrating() && !this.hasPluginPanelContent());
localUserHasDesync = this.voiceConnectivity.localUserHasDesync;
roomMembers = computed(() => this.currentRoom()?.members ?? []);
roomMemberIdentifiers = computed(() => {
@@ -196,6 +216,10 @@ export class RoomsSidePanelComponent implements OnDestroy {
return memberIds.size;
});
private hasPluginPanelContent(): boolean {
return this.pluginChannelSections().length > 0 || this.pluginMenuActions().length > 0 || this.pluginSidePanels().length > 0;
}
showChannelMenu = signal(false);
channelMenuX = signal(0);
channelMenuY = signal(0);
@@ -222,12 +246,47 @@ export class RoomsSidePanelComponent implements OnDestroy {
dragTargetVoiceChannelId = signal<string | null>(null);
activityNow = signal(Date.now());
constructor() {
effect(() => {
if (!this.panelHydrating()) {
this.clearSkeletonRevealTimer();
this.delayedPanelHydrating.set(false);
return;
}
if (this.delayedPanelHydrating() || this.skeletonRevealTimer) {
return;
}
this.skeletonRevealTimer = setTimeout(() => {
this.skeletonRevealTimer = null;
if (this.panelHydrating()) {
this.delayedPanelHydrating.set(true);
}
}, SKELETON_REVEAL_DELAY_MS);
});
visibilityAwareInterval$(1_000)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.activityNow.set(Date.now()));
}
ngOnDestroy(): void {
clearInterval(this.activityTimer);
this.clearSkeletonRevealTimer();
this.cancelQueuedProfileCardOpen();
this.pluginActionMenu.close();
}
private clearSkeletonRevealTimer(): void {
if (!this.skeletonRevealTimer) {
return;
}
clearTimeout(this.skeletonRevealTimer);
this.skeletonRevealTimer = null;
}
gameActivityElapsed(user: User | null | undefined): string {
const activity = user?.gameActivity;

View File

@@ -45,6 +45,8 @@ import {
LeaveServerDialogComponent
} from '../../../shared';
const ACTIVATION_DEBOUNCE_MS = 150;
@Component({
selector: 'app-servers-rail',
standalone: true,
@@ -72,6 +74,11 @@ export class ServersRailComponent {
private serverDirectory = inject(ServerDirectoryFacade);
private destroyRef = inject(DestroyRef);
private banLookupRequestVersion = 0;
private bannedLookupUserKey: string | null = null;
private activationRequestVersion = 0;
private activationTimer: ReturnType<typeof window.setTimeout> | null = null;
private joinRequestVersion = 0;
private joinRequestTimer: ReturnType<typeof window.setTimeout> | null = null;
private visibleSavedRoomCache: Room[] = [];
private savedRoomJoinRequests = new Subject<{ room: Room; password?: string }>();
savedRooms = this.store.selectSignal(selectSavedRooms);
@@ -208,6 +215,16 @@ export class ServersRailComponent {
takeUntilDestroyed(this.destroyRef)
)
.subscribe();
this.destroyRef.onDestroy(() => {
if (this.activationTimer) {
window.clearTimeout(this.activationTimer);
}
if (this.joinRequestTimer) {
window.clearTimeout(this.joinRequestTimer);
}
});
}
initial(name?: string): string {
@@ -249,8 +266,8 @@ export class ServersRailComponent {
}
this.optimisticSelectedRoomId.set(targetRoom.id);
this.activateSavedRoom(targetRoom);
this.savedRoomJoinRequests.next({ room: targetRoom });
this.queueSavedRoomActivation(targetRoom);
this.queueSavedRoomJoin(targetRoom);
}
openCall(callId: string): void {
@@ -436,13 +453,36 @@ export class ServersRailComponent {
const requestVersion = ++this.banLookupRequestVersion;
if (!currentUser || rooms.length === 0) {
this.bannedLookupUserKey = null;
this.bannedRoomLookup.set({});
return;
}
const persistedUserId = localStorage.getItem('metoyou_currentUserId');
const userKey = `${currentUser.id}:${currentUser.oderId}:${persistedUserId ?? ''}`;
const roomIds = new Set(rooms.map((room) => room.id));
if (this.bannedLookupUserKey !== userKey) {
this.bannedLookupUserKey = userKey;
this.bannedRoomLookup.set({});
}
const currentLookup = this.bannedRoomLookup();
const nextLookup = Object.fromEntries(
Object.entries(currentLookup).filter(([roomId]) => roomIds.has(roomId))
);
const roomsToLookup = rooms.filter((room) => nextLookup[room.id] === undefined);
if (roomsToLookup.length === 0) {
if (Object.keys(nextLookup).length !== Object.keys(currentLookup).length) {
this.bannedRoomLookup.set(nextLookup);
}
return;
}
const entries = await Promise.all(
rooms.map(async (room) => {
roomsToLookup.map(async (room) => {
const bans = await this.db.getBansForRoom(room.id);
return [room.id, hasRoomBanForUser(bans, currentUser, persistedUserId)] as const;
@@ -453,7 +493,10 @@ export class ServersRailComponent {
return;
}
this.bannedRoomLookup.set(Object.fromEntries(entries));
this.bannedRoomLookup.set({
...nextLookup,
...Object.fromEntries(entries)
});
}
private prepareVoiceContext(room: Room): void {
@@ -473,6 +516,40 @@ export class ServersRailComponent {
this.store.dispatch(RoomsActions.viewServer({ room, skipBanCheck: true }));
}
private queueSavedRoomActivation(room: Room): void {
const requestVersion = ++this.activationRequestVersion;
if (this.activationTimer) {
window.clearTimeout(this.activationTimer);
}
this.activationTimer = window.setTimeout(() => {
if (requestVersion !== this.activationRequestVersion) {
return;
}
this.activationTimer = null;
this.activateSavedRoom(room);
}, ACTIVATION_DEBOUNCE_MS);
}
private queueSavedRoomJoin(room: Room): void {
const requestVersion = ++this.joinRequestVersion;
if (this.joinRequestTimer) {
window.clearTimeout(this.joinRequestTimer);
}
this.joinRequestTimer = window.setTimeout(() => {
if (requestVersion !== this.joinRequestVersion) {
return;
}
this.joinRequestTimer = null;
this.savedRoomJoinRequests.next({ room });
}, ACTIVATION_DEBOUNCE_MS);
}
private requestJoinInBackground(room: Room, password?: string) {
const currentUserId = localStorage.getItem('metoyou_currentUserId');
const currentUser = this.currentUser();