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>
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
} from '@task-stream/api/shared/domain';
|
||||
import { CommentAddedEvent } from '../events/comment-added.event';
|
||||
import { CommentDecisionMarkedEvent } from '../events/comment-decision-marked.event';
|
||||
import { CommentThreadResolvedEvent } from '../events/comment-thread-resolved.event';
|
||||
import { CommentDeletedEvent } from '../events/comment-deleted.event';
|
||||
import { CommentEditedEvent } from '../events/comment-edited.event';
|
||||
import { CommentReactedEvent } from '../events/comment-reacted.event';
|
||||
@@ -44,6 +45,8 @@ export class Comment extends VersionedAggregateRoot implements CommentProps {
|
||||
public reactions: Record<string, string[]> = {};
|
||||
/** What was decided on the ticket: shown above it and searchable. */
|
||||
public decision = false;
|
||||
/** A top-level comment whose conversation was settled. */
|
||||
public resolved = false;
|
||||
public deleted = false;
|
||||
public createdAt!: string;
|
||||
public editedAt: string | null = null;
|
||||
@@ -114,6 +117,28 @@ export class Comment extends VersionedAggregateRoot implements CommentProps {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Settles the conversation this comment opens (`on`) or reopens it;
|
||||
* repeating it changes nothing. Only a top-level comment opens one.
|
||||
*/
|
||||
resolveThread(actorId: string, on: boolean): void {
|
||||
this.assertAlive();
|
||||
if (this.replyTo)
|
||||
throw new BusinessRuleError(
|
||||
'Only a conversation is resolved, not a reply',
|
||||
);
|
||||
if (on === this.resolved) return;
|
||||
this.apply(
|
||||
new CommentThreadResolvedEvent(
|
||||
actorId,
|
||||
this.id,
|
||||
this.projectId,
|
||||
this.issueId,
|
||||
on,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** @param canModerate whether the actor manages the project (may delete others' comments). */
|
||||
delete(actorId: string, canModerate: boolean): void {
|
||||
this.assertAlive();
|
||||
@@ -146,6 +171,10 @@ export class Comment extends VersionedAggregateRoot implements CommentProps {
|
||||
this.decision = event.decision;
|
||||
}
|
||||
|
||||
onCommentThreadResolvedEvent(event: CommentThreadResolvedEvent): void {
|
||||
this.resolved = event.resolved;
|
||||
}
|
||||
|
||||
onCommentDeletedEvent(): void {
|
||||
this.deleted = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user