feat: Add pm

This commit is contained in:
2026-04-27 00:45:16 +02:00
parent bc2fa7de22
commit 11c2588e45
65 changed files with 3653 additions and 214 deletions

View File

@@ -172,6 +172,7 @@
<textarea
#messageInputRef
[attr.data-testid]="textareaTestId()"
rows="1"
[(ngModel)]="messageContent"
(focus)="onInputFocus()"

View File

@@ -69,6 +69,7 @@ export class ChatMessageComposerComponent implements AfterViewInit, OnDestroy {
readonly showKlipyGifPicker = input(false);
readonly klipyEnabled = input(false);
readonly klipySignalSource = input<RoomSignalSourceInput | null>(null);
readonly textareaTestId = input<string | null>(null);
readonly messageSubmitted = output<ChatMessageComposerSubmitEvent>();
readonly typingStarted = output();

View File

@@ -66,6 +66,7 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
readonly isAdmin = input(false);
readonly bottomPadding = input(120);
readonly conversationKey = input.required<string>();
readonly userLookupOverrides = input<User[]>([]);
readonly replyRequested = output<ChatMessageReplyEvent>();
readonly deleteRequested = output<ChatMessageDeleteEvent>();
@@ -126,6 +127,14 @@ export class ChatMessageListComponent implements AfterViewChecked, OnDestroy {
}
}
for (const user of this.userLookupOverrides()) {
lookup.set(user.id, user);
if (user.oderId && user.oderId !== user.id) {
lookup.set(user.oderId, user);
}
}
return lookup;
});

View File

@@ -19,11 +19,10 @@
@for (user of onlineUsers(); track user.id) {
<div
class="group relative flex items-center gap-3 p-2 rounded-lg hover:bg-secondary/50 transition-colors cursor-pointer"
(click)="toggleUserMenu(user.id)"
(keydown.enter)="toggleUserMenu(user.id)"
(keydown.space)="toggleUserMenu(user.id)"
(keyup.enter)="toggleUserMenu(user.id)"
(keyup.space)="toggleUserMenu(user.id)"
[attr.data-testid]="'user-card-' + (user.oderId || user.id)"
(click)="openDirectMessage(user)"
(keydown.enter)="openDirectMessage(user)"
(keydown.space)="openDirectMessage(user)"
role="button"
tabindex="0"
>
@@ -70,6 +69,19 @@
<!-- Voice/Screen Status -->
<div class="flex items-center gap-1">
<button
type="button"
class="grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-card hover:text-foreground"
[class.hidden]="isCurrentUser(user)"
title="Message"
(click)="$event.stopPropagation(); openDirectMessage(user)"
>
<ng-icon
name="lucideMessageCircle"
class="w-4 h-4"
/>
</button>
@if (user.voiceState?.isSpeaking) {
<ng-icon
name="lucideMic"

View File

@@ -8,6 +8,7 @@ import {
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Store } from '@ngrx/store';
import { Router } from '@angular/router';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucideMic,
@@ -19,7 +20,8 @@ import {
lucideBan,
lucideUserX,
lucideVolume2,
lucideVolumeX
lucideVolumeX,
lucideMessageCircle
} from '@ng-icons/lucide';
import { UsersActions } from '../../../../store/users/users.actions';
@@ -30,6 +32,7 @@ import {
} from '../../../../store/users/users.selectors';
import { User } from '../../../../shared-kernel';
import { UserAvatarComponent, ConfirmDialogComponent } from '../../../../shared';
import { DirectMessageService } from '../../../direct-message';
@Component({
selector: 'app-user-list',
@@ -52,7 +55,8 @@ import { UserAvatarComponent, ConfirmDialogComponent } from '../../../../shared'
lucideBan,
lucideUserX,
lucideVolume2,
lucideVolumeX
lucideVolumeX,
lucideMessageCircle
})
],
templateUrl: './user-list.component.html'
@@ -62,6 +66,8 @@ import { UserAvatarComponent, ConfirmDialogComponent } from '../../../../shared'
*/
export class UserListComponent {
private store = inject(Store);
private router = inject(Router);
private directMessages = inject(DirectMessageService);
onlineUsers = this.store.selectSignal(selectOnlineUsers) as import('@angular/core').Signal<User[]>;
voiceUsers = computed(() => this.onlineUsers().filter((user: User) => !!user.voiceState?.isConnected));
@@ -84,6 +90,16 @@ export class UserListComponent {
return user.id === this.currentUser()?.id;
}
async openDirectMessage(user: User): Promise<void> {
if (this.isCurrentUser(user)) {
return;
}
const conversation = await this.directMessages.createConversation(user);
await this.router.navigate(['/dm', conversation.id]);
}
/** Toggle server-side mute on a user (admin action). */
muteUser(user: User): void {
if (user.voiceState?.isMutedByAdmin) {

View File

@@ -1,8 +1,23 @@
export * from './application/services/klipy.service';
export * from './application/services/link-metadata.service';
export * from './domain/rules/link-embed.rules';
export * from './domain/rules/message.rules';
export * from './domain/rules/message-sync.rules';
export { ChatMarkdownService } from './feature/chat-messages/services/chat-markdown.service';
export { ChatMessagesComponent } from './feature/chat-messages/chat-messages.component';
export type { ChatMessageEmbedRemoveEvent } from './feature/chat-messages/models/chat-messages.model';
export type {
ChatMessageComposerSubmitEvent,
ChatMessageDeleteEvent,
ChatMessageEditEvent,
ChatMessageImageContextMenuEvent,
ChatMessageReactionEvent,
ChatMessageReplyEvent
} from './feature/chat-messages/models/chat-messages.model';
export { ChatMessageComposerComponent } from './feature/chat-messages/components/message-composer/chat-message-composer.component';
export { ChatMessageListComponent } from './feature/chat-messages/components/message-list/chat-message-list.component';
export { ChatMessageOverlaysComponent } from './feature/chat-messages/components/message-overlays/chat-message-overlays.component';
export { TypingIndicatorComponent } from './feature/typing-indicator/typing-indicator.component';
export { KlipyGifPickerComponent } from './feature/klipy-gif-picker/klipy-gif-picker.component';
export { ChatMessageMarkdownComponent } from './feature/chat-messages/components/message-item/chat-message-markdown/chat-message-markdown.component';
export { UserListComponent } from './feature/user-list/user-list.component';