feat: Add deafen to pc, fix mobiel view, fix freeze on startup

This commit is contained in:
2026-06-05 15:27:06 +02:00
parent 35f52b0356
commit a675f12e61
85 changed files with 2499 additions and 519 deletions

View File

@@ -1,7 +1,7 @@
<ng-template #pageContent>
<div class="h-full min-h-0 overflow-y-auto bg-background text-foreground">
<div class="mx-auto w-full max-w-5xl space-y-8 p-4 sm:p-6 lg:py-8">
<header class="space-y-1">
<div class="h-full min-h-0 min-w-0 w-full overflow-x-hidden overflow-y-auto bg-background text-foreground">
<div class="mx-auto w-full min-w-0 max-w-5xl space-y-8 p-4 sm:p-6 lg:py-8">
<header class="min-w-0 space-y-1">
<h1 class="text-2xl font-semibold text-foreground">
@if (currentUser()) {
Welcome back, {{ currentUser()!.displayName || 'there' }}
@@ -12,8 +12,8 @@
<p class="text-sm text-muted-foreground">Find people, discover servers, or start your own community.</p>
</header>
<div>
<div class="relative">
<div class="min-w-0">
<div class="relative min-w-0">
<ng-icon
name="lucideSearch"
class="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground"
@@ -22,8 +22,8 @@
#searchInput
type="text"
aria-label="Search people, servers, and invites"
class="h-12 w-full rounded-xl border border-border bg-secondary py-2 pl-11 pr-20 text-base text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
placeholder="Search for people, servers, or paste an invite..."
class="h-12 w-full min-w-0 rounded-xl border border-border bg-secondary py-2 pl-11 pr-4 text-base text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary sm:pr-20"
[placeholder]="isMobile() ? 'Search people, servers, invites...' : 'Search for people, servers, or paste an invite...'"
[ngModel]="searchQuery()"
(ngModelChange)="onSearchChange($event)"
(keydown.enter)="submitSearch()"
@@ -189,10 +189,10 @@
</section>
} @else {
<!-- Primary actions -->
<section class="grid gap-3 sm:grid-cols-3">
<section class="grid min-w-0 gap-3 sm:grid-cols-3">
<a
routerLink="/people"
class="group flex items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
class="group flex min-w-0 w-full items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
>
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-purple-500/15 text-purple-400">
<ng-icon
@@ -212,7 +212,7 @@
<a
routerLink="/servers"
class="group flex items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
class="group flex min-w-0 w-full items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
>
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-blue-500/15 text-blue-400">
<ng-icon
@@ -232,7 +232,7 @@
<a
routerLink="/create-server"
class="group flex items-center gap-3 rounded-xl border border-emerald-500/40 bg-emerald-500/10 p-4 transition-colors hover:bg-emerald-500/15"
class="group flex min-w-0 w-full items-center gap-3 rounded-xl border border-emerald-500/40 bg-emerald-500/10 p-4 transition-colors hover:bg-emerald-500/15"
>
<div class="grid h-11 w-11 shrink-0 place-items-center rounded-lg bg-emerald-500/20 text-emerald-400">
<ng-icon
@@ -267,8 +267,8 @@
}
<!-- People + Popular servers -->
<section class="grid gap-4 lg:grid-cols-2">
<div class="rounded-xl border border-border bg-card/40 p-4">
<section class="grid min-w-0 gap-4 lg:grid-cols-2">
<div class="min-w-0 rounded-xl border border-border bg-card/40 p-4">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold text-foreground">People you might know</h2>
<a
@@ -280,7 +280,7 @@
@if (peopleYouMightKnow().length > 0) {
<div class="space-y-1">
@for (person of peopleYouMightKnow(); track person.id) {
<div class="flex items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
<div class="flex min-w-0 items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
<app-user-avatar
[name]="personLabel(person)"
[avatarUrl]="person.avatarUrl"
@@ -292,7 +292,10 @@
<p class="truncate text-sm font-medium text-foreground">{{ personLabel(person) }}</p>
<p class="text-xs text-muted-foreground">{{ isOnline(person) ? 'Online' : 'Offline' }}</p>
</div>
<app-friend-button [user]="person" />
<app-friend-button
class="shrink-0"
[user]="person"
/>
</div>
}
</div>
@@ -301,7 +304,7 @@
}
</div>
<div class="rounded-xl border border-border bg-card/40 p-4">
<div class="min-w-0 rounded-xl border border-border bg-card/40 p-4">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold text-foreground">Popular Servers</h2>
<a
@@ -313,7 +316,7 @@
@if (popularServers().length > 0) {
<div class="space-y-1">
@for (server of popularServers(); track server.id) {
<div class="flex items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
<div class="flex min-w-0 items-center gap-3 rounded-lg px-2 py-2 transition-colors hover:bg-secondary/60">
<div
class="grid h-10 w-10 shrink-0 place-items-center overflow-hidden rounded-lg bg-secondary text-sm font-semibold text-foreground"
>
@@ -358,9 +361,9 @@
>Manage</a
>
</div>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
<div class="grid min-w-0 grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
@for (friend of friends(); track friend.id) {
<div class="flex items-center gap-3 rounded-xl border border-border bg-card p-3">
<div class="flex min-w-0 items-center gap-3 rounded-xl border border-border bg-card p-3">
<app-user-avatar
[name]="personLabel(friend)"
[avatarUrl]="friend.avatarUrl"
@@ -383,11 +386,11 @@
@if (recentlyActiveServers().length > 0) {
<section>
<h2 class="mb-3 text-sm font-semibold text-foreground">Recently Active Servers</h2>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
<div class="grid min-w-0 grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
@for (room of recentlyActiveServers(); track room.id) {
<button
type="button"
class="flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 text-center transition-colors hover:border-primary/50 hover:bg-card/80"
class="flex min-w-0 flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 text-center transition-colors hover:border-primary/50 hover:bg-card/80"
(click)="openSavedRoom(room)"
>
<div
@@ -415,24 +418,4 @@
</div>
</ng-template>
@if (isMobile()) {
<swiper-container
class="block h-full min-h-0 w-full bg-background"
slides-per-view="1"
space-between="0"
initial-slide="0"
threshold="10"
resistance-ratio="0"
>
<swiper-slide class="block h-full w-full">
<div class="flex h-full w-full min-h-0 overflow-hidden">
<app-servers-rail class="block h-full shrink-0" />
<div class="flex min-h-0 flex-1 overflow-hidden border-l border-border">
<ng-container [ngTemplateOutlet]="pageContent" />
</div>
</div>
</swiper-slide>
</swiper-container>
} @else {
<ng-container [ngTemplateOutlet]="pageContent" />
}
<ng-container [ngTemplateOutlet]="pageContent" />

View File

@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/member-ordering */
import {
CUSTOM_ELEMENTS_SCHEMA,
Component,
ElementRef,
HostListener,
@@ -41,7 +40,6 @@ import { selectAllUsers, selectCurrentUser } from '../../store/users/users.selec
import type { Room, User } from '../../shared-kernel';
import type { ServerInfo } from '../../domains/server-directory/domain/models/server-directory.model';
import { ServerDirectoryFacade } from '../../domains/server-directory/application/facades/server-directory.facade';
import { ServersRailComponent } from '../servers/servers-rail/servers-rail.component';
import { ViewportService } from '../../core/platform';
import { FriendService } from '../../domains/direct-message/application/services/friend.service';
import { FriendButtonComponent } from '../../domains/direct-message/feature/friend-button/friend-button.component';
@@ -72,8 +70,7 @@ const RECENT_SEARCHES_STORAGE_KEY = 'metoyou_dashboard_recent_searches';
RouterLink,
NgIcon,
FriendButtonComponent,
UserAvatarComponent,
ServersRailComponent
UserAvatarComponent
],
viewProviders: [
provideIcons({
@@ -87,8 +84,10 @@ const RECENT_SEARCHES_STORAGE_KEY = 'metoyou_dashboard_recent_searches';
lucideX
})
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
templateUrl: './dashboard.component.html'
templateUrl: './dashboard.component.html',
host: {
class: 'block h-full min-h-0 min-w-0 w-full overflow-hidden'
}
})
export class DashboardComponent implements OnInit {
private store = inject(Store);

View File

@@ -15,7 +15,13 @@
<button
type="button"
class="grid h-12 w-12 place-items-center rounded-full bg-secondary text-foreground transition-colors hover:bg-secondary/80 disabled:opacity-45"
class="grid h-12 w-12 place-items-center rounded-full transition-colors disabled:opacity-45"
[class.bg-secondary]="!muted()"
[class.text-foreground]="!muted()"
[class.hover:bg-secondary/80]="!muted()"
[class.bg-destructive/10]="muted()"
[class.text-destructive]="muted()"
[class.hover:bg-destructive/15]="muted()"
[disabled]="!connected()"
(click)="muteToggled.emit()"
[attr.aria-label]="muted() ? 'Unmute' : 'Mute'"
@@ -27,6 +33,26 @@
/>
</button>
<button
type="button"
class="grid h-12 w-12 place-items-center rounded-full transition-colors disabled:opacity-45"
[class.bg-secondary]="!deafened()"
[class.text-foreground]="!deafened()"
[class.hover:bg-secondary/80]="!deafened()"
[class.bg-destructive/10]="deafened()"
[class.text-destructive]="deafened()"
[class.hover:bg-destructive/15]="deafened()"
[disabled]="!connected()"
(click)="deafenToggled.emit()"
[attr.aria-label]="deafened() ? 'Undeafen' : 'Deafen'"
[title]="deafened() ? 'Undeafen' : 'Deafen'"
>
<ng-icon
name="lucideHeadphones"
class="h-5 w-5"
/>
</button>
@if (showSpeakerphoneButton()) {
<button
type="button"

View File

@@ -0,0 +1,34 @@
import { Injector, runInInjectionContext } from '@angular/core';
import {
describe,
expect,
it,
vi
} from 'vitest';
import { PrivateCallControlsComponent } from './private-call-controls.component';
function createComponent(): PrivateCallControlsComponent {
const injector = Injector.create({ providers: [PrivateCallControlsComponent] });
return runInInjectionContext(injector, () => injector.get(PrivateCallControlsComponent));
}
describe('PrivateCallControlsComponent', () => {
it('exposes deafened input and deafenToggled output', () => {
const component = createComponent();
expect(component.deafened).toBeDefined();
expect(component.deafenToggled).toBeDefined();
});
it('emits deafenToggled when the output is triggered', () => {
const component = createComponent();
const handler = vi.fn();
component.deafenToggled.subscribe(handler);
component.deafenToggled.emit();
expect(handler).toHaveBeenCalledTimes(1);
});
});

View File

@@ -5,6 +5,7 @@ import {
} from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucideHeadphones,
lucideMic,
lucideMicOff,
lucideMonitor,
@@ -22,6 +23,7 @@ import {
imports: [NgIcon],
viewProviders: [
provideIcons({
lucideHeadphones,
lucideMic,
lucideMicOff,
lucideMonitor,
@@ -38,6 +40,7 @@ import {
export class PrivateCallControlsComponent {
readonly connected = input.required<boolean>();
readonly muted = input.required<boolean>();
readonly deafened = input.required<boolean>();
readonly cameraEnabled = input.required<boolean>();
readonly screenSharing = input.required<boolean>();
readonly showSpeakerphoneButton = input(false);
@@ -45,6 +48,7 @@ export class PrivateCallControlsComponent {
readonly joinRequested = output();
readonly muteToggled = output();
readonly deafenToggled = output();
readonly cameraToggled = output();
readonly screenShareToggled = output();
readonly speakerphoneToggled = output();

View File

@@ -196,12 +196,14 @@
class="mx-auto block w-full max-w-5xl"
[connected]="isConnected()"
[muted]="isMuted()"
[deafened]="isDeafened()"
[cameraEnabled]="isCameraEnabled()"
[screenSharing]="isScreenSharing()"
[showSpeakerphoneButton]="showSpeakerphoneButton()"
[speakerphoneEnabled]="speakerphoneEnabled()"
(joinRequested)="join()"
(muteToggled)="toggleMute()"
(deafenToggled)="toggleDeafen()"
(cameraToggled)="toggleCamera()"
(screenShareToggled)="toggleScreenShare()"
(speakerphoneToggled)="toggleSpeakerphone()"

View File

@@ -496,18 +496,27 @@ export class PrivateCallComponent {
return;
}
const 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
}
voiceState
})
);
this.voice.broadcastMessage({
type: 'voice-state',
oderId: user.oderId || user.id,
displayName: user.displayName || 'User',
voiceState
});
}
private remoteParticipantPeerIds(session: DirectCallSession, currentUserId: string): string[] {

View File

@@ -14,7 +14,7 @@
<swiper-slide class="block h-full w-full">
<div class="flex h-full w-full min-h-0 overflow-hidden">
<app-servers-rail class="block h-full shrink-0" />
<div class="flex min-h-0 flex-1 overflow-hidden border-l border-border bg-card">
<div class="flex min-h-0 min-w-0 flex-1 overflow-hidden border-l border-border bg-card">
<app-rooms-side-panel
panelMode="channels"
(textChannelSelected)="setMobilePage('main')"

View File

@@ -1,4 +1,4 @@
<nav class="relative flex h-full min-w-16 flex-col items-center gap-2 border-r border-border bg-secondary/35 px-0 py-3 md:min-w-0 md:w-full">
<nav class="relative flex h-full w-16 min-w-16 max-w-16 flex-col items-center gap-2 border-r border-border bg-secondary/35 px-0 py-3 md:min-w-0 md:max-w-none md:w-full">
<!-- Home / dashboard button -->
<button
appThemeNode="serversRailCreateButton"