fix: Mobile style fixes and other small ui fixes
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<div class="flex w-full flex-wrap items-center justify-center gap-3 rounded-2xl bg-background/75 px-4 py-3 backdrop-blur">
|
||||
<div class="flex w-full flex-wrap items-center justify-center gap-3 px-3 py-3 sm:px-4">
|
||||
@if (!connected()) {
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<article
|
||||
class="flex aspect-square min-w-0 flex-col items-center justify-center overflow-hidden rounded-2xl border border-border/80 bg-card/80 text-center shadow-sm backdrop-blur"
|
||||
[class.w-[11rem]]="compact()"
|
||||
[class.shrink-0]="compact()"
|
||||
[class.p-4]="compact()"
|
||||
[class.sm:w-[12.5rem]]="compact()"
|
||||
[class.w-full]="!compact()"
|
||||
[class.p-[clamp(1rem,4vw,1.5rem)]]="!compact()"
|
||||
class="flex min-w-0 flex-col items-center justify-center overflow-hidden rounded-xl text-center"
|
||||
[ngClass]="compact() ? 'min-h-[9.5rem] w-[12rem] shrink-0 p-3 sm:w-[14rem] sm:p-4' : 'min-h-[14rem] w-full p-3 sm:min-h-[17rem] sm:p-[clamp(1.25rem,4vw,2rem)]'"
|
||||
>
|
||||
<div
|
||||
class="relative h-[var(--participant-avatar-size)] w-[var(--participant-avatar-size)] rounded-full ring-2 transition-all duration-150 sm:h-[var(--participant-avatar-size-sm)] sm:w-[var(--participant-avatar-size-sm)]"
|
||||
@@ -67,16 +62,9 @@
|
||||
@if (connected()) {
|
||||
<span
|
||||
class="absolute rounded-full border-card"
|
||||
[class.bottom-3]="compact()"
|
||||
[class.right-3]="compact()"
|
||||
[class.h-4]="compact()"
|
||||
[class.w-4]="compact()"
|
||||
[class.border-[3px]]="compact()"
|
||||
[class.bottom-5]="!compact()"
|
||||
[class.right-5]="!compact()"
|
||||
[class.h-5]="!compact()"
|
||||
[class.w-5]="!compact()"
|
||||
[class.border-4]="!compact()"
|
||||
[ngClass]="
|
||||
compact() ? 'bottom-1 right-1 h-4 w-4 border-[3px] sm:bottom-3 sm:right-3' : 'bottom-1 right-1 h-5 w-5 border-4 sm:bottom-5 sm:right-5'
|
||||
"
|
||||
[class.bg-emerald-400]="speaking()"
|
||||
[class.bg-muted-foreground]="!speaking()"
|
||||
></span>
|
||||
|
||||
@@ -20,11 +20,11 @@ export class PrivateCallParticipantCardComponent {
|
||||
readonly compact = input(false);
|
||||
|
||||
avatarSize(): string {
|
||||
return this.compact() ? '5rem' : 'clamp(4.25rem, 22vw, 10rem)';
|
||||
return this.compact() ? '5.75rem' : 'clamp(6.5rem, 38vw, 13rem)';
|
||||
}
|
||||
|
||||
avatarSizeSm(): string {
|
||||
return this.compact() ? '6rem' : this.avatarSize();
|
||||
return this.compact() ? '6rem' : 'clamp(4.25rem, 22vw, 10rem)';
|
||||
}
|
||||
|
||||
participantInitial(): string {
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
@if (isMobile()) {
|
||||
<swiper-container
|
||||
class="block h-full min-h-0 w-full"
|
||||
direction="vertical"
|
||||
slides-per-view="1"
|
||||
space-between="0"
|
||||
initial-slide="1"
|
||||
threshold="10"
|
||||
resistance-ratio="0"
|
||||
(swiperslidechange)="onMobileCallSlideChange($event)"
|
||||
>
|
||||
<swiper-slide class="block h-full w-full" />
|
||||
<swiper-slide class="block h-full w-full">
|
||||
<ng-container *ngTemplateOutlet="privateCallSurface" />
|
||||
</swiper-slide>
|
||||
</swiper-container>
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="privateCallSurface" />
|
||||
}
|
||||
|
||||
<ng-template #privateCallSurface>
|
||||
<section
|
||||
class="grid h-full min-h-0 bg-background lg:grid-cols-[minmax(0,1fr)_var(--private-call-chat-width)]"
|
||||
[style.--private-call-chat-width]="chatWidthPx() + 'px'"
|
||||
>
|
||||
<main class="flex min-h-0 min-w-0 flex-col overflow-hidden bg-[radial-gradient(circle_at_top,rgba(16,185,129,0.10),transparent_34rem)]">
|
||||
<header class="flex min-h-16 shrink-0 items-center justify-between gap-3 border-b border-border/70 bg-background/80 px-5 backdrop-blur">
|
||||
<header class="flex min-h-16 shrink-0 items-center justify-between gap-3 border-b border-border/70 bg-background/80 px-3 backdrop-blur sm:px-5">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div class="grid h-10 w-10 shrink-0 place-items-center rounded-2xl bg-emerald-500/10 text-emerald-500">
|
||||
<ng-icon
|
||||
@@ -26,8 +47,22 @@
|
||||
|
||||
@if (session()) {
|
||||
<div class="flex items-center gap-2">
|
||||
@if (isMobile()) {
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-10 w-10 place-items-center rounded-full bg-secondary text-foreground transition-colors hover:bg-secondary/80"
|
||||
(click)="minimizeCall()"
|
||||
aria-label="Minimize call"
|
||||
title="Minimize call"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
<select
|
||||
class="h-9 max-w-44 rounded-md border border-border bg-secondary px-2 text-sm text-foreground"
|
||||
class="hidden h-9 max-w-44 rounded-md border border-border bg-secondary px-2 text-sm text-foreground sm:block"
|
||||
[ngModel]="inviteUserId()"
|
||||
(ngModelChange)="inviteUserId.set($event)"
|
||||
aria-label="Add user to call"
|
||||
@@ -39,7 +74,7 @@
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-9 w-9 place-items-center rounded-md bg-secondary text-foreground transition-colors hover:bg-secondary/80 disabled:opacity-50"
|
||||
class="hidden h-9 w-9 place-items-center rounded-md bg-secondary text-foreground transition-colors hover:bg-secondary/80 disabled:opacity-50 sm:grid"
|
||||
[disabled]="!inviteUserId()"
|
||||
(click)="inviteSelectedUser()"
|
||||
aria-label="Add user"
|
||||
@@ -55,8 +90,8 @@
|
||||
</header>
|
||||
|
||||
@if (session()) {
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-hidden px-4 py-4 sm:px-5">
|
||||
<div class="relative min-h-0 flex-1 overflow-hidden rounded-2xl border border-border/80 bg-card/45 shadow-sm">
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-hidden px-3 py-3 sm:px-5 sm:py-4">
|
||||
<div class="relative min-h-0 flex-1 overflow-hidden">
|
||||
@if (activeShares().length > 0) {
|
||||
@if (focusedShare()) {
|
||||
@if (hasMultipleShares()) {
|
||||
@@ -103,17 +138,18 @@
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="flex h-full min-h-0 items-center justify-center p-4 sm:p-6">
|
||||
<div class="flex h-full min-h-0 items-center justify-center p-1 sm:p-5">
|
||||
<div
|
||||
class="grid w-full max-w-5xl grid-cols-[repeat(auto-fit,minmax(min(10rem,100%),1fr))] items-stretch justify-center gap-3 sm:grid-cols-[repeat(auto-fit,minmax(min(13rem,100%),1fr))] sm:gap-5 lg:gap-7"
|
||||
class="grid w-full max-w-7xl grid-cols-[repeat(auto-fit,minmax(min(11rem,100%),1fr))] items-stretch justify-center gap-3 sm:grid-cols-[repeat(auto-fit,minmax(min(16rem,100%),1fr))] sm:gap-5 lg:gap-7"
|
||||
>
|
||||
<app-private-call-participant-card
|
||||
*ngFor="let user of participantUsers(); trackBy: trackUserKey"
|
||||
[user]="user"
|
||||
[connected]="isParticipantConnected(user)"
|
||||
[speaking]="isSpeaking(user)"
|
||||
[issueLabel]="participantIssueLabel(user)"
|
||||
></app-private-call-participant-card>
|
||||
@for (user of participantUsers(); track trackUserKey($index, user)) {
|
||||
<app-private-call-participant-card
|
||||
[user]="user"
|
||||
[connected]="isParticipantConnected(user)"
|
||||
[speaking]="isSpeaking(user)"
|
||||
[issueLabel]="participantIssueLabel(user)"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -122,14 +158,15 @@
|
||||
@if (activeShares().length > 0) {
|
||||
<div class="shrink-0 pt-4">
|
||||
<div class="flex w-full items-stretch gap-3 overflow-x-auto pb-1">
|
||||
<app-private-call-participant-card
|
||||
*ngFor="let user of participantUsers(); trackBy: trackUserKey"
|
||||
[user]="user"
|
||||
[connected]="isParticipantConnected(user)"
|
||||
[speaking]="isSpeaking(user)"
|
||||
[issueLabel]="participantIssueLabel(user)"
|
||||
[compact]="true"
|
||||
></app-private-call-participant-card>
|
||||
@for (user of participantUsers(); track trackUserKey($index, user)) {
|
||||
<app-private-call-participant-card
|
||||
[user]="user"
|
||||
[connected]="isParticipantConnected(user)"
|
||||
[speaking]="isSpeaking(user)"
|
||||
[issueLabel]="participantIssueLabel(user)"
|
||||
[compact]="true"
|
||||
/>
|
||||
}
|
||||
|
||||
@if (hasMultipleShares()) {
|
||||
@for (share of focusedShare() ? thumbnailShares() : activeShares(); track share.id) {
|
||||
@@ -166,7 +203,7 @@
|
||||
(cameraToggled)="toggleCamera()"
|
||||
(screenShareToggled)="toggleScreenShare()"
|
||||
(leaveRequested)="leave()"
|
||||
></app-private-call-controls>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
@@ -191,6 +228,7 @@
|
||||
/>
|
||||
</aside>
|
||||
</section>
|
||||
</ng-template>
|
||||
|
||||
@if (showScreenShareQualityDialog()) {
|
||||
<app-screen-share-quality-dialog
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering */
|
||||
import {
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
Component,
|
||||
DestroyRef,
|
||||
HostListener,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
untracked
|
||||
} from '@angular/core';
|
||||
@@ -17,6 +19,7 @@ import { Store } from '@ngrx/store';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
lucidePhone,
|
||||
lucideX,
|
||||
lucideUsers,
|
||||
lucideUserPlus
|
||||
} from '@ng-icons/lucide';
|
||||
@@ -39,6 +42,7 @@ import {
|
||||
} from '../../domains/screen-share';
|
||||
import { loadVoiceSettingsFromStorage, saveVoiceSettingsToStorage } from '../../domains/voice-session';
|
||||
import { ScreenShareQualityDialogComponent } from '../../shared';
|
||||
import { ViewportService } from '../../core/platform';
|
||||
import { selectAllUsers, selectCurrentUser } from '../../store/users/users.selectors';
|
||||
import { UsersActions } from '../../store/users/users.actions';
|
||||
import { User } from '../../shared-kernel';
|
||||
@@ -60,9 +64,12 @@ import { PrivateCallParticipantCardComponent } from './private-call-participant-
|
||||
ScreenShareQualityDialogComponent,
|
||||
VoiceWorkspaceStreamTileComponent
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
host: { class: 'block h-full w-full' },
|
||||
viewProviders: [
|
||||
provideIcons({
|
||||
lucidePhone,
|
||||
lucideX,
|
||||
lucideUsers,
|
||||
lucideUserPlus
|
||||
})
|
||||
@@ -79,13 +86,18 @@ export class PrivateCallComponent {
|
||||
private readonly voiceActivity = inject(VoiceActivityService);
|
||||
private readonly playback = inject(VoicePlaybackService);
|
||||
private readonly screenShare = inject(ScreenShareFacade);
|
||||
private readonly viewport = inject(ViewportService);
|
||||
private chatResizing = false;
|
||||
|
||||
readonly allUsers = this.store.selectSignal(selectAllUsers);
|
||||
readonly currentUser = this.store.selectSignal(selectCurrentUser);
|
||||
readonly callId = toSignal(this.route.paramMap.pipe(map((params) => params.get('callId'))), {
|
||||
readonly isMobile = this.viewport.isMobile;
|
||||
readonly callIdInput = input<string | null>(null);
|
||||
readonly overlayMode = input(false);
|
||||
readonly routeCallId = toSignal(this.route.paramMap.pipe(map((params) => params.get('callId'))), {
|
||||
initialValue: this.route.snapshot.paramMap.get('callId')
|
||||
});
|
||||
readonly callId = computed(() => this.callIdInput() ?? this.routeCallId());
|
||||
readonly session = computed(() => this.calls.sessionById(this.callId()));
|
||||
readonly participantUsers = computed(() => {
|
||||
const session = this.session();
|
||||
@@ -146,13 +158,11 @@ export class PrivateCallComponent {
|
||||
}
|
||||
|
||||
for (const user of this.participantUsers()) {
|
||||
const peerKey = this.getPeerKeyCandidates(user).find(
|
||||
(candidate) => candidate !== localPeerKey
|
||||
&& (
|
||||
!!this.screenShare.getRemoteScreenShareStream(candidate)
|
||||
|| !!this.voice.getRemoteCameraStream(candidate)
|
||||
)
|
||||
) ?? this.userKey(user);
|
||||
const peerKey =
|
||||
this.getPeerKeyCandidates(user).find(
|
||||
(candidate) =>
|
||||
candidate !== localPeerKey && (!!this.screenShare.getRemoteScreenShareStream(candidate) || !!this.voice.getRemoteCameraStream(candidate))
|
||||
) ?? this.userKey(user);
|
||||
|
||||
if (peerKey === localPeerKey) {
|
||||
continue;
|
||||
@@ -192,9 +202,7 @@ export class PrivateCallComponent {
|
||||
|
||||
return null;
|
||||
});
|
||||
readonly focusedShare = computed(
|
||||
() => this.activeShares().find((share) => share.id === this.focusedShareId()) ?? null
|
||||
);
|
||||
readonly focusedShare = computed(() => this.activeShares().find((share) => share.id === this.focusedShareId()) ?? null);
|
||||
readonly thumbnailShares = computed(() => {
|
||||
const focusedShareId = this.focusedShareId();
|
||||
|
||||
@@ -217,14 +225,31 @@ export class PrivateCallComponent {
|
||||
const session = this.session();
|
||||
|
||||
if (session && !this.calls.hasOngoingActivity(session)) {
|
||||
if (this.overlayMode()) {
|
||||
untracked(() => this.calls.closeMobileCallOverlay());
|
||||
return;
|
||||
}
|
||||
|
||||
untracked(() => void this.router.navigate(['/dm', session.conversationId], { replaceUrl: true }));
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
const callId = this.callId();
|
||||
const session = this.session();
|
||||
|
||||
if (callId && session?.conversationId && this.isMobile() && !this.overlayMode()) {
|
||||
untracked(() => {
|
||||
void this.calls.openMobileCallOverlay(callId);
|
||||
void this.router.navigate(['/pm', session.conversationId], { replaceUrl: true });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
const session = this.session();
|
||||
const currentUserId = this.currentUserKey();
|
||||
const peerIds = (session ? this.remoteParticipantPeerIds(session, currentUserId) : []);
|
||||
const peerIds = session ? this.remoteParticipantPeerIds(session, currentUserId) : [];
|
||||
|
||||
this.screenShare.syncRemoteScreenShareRequests(peerIds, this.isConnected() && !!session && session.status === 'connected');
|
||||
});
|
||||
@@ -240,13 +265,9 @@ export class PrivateCallComponent {
|
||||
this.untrackLocalMic();
|
||||
});
|
||||
|
||||
this.screenShare.onRemoteStream
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => this.bumpRemoteStreamRevision());
|
||||
this.screenShare.onRemoteStream.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.bumpRemoteStreamRevision());
|
||||
|
||||
this.screenShare.onPeerDisconnected
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => this.bumpRemoteStreamRevision());
|
||||
this.screenShare.onPeerDisconnected.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.bumpRemoteStreamRevision());
|
||||
|
||||
this.destroyRef.onDestroy(() => {
|
||||
this.screenShare.syncRemoteScreenShareRequests([], false);
|
||||
@@ -284,8 +305,36 @@ export class PrivateCallComponent {
|
||||
}
|
||||
|
||||
this.calls.leaveCall(session.callId);
|
||||
this.calls.closeMobileCallOverlay();
|
||||
this.untrackLocalMic();
|
||||
void this.router.navigate(['/dm', session.conversationId]);
|
||||
|
||||
if (!this.overlayMode()) {
|
||||
void this.router.navigate(['/pm', session.conversationId]);
|
||||
}
|
||||
}
|
||||
|
||||
minimizeCall(): void {
|
||||
const session = this.session();
|
||||
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.overlayMode()) {
|
||||
this.calls.closeMobileCallOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
void this.router.navigate(['/pm', session.conversationId]);
|
||||
}
|
||||
|
||||
onMobileCallSlideChange(event: Event): void {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
const swiper = Array.isArray(detail) ? detail[0] : detail;
|
||||
|
||||
if (this.isMobile() && swiper?.activeIndex === 0) {
|
||||
this.minimizeCall();
|
||||
}
|
||||
}
|
||||
|
||||
toggleMute(): void {
|
||||
@@ -378,12 +427,10 @@ export class PrivateCallComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!session.participants[userId]?.joined
|
||||
|| !!(
|
||||
user.voiceState?.isConnected
|
||||
&& user.voiceState.roomId === session.callId
|
||||
&& user.voiceState.serverId === session.callId
|
||||
);
|
||||
return (
|
||||
!!session.participants[userId]?.joined ||
|
||||
!!(user.voiceState?.isConnected && user.voiceState.roomId === session.callId && user.voiceState.serverId === session.callId)
|
||||
);
|
||||
}
|
||||
|
||||
participantIssueLabel(user: User): string | null {
|
||||
@@ -437,16 +484,18 @@ export class PrivateCallComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
this.store.dispatch(UsersActions.updateVoiceState({
|
||||
userId: user.id,
|
||||
voiceState: {
|
||||
isConnected: this.isConnected(),
|
||||
isMuted: this.isMuted(),
|
||||
isDeafened: this.isDeafened(),
|
||||
roomId: session.callId,
|
||||
serverId: session.callId
|
||||
}
|
||||
}));
|
||||
this.store.dispatch(
|
||||
UsersActions.updateVoiceState({
|
||||
userId: user.id,
|
||||
voiceState: {
|
||||
isConnected: this.isConnected(),
|
||||
isMuted: this.isMuted(),
|
||||
isDeafened: this.isDeafened(),
|
||||
roomId: session.callId,
|
||||
serverId: session.callId
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private remoteParticipantPeerIds(session: DirectCallSession, currentUserId: string): string[] {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<div class="flex min-h-0 flex-1 overflow-hidden border-l border-border bg-card">
|
||||
<app-rooms-side-panel
|
||||
panelMode="channels"
|
||||
(textChannelSelected)="setMobilePage('main')"
|
||||
class="block h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -52,6 +53,20 @@
|
||||
<p class="truncate text-sm font-semibold text-foreground">{{ currentRoom()?.name }}</p>
|
||||
}
|
||||
</div>
|
||||
@if (activeCall()) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="openActiveCall()"
|
||||
class="grid h-11 w-11 place-items-center rounded-lg text-emerald-600 transition-colors hover:bg-emerald-500/10 hover:text-emerald-500"
|
||||
aria-label="Return to call"
|
||||
title="Return to call"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucidePhoneCall"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
(click)="setMobilePage('members')"
|
||||
@@ -208,4 +223,3 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
lucideUsers,
|
||||
lucideMenu,
|
||||
lucideX,
|
||||
lucideChevronLeft
|
||||
lucideChevronLeft,
|
||||
lucidePhoneCall
|
||||
} from '@ng-icons/lucide';
|
||||
|
||||
import { ChatMessagesComponent } from '../../../domains/chat/feature/chat-messages/chat-messages.component';
|
||||
@@ -38,6 +39,7 @@ import { ViewportService } from '../../../core/platform';
|
||||
import { selectIsCurrentUserAdmin } from '../../../store/users/users.selectors';
|
||||
import { VoiceWorkspaceService } from '../../../domains/voice-session';
|
||||
import { ThemeNodeDirective, ThemeService } from '../../../domains/theme';
|
||||
import { DirectCallService } from '../../../domains/direct-call';
|
||||
|
||||
/** Mobile-only page identifier within the chat-room view. */
|
||||
export type ChatRoomMobilePage = 'channels' | 'main' | 'members';
|
||||
@@ -77,7 +79,8 @@ interface SwiperElement extends HTMLElement {
|
||||
lucideUsers,
|
||||
lucideMenu,
|
||||
lucideX,
|
||||
lucideChevronLeft
|
||||
lucideChevronLeft,
|
||||
lucidePhoneCall
|
||||
})
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
@@ -96,6 +99,7 @@ export class ChatRoomComponent {
|
||||
private readonly settingsModal = inject(SettingsModalService);
|
||||
private readonly theme = inject(ThemeService);
|
||||
private readonly viewport = inject(ViewportService);
|
||||
private readonly directCalls = inject(DirectCallService);
|
||||
private readonly zone = inject(NgZone);
|
||||
private voiceWorkspace = inject(VoiceWorkspaceService);
|
||||
private lastSeenChannelId: string | null = null;
|
||||
@@ -128,6 +132,12 @@ export class ChatRoomComponent {
|
||||
});
|
||||
isVoiceWorkspaceExpanded = this.voiceWorkspace.isExpanded;
|
||||
hasTextChannels = computed(() => this.textChannels().length > 0);
|
||||
activeCall = computed(() => {
|
||||
const currentSession = this.directCalls.currentSession();
|
||||
const visibleSessions = this.directCalls.visibleActiveSessions();
|
||||
|
||||
return visibleSessions.find((session) => session.callId === currentSession?.callId) ?? visibleSessions[0] ?? null;
|
||||
});
|
||||
roomLayoutStyles = computed(() => this.theme.getLayoutContainerStyles('roomLayout'));
|
||||
channelsPanelLayoutStyles = computed(() => this.theme.getLayoutItemStyles('chatRoomChannelsPanel'));
|
||||
mainPanelLayoutStyles = computed(() => this.theme.getLayoutItemStyles('chatRoomMainPanel'));
|
||||
@@ -209,6 +219,14 @@ export class ChatRoomComponent {
|
||||
this.mobilePage.set(page);
|
||||
}
|
||||
|
||||
openActiveCall(): void {
|
||||
const call = this.activeCall();
|
||||
|
||||
if (call) {
|
||||
void this.directCalls.openCallView(call.callId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Open the settings modal to the Server admin page for the current room. */
|
||||
toggleAdminPanel() {
|
||||
const room = this.currentRoom();
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
computed,
|
||||
input,
|
||||
OnDestroy,
|
||||
output,
|
||||
signal
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
@@ -138,6 +139,7 @@ export class RoomsSidePanelComponent implements OnDestroy {
|
||||
|
||||
readonly panelMode = input<PanelMode>('channels');
|
||||
readonly showVoiceControls = input(true);
|
||||
readonly textChannelSelected = output<string>();
|
||||
showFloatingControls = this.voiceSessionService.showFloatingControls;
|
||||
isVoiceWorkspaceExpanded = this.voiceWorkspace.isExpanded;
|
||||
onlineUsers = this.store.selectSignal(selectOnlineUsers);
|
||||
@@ -379,6 +381,7 @@ export class RoomsSidePanelComponent implements OnDestroy {
|
||||
|
||||
this.voiceWorkspace.showChat();
|
||||
this.store.dispatch(RoomsActions.selectChannel({ channelId }));
|
||||
this.textChannelSelected.emit(channelId);
|
||||
}
|
||||
|
||||
openChannelContextMenu(evt: MouseEvent, channel: Channel) {
|
||||
|
||||
@@ -63,15 +63,45 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!item().isLocal && item().hasAudio) {
|
||||
@if (canControlStreamAudio()) {
|
||||
<div class="flex min-w-32 items-center gap-2 rounded-full border border-white/10 bg-black/35 px-2.5 py-1.5 text-white/75">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-white/75 transition hover:bg-white/10 hover:text-white"
|
||||
[title]="muted() ? 'Unmute stream audio' : 'Mute stream audio'"
|
||||
(click)="toggleMuted(); $event.stopPropagation()"
|
||||
>
|
||||
<ng-icon
|
||||
[name]="muted() ? 'lucideVolumeX' : 'lucideVolume2'"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
[value]="volume()"
|
||||
class="w-20 accent-primary sm:w-28"
|
||||
aria-label="Stream volume"
|
||||
(click)="$event.stopPropagation()"
|
||||
(input)="updateVolume($event)"
|
||||
/>
|
||||
|
||||
<span class="w-9 text-right text-xs tabular-nums">{{ muted() ? 'Off' : volume() + '%' }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isMobile() && item().kind === 'screen') {
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-white/10 bg-black/45 text-white/75 transition hover:bg-black/60 hover:text-white"
|
||||
[title]="muted() ? 'Unmute stream audio' : 'Mute stream audio'"
|
||||
(click)="toggleMuted(); $event.stopPropagation()"
|
||||
title="Rotate to landscape"
|
||||
aria-label="Rotate to landscape"
|
||||
(click)="enterLandscapeFullscreen($event)"
|
||||
>
|
||||
<ng-icon
|
||||
[name]="muted() ? 'lucideVolumeX' : 'lucideVolume2'"
|
||||
name="lucideRotateCw"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
@@ -92,6 +122,72 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (immersive() && item().kind === 'screen' && !isFullscreen()) {
|
||||
<div class="absolute inset-x-3 bottom-3 z-20 sm:inset-x-5 sm:bottom-5">
|
||||
<div class="mx-auto flex w-full max-w-3xl flex-wrap items-center justify-center gap-2 rounded-2xl border border-white/10 bg-black/55 px-3 py-3 text-white/80 shadow-2xl backdrop-blur-lg sm:gap-3 sm:px-4">
|
||||
@if (canControlStreamAudio()) {
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 rounded-full bg-white/10 px-2.5 py-2 sm:max-w-md">
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-9 w-9 shrink-0 place-items-center rounded-full text-white/85 transition hover:bg-white/10 hover:text-white"
|
||||
[title]="muted() ? 'Unmute stream audio' : 'Mute stream audio'"
|
||||
[attr.aria-label]="muted() ? 'Unmute stream audio' : 'Mute stream audio'"
|
||||
(click)="toggleMuted(); $event.stopPropagation()"
|
||||
>
|
||||
<ng-icon
|
||||
[name]="muted() ? 'lucideVolumeX' : 'lucideVolume2'"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
[value]="volume()"
|
||||
class="min-w-0 flex-1 accent-primary"
|
||||
aria-label="Screen share volume"
|
||||
(click)="$event.stopPropagation()"
|
||||
(input)="updateVolume($event)"
|
||||
/>
|
||||
|
||||
<span class="w-10 text-right text-xs font-semibold tabular-nums text-white/70">{{ muted() ? 'Off' : volume() + '%' }}</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="min-w-0 flex-1 px-2 text-center text-xs font-medium text-white/65 sm:text-left">No screen audio</div>
|
||||
}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-11 w-11 place-items-center rounded-full bg-white/10 text-white transition hover:bg-white/15"
|
||||
[title]="isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'"
|
||||
[attr.aria-label]="isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'"
|
||||
(click)="toggleFullscreen($event)"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideMaximize"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@if (isMobile()) {
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-11 w-11 place-items-center rounded-full bg-white/10 text-white transition hover:bg-white/15"
|
||||
title="Rotate to landscape"
|
||||
aria-label="Rotate to landscape"
|
||||
(click)="enterLandscapeFullscreen($event)"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideRotateCw"
|
||||
class="h-5 w-5"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (mini()) {
|
||||
<div class="absolute inset-x-0 bottom-0 p-2">
|
||||
<div class="rounded-xl border border-white/10 bg-black/55 px-2.5 py-2 backdrop-blur-md">
|
||||
|
||||
@@ -17,12 +17,14 @@ import {
|
||||
lucideMaximize,
|
||||
lucideMinimize,
|
||||
lucideMonitor,
|
||||
lucideRotateCw,
|
||||
lucideVideo,
|
||||
lucideVolume2,
|
||||
lucideVolumeX
|
||||
} from '@ng-icons/lucide';
|
||||
|
||||
import { UserAvatarComponent } from '../../../../shared';
|
||||
import { ViewportService } from '../../../../core/platform';
|
||||
import { VoiceWorkspacePlaybackService } from '../voice-workspace-playback.service';
|
||||
import { VoiceWorkspaceStreamItem } from '../voice-workspace.models';
|
||||
|
||||
@@ -39,6 +41,7 @@ import { VoiceWorkspaceStreamItem } from '../voice-workspace.models';
|
||||
lucideMaximize,
|
||||
lucideMinimize,
|
||||
lucideMonitor,
|
||||
lucideRotateCw,
|
||||
lucideVideo,
|
||||
lucideVolume2,
|
||||
lucideVolumeX
|
||||
@@ -51,6 +54,7 @@ import { VoiceWorkspaceStreamItem } from '../voice-workspace.models';
|
||||
})
|
||||
export class VoiceWorkspaceStreamTileComponent implements OnDestroy {
|
||||
private readonly workspacePlayback = inject(VoiceWorkspacePlaybackService);
|
||||
private readonly viewport = inject(ViewportService);
|
||||
private fullscreenHeaderHideTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
readonly item = input.required<VoiceWorkspaceStreamItem>();
|
||||
@@ -64,6 +68,7 @@ export class VoiceWorkspaceStreamTileComponent implements OnDestroy {
|
||||
readonly videoRef = viewChild<ElementRef<HTMLVideoElement>>('streamVideo');
|
||||
|
||||
readonly isFullscreen = signal(false);
|
||||
readonly isMobile = this.viewport.isMobile;
|
||||
readonly showFullscreenHeader = signal(true);
|
||||
readonly volume = signal(100);
|
||||
readonly muted = signal(false);
|
||||
@@ -138,6 +143,7 @@ export class VoiceWorkspaceStreamTileComponent implements OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.unlockOrientation();
|
||||
this.clearFullscreenHeaderHideTimeout();
|
||||
this.showFullscreenHeader.set(true);
|
||||
}
|
||||
@@ -150,6 +156,8 @@ export class VoiceWorkspaceStreamTileComponent implements OnDestroy {
|
||||
if (tile && document.fullscreenElement === tile) {
|
||||
void document.exitFullscreen().catch(() => {});
|
||||
}
|
||||
|
||||
this.unlockOrientation();
|
||||
}
|
||||
|
||||
canToggleFullscreen(): boolean {
|
||||
@@ -168,22 +176,38 @@ export class VoiceWorkspaceStreamTileComponent implements OnDestroy {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
await this.toggleFullscreen();
|
||||
}
|
||||
|
||||
async toggleFullscreen(event?: Event): Promise<void> {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
if (!this.canToggleFullscreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tile = this.tileRef()?.nativeElement;
|
||||
|
||||
if (!tile || !tile.requestFullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.fullscreenElement === tile) {
|
||||
if (this.isFullscreen()) {
|
||||
await document.exitFullscreen().catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
await tile.requestFullscreen().catch(() => {});
|
||||
await this.enterFullscreen();
|
||||
}
|
||||
|
||||
async enterLandscapeFullscreen(event?: Event): Promise<void> {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
if (!this.canToggleFullscreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isFullscreen()) {
|
||||
await this.enterFullscreen();
|
||||
}
|
||||
|
||||
await this.lockLandscape();
|
||||
}
|
||||
|
||||
async exitFullscreen(event?: Event): Promise<void> {
|
||||
@@ -263,6 +287,41 @@ export class VoiceWorkspaceStreamTileComponent implements OnDestroy {
|
||||
: 'Your preview stays muted locally to avoid audio feedback.';
|
||||
}
|
||||
|
||||
canControlStreamAudio(): boolean {
|
||||
const item = this.item();
|
||||
|
||||
return !item.isLocal && item.hasAudio;
|
||||
}
|
||||
|
||||
private async enterFullscreen(): Promise<void> {
|
||||
const tile = this.tileRef()?.nativeElement;
|
||||
|
||||
if (tile?.requestFullscreen) {
|
||||
await tile.requestFullscreen().catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
const video = this.videoRef()?.nativeElement as WebKitFullscreenVideoElement | undefined;
|
||||
|
||||
if (video?.webkitSupportsFullscreen && video.webkitEnterFullscreen) {
|
||||
video.webkitEnterFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
private async lockLandscape(): Promise<void> {
|
||||
if (!this.isMobile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const orientation = screen.orientation as LockableScreenOrientation | undefined;
|
||||
|
||||
await orientation?.lock?.('landscape').catch(() => {});
|
||||
}
|
||||
|
||||
private unlockOrientation(): void {
|
||||
screen.orientation?.unlock?.();
|
||||
}
|
||||
|
||||
private scheduleFullscreenHeaderHide(): void {
|
||||
this.clearFullscreenHeaderHideTimeout();
|
||||
|
||||
@@ -286,3 +345,12 @@ export class VoiceWorkspaceStreamTileComponent implements OnDestroy {
|
||||
this.fullscreenHeaderHideTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
interface WebKitFullscreenVideoElement extends HTMLVideoElement {
|
||||
webkitEnterFullscreen?: () => void;
|
||||
webkitSupportsFullscreen?: boolean;
|
||||
}
|
||||
|
||||
interface LockableScreenOrientation extends ScreenOrientation {
|
||||
lock?: (orientation: 'landscape') => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
<nav class="relative flex h-full w-full flex-col items-center gap-2 border-r border-border bg-secondary/35 px-2 py-3">
|
||||
<nav class="relative flex h-full min-w-14 flex-col items-center gap-2 border-r border-border bg-secondary/35 px-0 py-3 md:min-w-0 md:w-full">
|
||||
<!-- Create button -->
|
||||
<button
|
||||
appThemeNode="serversRailCreateButton"
|
||||
type="button"
|
||||
class="flex h-10 w-10 items-center justify-center rounded-md bg-primary text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
class="flex h-11 w-11 items-center justify-center rounded-md bg-primary text-primary-foreground transition-colors hover:bg-primary/90 md:h-10 md:w-10"
|
||||
title="Create Server"
|
||||
(click)="createServer()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucidePlus"
|
||||
class="w-5 h-5"
|
||||
class="h-[22px] w-[22px] md:h-5 md:w-5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@if (dmRailComponent()) {
|
||||
<ng-container *ngComponentOutlet="dmRailComponent()" />
|
||||
}
|
||||
<app-dm-rail />
|
||||
|
||||
@for (call of directCalls.visibleActiveSessions(); track call.callId + ':' + $index) {
|
||||
<div class="group/call relative flex w-full justify-center">
|
||||
@@ -27,7 +25,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="relative z-10 grid h-10 w-10 place-items-center overflow-hidden rounded-xl transition-colors hover:rounded-lg"
|
||||
class="relative z-10 grid h-11 w-11 place-items-center overflow-hidden rounded-xl transition-colors hover:rounded-lg md:h-10 md:w-10"
|
||||
[ngClass]="
|
||||
callAvatarUrls(call).length > 0
|
||||
? 'bg-emerald-950 text-white shadow-sm hover:bg-emerald-900'
|
||||
@@ -61,7 +59,7 @@
|
||||
|
||||
<ng-icon
|
||||
name="lucidePhone"
|
||||
class="relative z-10 h-5 w-5 drop-shadow"
|
||||
class="relative z-10 h-[22px] w-[22px] drop-shadow md:h-5 md:w-5"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@@ -83,7 +81,7 @@
|
||||
<button
|
||||
appThemeNode="serversRailItem"
|
||||
type="button"
|
||||
class="relative z-10 flex h-10 w-10 cursor-pointer flex-shrink-0 items-center justify-center border border-transparent transition-[border-radius,box-shadow,background-color] duration-100 hover:rounded-lg hover:bg-card"
|
||||
class="relative z-10 flex h-11 w-11 cursor-pointer flex-shrink-0 items-center justify-center border border-transparent transition-[border-radius,box-shadow,background-color] duration-100 hover:rounded-lg hover:bg-card md:h-10 md:w-10"
|
||||
[ngClass]="isSelectedRoom(room) ? 'rounded-lg ring-2 ring-primary/40 bg-primary/10' : 'rounded-xl bg-card'"
|
||||
[title]="room.name"
|
||||
[attr.aria-current]="isSelectedRoom(room) ? 'page' : null"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import {
|
||||
Component,
|
||||
DestroyRef,
|
||||
Type,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
@@ -36,6 +35,7 @@ import { RoomsActions } from '../../../store/rooms/rooms.actions';
|
||||
import { DatabaseService } from '../../../infrastructure/persistence';
|
||||
import { NotificationsFacade } from '../../../domains/notifications';
|
||||
import { DirectCallService, DirectCallSession } from '../../../domains/direct-call';
|
||||
import { DmRailComponent } from '../../../domains/direct-message/feature/dm-rail/dm-rail.component';
|
||||
import { type ServerInfo, ServerDirectoryFacade } from '../../../domains/server-directory';
|
||||
import { ThemeNodeDirective } from '../../../domains/theme';
|
||||
import { hasRoomBanForUser } from '../../../domains/access-control';
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
NgIcon,
|
||||
ConfirmDialogComponent,
|
||||
ContextMenuComponent,
|
||||
DmRailComponent,
|
||||
LeaveServerDialogComponent,
|
||||
ThemeNodeDirective,
|
||||
UserBarComponent
|
||||
@@ -71,12 +72,12 @@ export class ServersRailComponent {
|
||||
private serverDirectory = inject(ServerDirectoryFacade);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private banLookupRequestVersion = 0;
|
||||
private visibleSavedRoomCache: Room[] = [];
|
||||
private savedRoomJoinRequests = new Subject<{ room: Room; password?: string }>();
|
||||
savedRooms = this.store.selectSignal(selectSavedRooms);
|
||||
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
||||
|
||||
showMenu = signal(false);
|
||||
dmRailComponent = signal<Type<unknown> | null>(null);
|
||||
menuX = signal(72);
|
||||
menuY = signal(100);
|
||||
contextRoom = signal<Room | null>(null);
|
||||
@@ -95,9 +96,9 @@ export class ServersRailComponent {
|
||||
isOnDirectMessage = toSignal(
|
||||
this.router.events.pipe(
|
||||
filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
|
||||
map((navigationEvent) => navigationEvent.urlAfterRedirects.startsWith('/dm/') || navigationEvent.urlAfterRedirects.startsWith('/pm/'))
|
||||
map((navigationEvent) => this.isDirectMessageUrl(navigationEvent.urlAfterRedirects))
|
||||
),
|
||||
{ initialValue: this.router.url.startsWith('/dm/') || this.router.url.startsWith('/pm/') }
|
||||
{ initialValue: this.isDirectMessageUrl(this.router.url) }
|
||||
);
|
||||
isOnCall = toSignal(
|
||||
this.router.events.pipe(
|
||||
@@ -139,7 +140,7 @@ export class ServersRailComponent {
|
||||
passwordPromptRoom = signal<Room | null>(null);
|
||||
joinPassword = signal('');
|
||||
joinPasswordError = signal<string | null>(null);
|
||||
visibleSavedRooms = computed(() => this.savedRooms().filter((room) => !this.isRoomMarkedBanned(room)));
|
||||
visibleSavedRooms = computed(() => this.stabilizeVisibleSavedRooms(this.savedRooms().filter((room) => !this.isRoomMarkedBanned(room))));
|
||||
voicePresenceByRoom = computed(() => {
|
||||
const presence: Record<string, number> = {};
|
||||
const seenByRoom = new Map<string, Set<string>>();
|
||||
@@ -182,10 +183,6 @@ export class ServersRailComponent {
|
||||
});
|
||||
|
||||
constructor() {
|
||||
void import('../../../domains/direct-message/feature/dm-rail/dm-rail.component').then((module) => {
|
||||
this.dmRailComponent.set(module.DmRailComponent);
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
const rooms = this.savedRooms();
|
||||
const currentUser = this.currentUser();
|
||||
@@ -237,6 +234,7 @@ export class ServersRailComponent {
|
||||
}
|
||||
|
||||
joinSavedRoom(room: Room): void {
|
||||
const targetRoom = this.savedRooms().find((savedRoom) => savedRoom.id === room.id) ?? room;
|
||||
const currentUserId = localStorage.getItem('metoyou_currentUserId');
|
||||
|
||||
if (!currentUserId) {
|
||||
@@ -244,20 +242,20 @@ export class ServersRailComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isRoomMarkedBanned(room)) {
|
||||
this.bannedServerName.set(room.name);
|
||||
if (this.isRoomMarkedBanned(targetRoom)) {
|
||||
this.bannedServerName.set(targetRoom.name);
|
||||
this.showBannedDialog.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.optimisticSelectedRoomId.set(room.id);
|
||||
this.activateSavedRoom(room);
|
||||
this.savedRoomJoinRequests.next({ room });
|
||||
this.optimisticSelectedRoomId.set(targetRoom.id);
|
||||
this.activateSavedRoom(targetRoom);
|
||||
this.savedRoomJoinRequests.next({ room: targetRoom });
|
||||
}
|
||||
|
||||
openCall(callId: string): void {
|
||||
this.optimisticSelectedRoomId.set(null);
|
||||
void this.router.navigate(['/call', callId]);
|
||||
void this.directCalls.openCallView(callId);
|
||||
}
|
||||
|
||||
isSelectedCall(callIndex: number): boolean {
|
||||
@@ -392,17 +390,46 @@ export class ServersRailComponent {
|
||||
}
|
||||
|
||||
isSelectedRoom(room: Room): boolean {
|
||||
if (this.isOnDirectMessage() || this.isOnCall()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const optimisticRoomId = this.optimisticSelectedRoomId();
|
||||
|
||||
if (optimisticRoomId) {
|
||||
return optimisticRoomId === room.id;
|
||||
}
|
||||
|
||||
if (this.isOnDirectMessage() || this.isOnCall()) {
|
||||
return false;
|
||||
return this.currentRoom()?.id === room.id;
|
||||
}
|
||||
|
||||
private stabilizeVisibleSavedRooms(nextRooms: Room[]): Room[] {
|
||||
const previousById = new Map(this.visibleSavedRoomCache.map((room) => [room.id, room]));
|
||||
const stabilizedRooms = nextRooms.map((room) => {
|
||||
const previousRoom = previousById.get(room.id);
|
||||
|
||||
return previousRoom && this.hasSameRailRoomView(previousRoom, room) ? previousRoom : room;
|
||||
});
|
||||
|
||||
if (
|
||||
stabilizedRooms.length === this.visibleSavedRoomCache.length
|
||||
&& stabilizedRooms.every((room, index) => room === this.visibleSavedRoomCache[index])
|
||||
) {
|
||||
return this.visibleSavedRoomCache;
|
||||
}
|
||||
|
||||
return this.currentRoom()?.id === room.id;
|
||||
this.visibleSavedRoomCache = stabilizedRooms;
|
||||
return stabilizedRooms;
|
||||
}
|
||||
|
||||
private hasSameRailRoomView(previousRoom: Room, nextRoom: Room): boolean {
|
||||
return previousRoom.id === nextRoom.id && previousRoom.name === nextRoom.name && previousRoom.icon === nextRoom.icon;
|
||||
}
|
||||
|
||||
private isDirectMessageUrl(url: string): boolean {
|
||||
const path = url.split(/[?#]/, 1)[0];
|
||||
|
||||
return path === '/dm' || path.startsWith('/dm/') || path === '/pm' || path.startsWith('/pm/');
|
||||
}
|
||||
|
||||
private async refreshBannedLookup(rooms: Room[], currentUser: User | null): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user