feat: Add pm
This commit is contained in:
@@ -301,12 +301,14 @@
|
||||
@for (user of onlineRoomUsers(); track user.id) {
|
||||
<div
|
||||
class="group/user flex items-center gap-2 rounded-md px-3 py-2 transition-colors hover:bg-secondary/50 cursor-pointer"
|
||||
[attr.data-testid]="'room-user-card-' + (user.oderId || user.id)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
(contextmenu)="openUserContextMenu($event, user)"
|
||||
(click)="openProfileCard($event, user, false); $event.stopPropagation()"
|
||||
(keydown.enter)="openProfileCard($event, user, false); $event.stopPropagation()"
|
||||
(keydown.space)="openProfileCard($event, user, false); $event.preventDefault(); $event.stopPropagation()"
|
||||
(click)="openUserCard($event, user)"
|
||||
(dblclick)="openDirectMessage($event, user)"
|
||||
(keydown.enter)="openUserCard($event, user)"
|
||||
(keydown.space)="openUserCard($event, user); $event.preventDefault()"
|
||||
>
|
||||
<app-user-avatar
|
||||
[name]="user.displayName"
|
||||
@@ -350,6 +352,19 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-7 w-7 shrink-0 place-items-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-card hover:text-foreground group-hover/user:opacity-100 focus:opacity-100"
|
||||
title="Message"
|
||||
[attr.aria-label]="'Message ' + user.displayName"
|
||||
(click)="openDirectMessage($event, user)"
|
||||
(dblclick)="$event.stopPropagation()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideMessageSquare"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -363,12 +378,14 @@
|
||||
<div class="space-y-1">
|
||||
@for (member of offlineRoomMembers(); track member.oderId || member.id) {
|
||||
<div
|
||||
class="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"
|
||||
[attr.data-testid]="'room-user-card-' + (member.oderId || member.id)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
(click)="openProfileCardForMember($event, member); $event.stopPropagation()"
|
||||
(keydown.enter)="openProfileCardForMember($event, member); $event.stopPropagation()"
|
||||
(keydown.space)="openProfileCardForMember($event, member); $event.preventDefault(); $event.stopPropagation()"
|
||||
(click)="openMemberCard($event, member)"
|
||||
(dblclick)="openDirectMessageForMember($event, member)"
|
||||
(keydown.enter)="openMemberCard($event, member)"
|
||||
(keydown.space)="openMemberCard($event, member); $event.preventDefault()"
|
||||
>
|
||||
<app-user-avatar
|
||||
[name]="member.displayName"
|
||||
@@ -390,6 +407,19 @@
|
||||
</div>
|
||||
<p class="text-[10px] text-muted-foreground">Offline</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="grid h-7 w-7 shrink-0 place-items-center rounded-md text-muted-foreground opacity-0 transition-colors hover:bg-card hover:text-foreground group-hover/user:opacity-100 focus:opacity-100"
|
||||
title="Message"
|
||||
[attr.aria-label]="'Message ' + member.displayName"
|
||||
(click)="openDirectMessageForMember($event, member)"
|
||||
(dblclick)="$event.stopPropagation()"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideMessageSquare"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import {
|
||||
@@ -42,6 +43,7 @@ import {
|
||||
VoiceConnectivityHealthService
|
||||
} from '../../../domains/voice-connection';
|
||||
import { VoiceSessionFacade, VoiceWorkspaceService } from '../../../domains/voice-session';
|
||||
import { DirectMessageService } from '../../../domains/direct-message';
|
||||
import { VoicePlaybackService } from '../../../domains/voice-connection';
|
||||
import { VoiceControlsComponent } from '../../../domains/voice-session/feature/voice-controls/voice-controls.component';
|
||||
import { isChannelNameTaken, normalizeChannelName } from '../../../store/rooms/room-channels.rules';
|
||||
@@ -101,6 +103,7 @@ type PanelMode = 'channels' | 'users';
|
||||
})
|
||||
export class RoomsSidePanelComponent {
|
||||
private store = inject(Store);
|
||||
private router = inject(Router);
|
||||
private realtime = inject(RealtimeSessionFacade);
|
||||
private voiceConnection = inject(VoiceConnectionFacade);
|
||||
private screenShare = inject(ScreenShareFacade);
|
||||
@@ -109,8 +112,10 @@ export class RoomsSidePanelComponent {
|
||||
private voiceWorkspace = inject(VoiceWorkspaceService);
|
||||
private voicePlayback = inject(VoicePlaybackService);
|
||||
private profileCard = inject(ProfileCardService);
|
||||
private directMessages = inject(DirectMessageService);
|
||||
private readonly voiceActivity = inject(VoiceActivityService);
|
||||
private readonly voiceConnectivity = inject(VoiceConnectivityHealthService);
|
||||
private profileCardOpenTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
readonly panelMode = input<PanelMode>('channels');
|
||||
readonly showVoiceControls = input(true);
|
||||
@@ -201,8 +206,41 @@ export class RoomsSidePanelComponent {
|
||||
this.profileCard.open(el, user, { placement: 'left', editable });
|
||||
}
|
||||
|
||||
openUserCard(event: Event, user: User): void {
|
||||
event.stopPropagation();
|
||||
this.queueProfileCardOpen(event.currentTarget as HTMLElement, user, false);
|
||||
}
|
||||
|
||||
openProfileCardForMember(event: Event, member: RoomMember): void {
|
||||
const user: User = {
|
||||
const user = this.roomMemberToUser(member);
|
||||
|
||||
this.openProfileCard(event, user, false);
|
||||
}
|
||||
|
||||
openMemberCard(event: Event, member: RoomMember): void {
|
||||
event.stopPropagation();
|
||||
this.queueProfileCardOpen(event.currentTarget as HTMLElement, this.roomMemberToUser(member), false);
|
||||
}
|
||||
|
||||
async openDirectMessage(event: Event, user: User): Promise<void> {
|
||||
event.stopPropagation();
|
||||
this.cancelQueuedProfileCardOpen();
|
||||
|
||||
if (this.isCurrentUserIdentity(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conversation = await this.directMessages.createConversation(user);
|
||||
|
||||
await this.router.navigate(['/dm', conversation.id]);
|
||||
}
|
||||
|
||||
async openDirectMessageForMember(event: Event, member: RoomMember): Promise<void> {
|
||||
await this.openDirectMessage(event, this.roomMemberToUser(member));
|
||||
}
|
||||
|
||||
private roomMemberToUser(member: RoomMember): User {
|
||||
return {
|
||||
id: member.id,
|
||||
oderId: member.oderId || member.id,
|
||||
username: member.username,
|
||||
@@ -210,12 +248,13 @@ export class RoomsSidePanelComponent {
|
||||
description: member.description,
|
||||
profileUpdatedAt: member.profileUpdatedAt,
|
||||
avatarUrl: member.avatarUrl,
|
||||
avatarHash: member.avatarHash,
|
||||
avatarMime: member.avatarMime,
|
||||
avatarUpdatedAt: member.avatarUpdatedAt,
|
||||
status: 'disconnected',
|
||||
role: member.role,
|
||||
joinedAt: member.joinedAt
|
||||
};
|
||||
|
||||
this.openProfileCard(event, user, false);
|
||||
}
|
||||
|
||||
private roomMemberKey(member: RoomMember): string {
|
||||
@@ -256,6 +295,23 @@ export class RoomsSidePanelComponent {
|
||||
);
|
||||
}
|
||||
|
||||
private queueProfileCardOpen(anchor: HTMLElement, user: User, editable: boolean): void {
|
||||
this.cancelQueuedProfileCardOpen();
|
||||
this.profileCardOpenTimer = setTimeout(() => {
|
||||
this.profileCardOpenTimer = null;
|
||||
this.profileCard.open(anchor, user, { placement: 'left', editable });
|
||||
}, 180);
|
||||
}
|
||||
|
||||
private cancelQueuedProfileCardOpen(): void {
|
||||
if (!this.profileCardOpenTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.profileCardOpenTimer);
|
||||
this.profileCardOpenTimer = null;
|
||||
}
|
||||
|
||||
hasConnectivityIssue(user: User): boolean {
|
||||
return this.voiceConnectivity.hasPeerDesync(user.oderId || user.id);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
/>
|
||||
</button>
|
||||
|
||||
@if (dmRailComponent()) {
|
||||
<ng-container *ngComponentOutlet="dmRailComponent()" />
|
||||
}
|
||||
|
||||
<!-- Saved servers icons -->
|
||||
<div class="no-scrollbar mt-2 flex w-full flex-1 flex-col items-center gap-2 overflow-y-auto pt-0.5">
|
||||
@for (room of visibleSavedRooms(); track room.id) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import {
|
||||
Component,
|
||||
DestroyRef,
|
||||
Type,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
@@ -72,6 +73,7 @@ export class ServersRailComponent {
|
||||
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);
|
||||
@@ -86,6 +88,13 @@ export class ServersRailComponent {
|
||||
),
|
||||
{ initialValue: this.router.url.startsWith('/search') }
|
||||
);
|
||||
isOnDirectMessage = toSignal(
|
||||
this.router.events.pipe(
|
||||
filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
|
||||
map((navigationEvent) => navigationEvent.urlAfterRedirects.startsWith('/dm/'))
|
||||
),
|
||||
{ initialValue: this.router.url.startsWith('/dm/') }
|
||||
);
|
||||
bannedServerName = signal('');
|
||||
showBannedDialog = signal(false);
|
||||
showPasswordDialog = signal(false);
|
||||
@@ -135,6 +144,10 @@ 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();
|
||||
@@ -296,6 +309,10 @@ export class ServersRailComponent {
|
||||
}
|
||||
|
||||
isSelectedRoom(room: Room): boolean {
|
||||
if (this.isOnDirectMessage()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.currentRoom()?.id === room.id;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable @angular-eslint/template/cyclomatic-complexity -->
|
||||
<div
|
||||
appThemeNode="titleBar"
|
||||
class="relative z-50 flex h-10 w-full items-center justify-between border-b border-border bg-card px-4 select-none"
|
||||
@@ -55,7 +56,7 @@
|
||||
{{ roomContextMeta() }}
|
||||
</span>
|
||||
}
|
||||
} @else {
|
||||
} @else if (!isInDirectMessage()) {
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
data-theme-slot="text"
|
||||
|
||||
@@ -18,7 +18,9 @@ import {
|
||||
lucideMenu,
|
||||
lucideRefreshCw
|
||||
} from '@ng-icons/lucide';
|
||||
import { Router } from '@angular/router';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { filter, map } from 'rxjs';
|
||||
import {
|
||||
selectCurrentRoom,
|
||||
selectActiveChannelId,
|
||||
@@ -93,7 +95,14 @@ export class TitleBarComponent {
|
||||
isVoiceWorkspaceExpanded = this.voiceWorkspace.isExpanded;
|
||||
isSignalServerReconnecting = this.store.selectSignal(selectIsSignalServerReconnecting);
|
||||
signalServerCompatibilityError = this.store.selectSignal(selectSignalServerCompatibilityError);
|
||||
inRoom = computed(() => !!this.currentRoom());
|
||||
isInDirectMessage = toSignal(
|
||||
this.router.events.pipe(
|
||||
filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
|
||||
map((navigationEvent) => navigationEvent.urlAfterRedirects.startsWith('/dm/'))
|
||||
),
|
||||
{ initialValue: this.router.url.startsWith('/dm/') }
|
||||
);
|
||||
inRoom = computed(() => !!this.currentRoom() && !this.isInDirectMessage());
|
||||
roomName = computed(() => this.currentRoom()?.name || '');
|
||||
activeTextChannelName = computed(() => {
|
||||
const textChannels = this.textChannels();
|
||||
|
||||
Reference in New Issue
Block a user