Files
task-stream/libs/api/tracker/application/src/lib/event-handlers/comment-added.event-handler.ts
Josep Piguillem 199b60e4ce feat: conversations on a ticket are settled, and you see who is typing
A top-level comment and its replies can be resolved by whoever can
change the ticket (CommentThreadResolvedEvent), with an optional
conclusion added as a reply marked as a decision, so it shows above the
ticket. A settled conversation folds to one line (topic, who settled it
and the conclusion) and can be unfolded or reopened; the header counts
the open ones, and finishing the ticket with one open asks first.

«Ana está escribiendo…» shows in the conversation being answered, at
once: a `typing` mutation publishes to a Redis channel per ticket
(`typing` subscription), never stored. In replies, Enter sends and
Shift+Enter breaks the line, except inside lists and code.

API: resolveThread / PUT-DELETE /api/comments/{id}/resolution, Comment
.resolved/resolvedBy/resolvedAt, typing mutation and subscription. MCP:
resolve_thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:35:46 +02:00

52 lines
2.0 KiB
TypeScript

import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { CommentAddedEvent } from '@task-stream/api/tracker/domain';
import { FindCommentsRepository } from '../ports/find-comments.repository';
import { UpsertMaterializedCommentRepository } from '../ports/upsert-materialized-comment.repository';
import { UpsertMaterializedIssueRepository } from '../ports/upsert-materialized-issue.repository';
import { UserDirectory } from '../ports/user-directory';
import { ReadModelNotifier, versionOf } from './read-model-notifier';
@EventsHandler(CommentAddedEvent)
export class CommentAddedEventHandler
implements IEventHandler<CommentAddedEvent>
{
constructor(
private readonly upsertMaterializedCommentRepository: UpsertMaterializedCommentRepository,
private readonly upsertMaterializedIssueRepository: UpsertMaterializedIssueRepository,
private readonly findComments: FindCommentsRepository,
private readonly users: UserDirectory,
private readonly notifier: ReadModelNotifier,
) {}
async handle(event: CommentAddedEvent) {
const { comment } = event;
const users = await this.users.refs([comment.authorId]);
await this.upsertMaterializedCommentRepository.upsert({
id: comment.id,
issueId: comment.issueId,
projectId: comment.projectId,
author: users.of(comment.authorId),
body: comment.body,
replyTo: comment.replyTo ?? null,
deleted: false,
reactions: [],
decision: false,
decidedBy: null,
decidedAt: null,
resolved: false,
resolvedBy: null,
resolvedAt: null,
createdAt: comment.createdAt,
editedAt: null,
version: versionOf(event),
});
// Recount instead of incrementing, so redelivery stays idempotent.
await this.upsertMaterializedIssueRepository.setCommentCount(
comment.issueId,
await this.findComments.countByIssue(comment.issueId),
);
await this.notifier.comment(comment.id, comment.issueId, event);
await this.notifier.issue(comment.issueId, event);
}
}