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:
@@ -17,7 +17,11 @@ import {
|
||||
import { ApiCommandErrors } from '@task-stream/api/shared/presenters';
|
||||
import { CommentsService } from '@task-stream/api/tracker/application';
|
||||
import { REACTIONS } from '@task-stream/api/tracker/domain';
|
||||
import { CommentBodyDto, ReactToCommentDto } from './dto/requests.dto';
|
||||
import {
|
||||
CommentBodyDto,
|
||||
ReactToCommentDto,
|
||||
ResolveThreadDto,
|
||||
} from './dto/requests.dto';
|
||||
import { CommandResultDto } from './dto/responses.dto';
|
||||
|
||||
/** REST adapter of the comment use cases (listing and adding live under /issues). */
|
||||
@@ -78,6 +82,35 @@ export class CommentsController {
|
||||
return this.comments.markDecision(me.id, commentId, false);
|
||||
}
|
||||
|
||||
@Put(':commentId/resolution')
|
||||
@ApiOperation({
|
||||
summary: 'Settle a conversation (a top-level comment and its replies)',
|
||||
description:
|
||||
'Whoever can change the ticket. A conclusion is added as a reply marked as a decision. The web folds a settled conversation.',
|
||||
})
|
||||
@ApiOkResponse({ type: CommandResultDto })
|
||||
@ApiCommandErrors()
|
||||
resolveThread(
|
||||
@CurrentUser() me: User,
|
||||
@Param('commentId') commentId: string,
|
||||
@Body() body: ResolveThreadDto,
|
||||
) {
|
||||
return this.comments.resolveThread(
|
||||
me.id,
|
||||
commentId,
|
||||
true,
|
||||
body.conclusion ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':commentId/resolution')
|
||||
@ApiOperation({ summary: 'Reopen a conversation' })
|
||||
@ApiOkResponse({ type: CommandResultDto })
|
||||
@ApiCommandErrors()
|
||||
reopenThread(@CurrentUser() me: User, @Param('commentId') commentId: string) {
|
||||
return this.comments.resolveThread(me.id, commentId, false);
|
||||
}
|
||||
|
||||
@Delete(':commentId')
|
||||
@ApiOperation({ summary: 'Delete a comment (author or project admin)' })
|
||||
@ApiOkResponse({ type: CommandResultDto })
|
||||
|
||||
Reference in New Issue
Block a user