wip: optimizations
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user