feat: Theme studio v2

This commit is contained in:
2026-04-27 03:02:13 +02:00
parent 11c2588e45
commit 1b91eacb5b
52 changed files with 2792 additions and 844 deletions

View File

@@ -1,5 +1,11 @@
<section class="chat-layout relative h-full bg-background">
<header class="flex h-14 shrink-0 items-center gap-3 border-b border-border px-4">
<section
appThemeNode="dmChatSurface"
class="chat-layout relative h-full bg-background"
>
<header
appThemeNode="dmChatHeader"
class="flex h-14 shrink-0 items-center gap-3 border-b border-border px-4"
>
<app-user-avatar
[name]="peerName()"
[avatarUrl]="peerUser()?.avatarUrl"
@@ -14,7 +20,10 @@
</header>
@if (conversation()) {
<div class="absolute inset-x-0 bottom-0 top-14">
<div
appThemeNode="dmMessageRegion"
class="absolute inset-x-0 bottom-0 top-14"
>
<app-chat-message-list
[allMessages]="chatMessages()"
[channelMessages]="chatMessages()"
@@ -45,7 +54,10 @@
}
</div>
<div class="chat-bottom-bar absolute bottom-0 left-0 right-2 z-10 bg-background/85 backdrop-blur-md">
<div
appThemeNode="chatComposerBar"
class="chat-bottom-bar absolute bottom-0 left-0 right-2 z-10 bg-background/85 backdrop-blur-md"
>
<app-chat-message-composer
[replyTo]="replyTo()"
[showKlipyGifPicker]="showGifPicker()"
@@ -72,6 +84,7 @@
<div class="pointer-events-none fixed inset-0 z-[90]">
<div
appThemeNode="chatGifPickerSurface"
class="pointer-events-auto absolute w-[calc(100vw-2rem)] max-w-5xl sm:w-[34rem] md:w-[42rem] xl:w-[52rem]"
[style.bottom.px]="composerBottomPadding() + 8"
[style.right.px]="gifPickerAnchorRight()"

View File

@@ -16,6 +16,7 @@ import { map } from 'rxjs';
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
import { UserAvatarComponent } from '../../../../shared';
import { Attachment, AttachmentFacade } from '../../../attachment';
import { ThemeNodeDirective } from '../../../theme';
import { DirectMessageService } from '../../application/services/direct-message.service';
import { selectAllUsers, selectCurrentUser } from '../../../../store/users/users.selectors';
import {
@@ -56,6 +57,7 @@ interface DmStatusLabel {
ChatMessageListComponent,
ChatMessageOverlaysComponent,
KlipyGifPickerComponent,
ThemeNodeDirective,
UserAvatarComponent
],
templateUrl: './dm-chat.component.html',
@@ -106,21 +108,23 @@ export class DmChatComponent {
const knownUser = knownUsers.find((user) => user.id === participantId || user.oderId === participantId);
const participant = conversation.participantProfiles[participantId];
return knownUser ?? {
id: participantId,
oderId: participantId,
username: participant?.username || participant?.displayName || participantId,
displayName: participant?.displayName || participant?.username || participantId,
description: participant?.description,
profileUpdatedAt: participant?.profileUpdatedAt,
avatarUrl: participant?.avatarUrl,
avatarHash: participant?.avatarHash,
avatarMime: participant?.avatarMime,
avatarUpdatedAt: participant?.avatarUpdatedAt,
status: 'disconnected',
role: 'member',
joinedAt: 0
};
return (
knownUser ?? {
id: participantId,
oderId: participantId,
username: participant?.username || participant?.displayName || participantId,
displayName: participant?.displayName || participant?.username || participantId,
description: participant?.description,
profileUpdatedAt: participant?.profileUpdatedAt,
avatarUrl: participant?.avatarUrl,
avatarHash: participant?.avatarHash,
avatarMime: participant?.avatarMime,
avatarUpdatedAt: participant?.avatarUpdatedAt,
status: 'disconnected',
role: 'member',
joinedAt: 0
}
);
});
});
readonly messageStatuses = computed<DmStatusLabel[]>(() => {
@@ -218,14 +222,13 @@ export class DmChatComponent {
const content = event.content.trim() || event.pendingFiles.map((file) => file.name).join('\n');
void this.directMessages.sendMessage(conversation.id, content, this.replyTo()?.id)
.then((message) => {
this.replyTo.set(null);
void this.directMessages.sendMessage(conversation.id, content, this.replyTo()?.id).then((message) => {
this.replyTo.set(null);
if (event.pendingFiles.length > 0) {
this.attachments.publishAttachments(message.id, event.pendingFiles, this.currentUserId() || undefined);
}
});
if (event.pendingFiles.length > 0) {
this.attachments.publishAttachments(message.id, event.pendingFiles, this.currentUserId() || undefined);
}
});
}
setReplyTo(message: ChatMessageReplyEvent): void {
@@ -388,8 +391,7 @@ export class DmChatComponent {
continue;
}
const urls = this.linkMetadata.extractUrls(message.content)
.filter((url) => !hasDedicatedChatEmbed(url));
const urls = this.linkMetadata.extractUrls(message.content).filter((url) => !hasDedicatedChatEmbed(url));
if (urls.length === 0) {
continue;

View File

@@ -8,7 +8,10 @@
[ngStyle]="listPanelStyles()"
>
<section class="flex h-full w-full min-w-0 flex-col">
<header class="flex h-14 shrink-0 items-center gap-2 border-b border-border px-3">
<header
appThemeNode="dmConversationsHeader"
class="flex h-14 shrink-0 items-center gap-2 border-b border-border px-3"
>
<div class="grid h-8 w-8 place-items-center rounded-lg bg-secondary text-muted-foreground">
<ng-icon
name="lucideMessageCircle"
@@ -21,13 +24,17 @@
</div>
</header>
<div class="min-h-0 flex-1 overflow-y-auto p-2">
<div
appThemeNode="dmConversationList"
class="min-h-0 flex-1 overflow-y-auto p-2"
>
@if (directMessages.conversations().length === 0) {
<div class="flex h-full items-center justify-center px-4 text-center text-sm text-muted-foreground">No direct messages yet.</div>
} @else {
<div class="space-y-1">
@for (conversation of directMessages.conversations(); track conversation.id) {
<div
appThemeNode="dmConversationItem"
class="group flex w-full items-center gap-2 rounded-md px-2 py-2 text-left transition-colors hover:bg-secondary/60"
[class.bg-primary/10]="isSelectedConversation(conversation)"
[class.text-foreground]="isSelectedConversation(conversation)"
@@ -72,7 +79,10 @@
}
</div>
<div class="border-t border-border px-2 py-3">
<div
appThemeNode="dmVoiceControlsArea"
class="border-t border-border px-2 py-3"
>
<app-voice-controls />
</div>
</section>

View File

@@ -50,7 +50,7 @@ export class DmWorkspaceComponent implements OnDestroy {
readonly routeConversationId = toSignal(this.route.paramMap.pipe(map((params) => params.get('conversationId'))), {
initialValue: this.route.snapshot.paramMap.get('conversationId')
});
readonly layoutStyles = computed(() => this.theme.getLayoutContainerStyles('roomLayout'));
readonly layoutStyles = computed(() => this.theme.getLayoutContainerStyles('dmLayout'));
readonly listPanelStyles = computed(() => this.theme.getLayoutItemStyles('dmConversationsPanel'));
readonly chatPanelStyles = computed(() => this.theme.getLayoutItemStyles('dmChatPanel'));