refactor: stricter domain: chat

This commit is contained in:
2026-04-11 14:01:19 +02:00
parent 39b85e2e3a
commit 98ed8eeb68
16 changed files with 34 additions and 29 deletions

View File

@@ -7,11 +7,14 @@ Text messaging, reactions, GIF search, typing indicators, and the user list. All
``` ```
chat/ chat/
├── application/ ├── application/
│ └── klipy.service.ts GIF search via the KLIPY API (proxied through the server) │ └── services/
│ ├── klipy.service.ts GIF search via the KLIPY API (proxied through the server)
│ └── link-metadata.service.ts Link preview metadata fetching
├── domain/ ├── domain/
── message.rules.ts canEditMessage, normaliseDeletedMessage, getMessageTimestamp ── rules/
── message-sync.rules.ts Inventory-based sync: chunkArray, findMissingIds, limits ── message.rules.ts canEditMessage, normaliseDeletedMessage, getMessageTimestamp
│ └── message-sync.rules.ts Inventory-based sync: chunkArray, findMissingIds, limits
├── feature/ ├── feature/
│ ├── chat-messages/ Main chat view (orchestrates composer, list, overlays) │ ├── chat-messages/ Main chat view (orchestrates composer, list, overlays)
@@ -25,6 +28,7 @@ chat/
│ │ └── services/ │ │ └── services/
│ │ └── chat-markdown.service.ts Markdown-to-HTML rendering │ │ └── chat-markdown.service.ts Markdown-to-HTML rendering
│ │ │ │
│ ├── chat-image-proxy-fallback.directive.ts Image proxy fallback for broken URLs
│ ├── klipy-gif-picker/ GIF search/browse picker panel │ ├── klipy-gif-picker/ GIF search/browse picker panel
│ ├── typing-indicator/ "X is typing..." display (3 s TTL, max 4 names) │ ├── typing-indicator/ "X is typing..." display (3 s TTL, max 4 names)
│ └── user-list/ Online user sidebar │ └── user-list/ Online user sidebar
@@ -129,7 +133,7 @@ graph LR
Klipy --> API Klipy --> API
click Picker "feature/klipy-gif-picker/" "GIF search panel" _blank click Picker "feature/klipy-gif-picker/" "GIF search panel" _blank
click Klipy "application/klipy.service.ts" "GIF search via KLIPY API" _blank click Klipy "application/services/klipy.service.ts" "GIF search via KLIPY API" _blank
click SD "../server-directory/application/server-directory.facade.ts" "Resolves API base URL" _blank click SD "../server-directory/application/server-directory.facade.ts" "Resolves API base URL" _blank
``` ```

View File

@@ -13,7 +13,7 @@ import {
throwError throwError
} from 'rxjs'; } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
import { ServerDirectoryFacade } from '../../server-directory'; import { ServerDirectoryFacade } from '../../../server-directory';
export interface KlipyGif { export interface KlipyGif {
id: string; id: string;

View File

@@ -1,8 +1,8 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs'; import { firstValueFrom } from 'rxjs';
import { ServerDirectoryFacade } from '../../server-directory'; import { ServerDirectoryFacade } from '../../../server-directory';
import { LinkMetadata } from '../../../shared-kernel'; import { LinkMetadata } from '../../../../shared-kernel';
const URL_PATTERN = /https?:\/\/[^\s<>)"']+/g; const URL_PATTERN = /https?:\/\/[^\s<>)"']+/g;

View File

@@ -1,4 +1,4 @@
import { DELETED_MESSAGE_CONTENT, type Message } from '../../../shared-kernel'; import { DELETED_MESSAGE_CONTENT, type Message } from '../../../../shared-kernel';
/** Extracts the effective timestamp from a message (editedAt takes priority). */ /** Extracts the effective timestamp from a message (editedAt takes priority). */
export function getMessageTimestamp(msg: Message): number { export function getMessageTimestamp(msg: Message): number {

View File

@@ -7,7 +7,7 @@ import {
input, input,
signal signal
} from '@angular/core'; } from '@angular/core';
import { KlipyService } from '../application/klipy.service'; import { KlipyService } from '../application/services/klipy.service';
@Directive({ @Directive({
selector: 'img[appChatImageProxyFallback]', selector: 'img[appChatImageProxyFallback]',

View File

@@ -11,7 +11,7 @@ import { Store } from '@ngrx/store';
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service'; import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
import { RealtimeSessionFacade } from '../../../../core/realtime'; import { RealtimeSessionFacade } from '../../../../core/realtime';
import { Attachment, AttachmentFacade } from '../../../attachment'; import { Attachment, AttachmentFacade } from '../../../attachment';
import { KlipyGif } from '../../application/klipy.service'; import { KlipyGif } from '../../application/services/klipy.service';
import { MessagesActions } from '../../../../store/messages/messages.actions'; import { MessagesActions } from '../../../../store/messages/messages.actions';
import { import {
selectAllMessages, selectAllMessages,
@@ -33,7 +33,7 @@ import {
ChatMessageImageContextMenuEvent, ChatMessageImageContextMenuEvent,
ChatMessageReactionEvent, ChatMessageReactionEvent,
ChatMessageReplyEvent ChatMessageReplyEvent
} from './models/chat-messages.models'; } from './models/chat-messages.model';
@Component({ @Component({
selector: 'app-chat-messages', selector: 'app-chat-messages',

View File

@@ -21,12 +21,12 @@ import {
} from '@ng-icons/lucide'; } from '@ng-icons/lucide';
import type { ClipboardFilePayload } from '../../../../../../core/platform/electron/electron-api.models'; import type { ClipboardFilePayload } from '../../../../../../core/platform/electron/electron-api.models';
import { ElectronBridgeService } from '../../../../../../core/platform/electron/electron-bridge.service'; import { ElectronBridgeService } from '../../../../../../core/platform/electron/electron-bridge.service';
import { KlipyGif, KlipyService } from '../../../../application/klipy.service'; import { KlipyGif, KlipyService } from '../../../../application/services/klipy.service';
import { Message } from '../../../../../../shared-kernel'; import { Message } from '../../../../../../shared-kernel';
import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive'; import { ChatImageProxyFallbackDirective } from '../../../chat-image-proxy-fallback.directive';
import { TypingIndicatorComponent } from '../../../typing-indicator/typing-indicator.component'; import { TypingIndicatorComponent } from '../../../typing-indicator/typing-indicator.component';
import { ChatMarkdownService } from '../../services/chat-markdown.service'; import { ChatMarkdownService } from '../../services/chat-markdown.service';
import { ChatMessageComposerSubmitEvent } from '../../models/chat-messages.models'; import { ChatMessageComposerSubmitEvent } from '../../models/chat-messages.model';
type LocalFileWithPath = File & { type LocalFileWithPath = File & {
path?: string; path?: string;

View File

@@ -29,7 +29,7 @@ import {
AttachmentFacade, AttachmentFacade,
MAX_AUTO_SAVE_SIZE_BYTES MAX_AUTO_SAVE_SIZE_BYTES
} from '../../../../../attachment'; } from '../../../../../attachment';
import { KlipyService } from '../../../../application/klipy.service'; import { KlipyService } from '../../../../application/services/klipy.service';
import { DELETED_MESSAGE_CONTENT, Message } from '../../../../../../shared-kernel'; import { DELETED_MESSAGE_CONTENT, Message } from '../../../../../../shared-kernel';
import { import {
ChatAudioPlayerComponent, ChatAudioPlayerComponent,
@@ -45,7 +45,7 @@ import {
ChatMessageImageContextMenuEvent, ChatMessageImageContextMenuEvent,
ChatMessageReactionEvent, ChatMessageReactionEvent,
ChatMessageReplyEvent ChatMessageReplyEvent
} from '../../models/chat-messages.models'; } from '../../models/chat-messages.model';
const COMMON_EMOJIS = [ const COMMON_EMOJIS = [
'👍', '👍',

View File

@@ -13,7 +13,7 @@ import {
signal signal
} from '@angular/core'; } from '@angular/core';
import { Attachment } from '../../../../../attachment'; import { Attachment } from '../../../../../attachment';
import { getMessageTimestamp } from '../../../../domain/message.rules'; import { getMessageTimestamp } from '../../../../domain/rules/message.rules';
import { Message } from '../../../../../../shared-kernel'; import { Message } from '../../../../../../shared-kernel';
import { import {
ChatMessageDeleteEvent, ChatMessageDeleteEvent,
@@ -22,7 +22,7 @@ import {
ChatMessageImageContextMenuEvent, ChatMessageImageContextMenuEvent,
ChatMessageReactionEvent, ChatMessageReactionEvent,
ChatMessageReplyEvent ChatMessageReplyEvent
} from '../../models/chat-messages.models'; } from '../../models/chat-messages.model';
import { ChatMessageItemComponent } from '../message-item/chat-message-item.component'; import { ChatMessageItemComponent } from '../message-item/chat-message-item.component';
interface PrismGlobal { interface PrismGlobal {

View File

@@ -12,7 +12,7 @@ import {
} from '@ng-icons/lucide'; } from '@ng-icons/lucide';
import { Attachment } from '../../../../../attachment'; import { Attachment } from '../../../../../attachment';
import { ContextMenuComponent } from '../../../../../../shared'; import { ContextMenuComponent } from '../../../../../../shared';
import { ChatMessageImageContextMenuEvent } from '../../models/chat-messages.models'; import { ChatMessageImageContextMenuEvent } from '../../models/chat-messages.model';
@Component({ @Component({
selector: 'app-chat-message-overlays', selector: 'app-chat-message-overlays',

View File

@@ -20,7 +20,7 @@ import {
lucideSearch, lucideSearch,
lucideX lucideX
} from '@ng-icons/lucide'; } from '@ng-icons/lucide';
import { KlipyGif, KlipyService } from '../../application/klipy.service'; import { KlipyGif, KlipyService } from '../../application/services/klipy.service';
import { ChatImageProxyFallbackDirective } from '../chat-image-proxy-fallback.directive'; import { ChatImageProxyFallbackDirective } from '../chat-image-proxy-fallback.directive';
const KLIPY_CARD_MIN_WIDTH = 140; const KLIPY_CARD_MIN_WIDTH = 140;

View File

@@ -1,6 +1,7 @@
export * from './application/klipy.service'; export * from './application/services/klipy.service';
export * from './domain/message.rules'; export * from './application/services/link-metadata.service';
export * from './domain/message-sync.rules'; export * from './domain/rules/message.rules';
export * from './domain/rules/message-sync.rules';
export { ChatMessagesComponent } from './feature/chat-messages/chat-messages.component'; export { ChatMessagesComponent } from './feature/chat-messages/chat-messages.component';
export { TypingIndicatorComponent } from './feature/typing-indicator/typing-indicator.component'; export { TypingIndicatorComponent } from './feature/typing-indicator/typing-indicator.component';
export { KlipyGifPickerComponent } from './feature/klipy-gif-picker/klipy-gif-picker.component'; export { KlipyGifPickerComponent } from './feature/klipy-gif-picker/klipy-gif-picker.component';

View File

@@ -37,7 +37,7 @@ import { DatabaseService } from '../../infrastructure/persistence';
import { reportDebuggingError, trackDebuggingTaskFailure } from '../../core/helpers/debugging-helpers'; import { reportDebuggingError, trackDebuggingTaskFailure } from '../../core/helpers/debugging-helpers';
import { DebuggingService } from '../../core/services'; import { DebuggingService } from '../../core/services';
import { AttachmentFacade } from '../../domains/attachment'; import { AttachmentFacade } from '../../domains/attachment';
import { LinkMetadataService } from '../../domains/chat/application/link-metadata.service'; import { LinkMetadataService } from '../../domains/chat/application/services/link-metadata.service';
import { TimeSyncService } from '../../core/services/time-sync.service'; import { TimeSyncService } from '../../core/services/time-sync.service';
import { import {
DELETED_MESSAGE_CONTENT, DELETED_MESSAGE_CONTENT,
@@ -45,7 +45,7 @@ import {
Reaction Reaction
} from '../../shared-kernel'; } from '../../shared-kernel';
import { hydrateMessages } from './messages.helpers'; import { hydrateMessages } from './messages.helpers';
import { canEditMessage } from '../../domains/chat/domain/message.rules'; import { canEditMessage } from '../../domains/chat/domain/rules/message.rules';
import { resolveRoomPermission } from '../../domains/access-control'; import { resolveRoomPermission } from '../../domains/access-control';
import { dispatchIncomingMessage, IncomingMessageContext } from './messages-incoming.handlers'; import { dispatchIncomingMessage, IncomingMessageContext } from './messages-incoming.handlers';

View File

@@ -4,8 +4,8 @@
*/ */
import { Message } from '../../shared-kernel'; import { Message } from '../../shared-kernel';
import { DatabaseService } from '../../infrastructure/persistence'; import { DatabaseService } from '../../infrastructure/persistence';
import { getMessageTimestamp, normaliseDeletedMessage } from '../../domains/chat/domain/message.rules'; import { getMessageTimestamp, normaliseDeletedMessage } from '../../domains/chat/domain/rules/message.rules';
import type { InventoryItem } from '../../domains/chat/domain/message-sync.rules'; import type { InventoryItem } from '../../domains/chat/domain/rules/message-sync.rules';
// Re-export domain logic so existing callers keep working // Re-export domain logic so existing callers keep working
export { export {
@@ -13,7 +13,7 @@ export {
getLatestTimestamp, getLatestTimestamp,
normaliseDeletedMessage, normaliseDeletedMessage,
canEditMessage canEditMessage
} from '../../domains/chat/domain/message.rules'; } from '../../domains/chat/domain/rules/message.rules';
export { export {
INVENTORY_LIMIT, INVENTORY_LIMIT,
CHUNK_SIZE, CHUNK_SIZE,
@@ -23,8 +23,8 @@ export {
FULL_SYNC_LIMIT, FULL_SYNC_LIMIT,
chunkArray, chunkArray,
findMissingIds findMissingIds
} from '../../domains/chat/domain/message-sync.rules'; } from '../../domains/chat/domain/rules/message-sync.rules';
export type { InventoryItem } from '../../domains/chat/domain/message-sync.rules'; export type { InventoryItem } from '../../domains/chat/domain/rules/message-sync.rules';
/** Hydrates a single message with its reactions from the database. */ /** Hydrates a single message with its reactions from the database. */
export async function hydrateMessage( export async function hydrateMessage(