fix: Mobile style fixes and other small ui fixes

This commit is contained in:
2026-05-18 23:14:16 +02:00
parent afb64520ed
commit 94428ed170
32 changed files with 808 additions and 239 deletions

View File

@@ -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"

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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

View File

@@ -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[] {