feat: mentions and following tickets

Mention project members in comments with @, follow and unfollow tickets,
and get told when something changes on the tickets you follow. Commenting
on a ticket starts following it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 09:41:32 +02:00
parent efa06a0723
commit 88928b402f
49 changed files with 1318 additions and 41 deletions

View File

@@ -77,6 +77,7 @@ export const IssueSchema = new Schema<IssueDoc>(
release: { _id: false, id: String, name: String },
outwardLinks: [link],
inwardLinks: [link],
watchers: [userRef],
attachments: [
{
_id: false,
@@ -97,6 +98,7 @@ export const IssueSchema = new Schema<IssueDoc>(
IssueSchema.index({ projectId: 1, status: 1, updatedAt: -1 });
IssueSchema.index({ projectId: 1, statusCategory: 1 });
IssueSchema.index({ 'assignee.id': 1 });
IssueSchema.index({ 'watchers.id': 1 });
IssueSchema.index({ 'release.id': 1 });
IssueSchema.index({ title: 'text', description: 'text', key: 'text' });
@@ -179,5 +181,6 @@ export function issueFromDoc(doc: IssueDoc): IssueReadModel {
links: [...(outwardLinks ?? []), ...(inwardLinks ?? [])],
// Tickets projected before attachments existed have none.
attachments: rest.attachments ?? [],
watchers: rest.watchers ?? [],
};
}

View File

@@ -18,6 +18,7 @@ import {
ProjectReadModel,
ReleaseReadModel,
StatusCategory,
UserRefReadModel,
} from '@task-stream/api/tracker/domain';
import { Model } from 'mongoose';
import {
@@ -100,15 +101,35 @@ export class MongoUpsertMaterializedIssueRepository extends UpsertMaterializedIs
version,
...fields
}: Pick<IssueReadModel, 'id' | 'version'> &
Partial<Omit<IssueReadModel, 'links' | 'commentCount' | 'attachments'>>) {
Partial<
Omit<
IssueReadModel,
'links' | 'commentCount' | 'attachments' | 'watchers'
>
>) {
await upsertIfNewer(this.issues, id, version, fields, {
outwardLinks: [],
inwardLinks: [],
attachments: [],
watchers: [],
commentCount: 0,
});
}
async addWatcher(issueId: string, user: UserRefReadModel) {
await this.issues.updateOne(
{ _id: issueId, 'watchers.id': { $ne: user.id } },
{ $push: { watchers: user } },
);
}
async removeWatcher(issueId: string, userId: string) {
await this.issues.updateOne(
{ _id: issueId },
{ $pull: { watchers: { id: userId } } },
);
}
async addAttachment(issueId: string, attachment: AttachmentReadModel) {
await this.issues.updateOne(
{ _id: issueId, 'attachments.id': { $ne: attachment.id } },