feat: Add user statuses and cards

This commit is contained in:
2026-04-16 22:52:45 +02:00
parent b4ac0cdc92
commit 2927a86fbb
57 changed files with 1964 additions and 185 deletions

View File

@@ -6,11 +6,15 @@
class="group relative flex gap-3 rounded-lg p-2 transition-colors hover:bg-secondary/30"
[class.opacity-50]="msg.isDeleted"
>
<app-user-avatar
[name]="msg.senderName"
size="md"
class="flex-shrink-0"
/>
<div
class="flex-shrink-0 cursor-pointer"
(click)="openSenderProfileCard($event); $event.stopPropagation()"
>
<app-user-avatar
[name]="msg.senderName"
size="md"
/>
</div>
<div class="min-w-0 flex-1">
@if (msg.replyToId) {
@@ -34,7 +38,11 @@
}
<div class="flex items-baseline gap-2">
<span class="font-semibold text-foreground">{{ msg.senderName }}</span>
<span
class="font-semibold text-foreground cursor-pointer hover:underline"
(click)="openSenderProfileCard($event); $event.stopPropagation()"
>{{ msg.senderName }}</span
>
<span class="text-xs text-muted-foreground">{{ formatTimestamp(msg.timestamp) }}</span>
@if (msg.editedAt && !msg.isDeleted) {
<span class="text-xs text-muted-foreground">(edited)</span>

View File

@@ -12,6 +12,7 @@ import {
signal,
ViewChild
} from '@angular/core';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucideCheck,
@@ -30,10 +31,16 @@ import {
MAX_AUTO_SAVE_SIZE_BYTES
} from '../../../../../attachment';
import { KlipyService } from '../../../../application/services/klipy.service';
import { DELETED_MESSAGE_CONTENT, Message } from '../../../../../../shared-kernel';
import {
DELETED_MESSAGE_CONTENT,
Message,
User
} from '../../../../../../shared-kernel';
import { selectAllUsers } from '../../../../../../store/users/users.selectors';
import {
ChatAudioPlayerComponent,
ChatVideoPlayerComponent,
ProfileCardService,
UserAvatarComponent
} from '../../../../../../shared';
import { ChatMessageMarkdownComponent } from './chat-message-markdown.component';
@@ -114,6 +121,9 @@ export class ChatMessageItemComponent {
private readonly attachmentsSvc = inject(AttachmentFacade);
private readonly klipy = inject(KlipyService);
private readonly store = inject(Store);
private readonly allUsers = this.store.selectSignal(selectAllUsers);
private readonly profileCard = inject(ProfileCardService);
private readonly attachmentVersion = signal(this.attachmentsSvc.updated());
readonly message = input.required<Message>();
@@ -139,6 +149,27 @@ export class ChatMessageItemComponent {
editContent = '';
openSenderProfileCard(event: MouseEvent): void {
event.stopPropagation();
const el = event.currentTarget as HTMLElement;
const msg = this.message();
// Look up full user from store
const users = this.allUsers();
const found = users.find((u) => u.id === msg.senderId || u.oderId === msg.senderId);
const user: User = found ?? {
id: msg.senderId,
oderId: msg.senderId,
username: msg.senderName,
displayName: msg.senderName,
status: 'disconnected',
role: 'member',
joinedAt: 0
};
const editable = user.id === this.currentUserId();
this.profileCard.open(el, user, { editable });
}
readonly attachmentViewModels = computed<ChatMessageAttachmentViewModel[]>(() => {
void this.attachmentVersion();

View File

@@ -1,4 +1,8 @@
import { Component, computed, input } from '@angular/core';
import {
Component,
computed,
input
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
const YOUTUBE_URL_PATTERN = /(?:youtube\.com\/(?:watch\?.*v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{11})/;

View File

@@ -27,17 +27,14 @@
role="button"
tabindex="0"
>
<!-- Avatar with online indicator -->
<!-- Avatar with status indicator -->
<div class="relative">
<app-user-avatar
[name]="user.displayName"
[status]="user.status"
[showStatusBadge]="true"
size="sm"
/>
<span
class="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-card"
[class.bg-green-500]="user.isOnline !== false && user.status !== 'offline'"
[class.bg-gray-500]="user.isOnline === false || user.status === 'offline'"
></span>
</div>
<!-- User Info -->
@@ -59,6 +56,16 @@
/>
}
</div>
@if (user.status && user.status !== 'online') {
<span
class="text-xs"
[class.text-yellow-500]="user.status === 'away'"
[class.text-red-500]="user.status === 'busy'"
[class.text-muted-foreground]="user.status === 'offline'"
>
{{ user.status === 'busy' ? 'Do Not Disturb' : (user.status | titlecase) }}
</span>
}
</div>
<!-- Voice/Screen Status -->