feat: what is new since my last visit, and turning a comment into a ticket

Opening a ticket notes the visit and answers the one before (issue_visits),
so the page marks others' comments and changes since. A comment can become
a ticket of its own, as a subtask or linked; it remembers where it went.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 21:05:44 +02:00
parent 429b52e6f5
commit 285bd5c454
50 changed files with 1953 additions and 79 deletions

View File

@@ -58,6 +58,7 @@ const commentFromDoc = (doc: CommentDoc): CommentReadModel => ({
resolved: doc.resolved ?? false,
resolvedBy: doc.resolvedBy ?? null,
resolvedAt: doc.resolvedAt ?? null,
spawned: doc.spawned ?? [],
});
const escapeRegex = (text: string) =>

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import {
IssueVisit,
IssueVisitsRepository,
} from '@task-stream/api/tracker/application';
import { Model } from 'mongoose';
import { ReadModels } from './read-model.schemas';
@Injectable()
export class MongoIssueVisitsRepository extends IssueVisitsRepository {
constructor(
@InjectModel(ReadModels.IssueVisit)
private readonly visits: Model<IssueVisit>,
) {
super();
}
async visit(visit: IssueVisit): Promise<string | null> {
// One round trip: the document as it was before this visit.
const before = await this.visits
.findOneAndUpdate(
{ userId: visit.userId, issueId: visit.issueId },
{ $set: { seenAt: visit.seenAt } },
{ upsert: true, new: false },
)
.lean<IssueVisit>();
return before?.seenAt ?? null;
}
}

View File

@@ -110,6 +110,7 @@ export const ReadModels = {
AlertIntegration: 'AlertIntegration',
AlertTrace: 'AlertTrace',
TreeFollow: 'TreeFollow',
IssueVisit: 'IssueVisit',
IssueReminder: 'IssueReminder',
MyDay: 'MyDay',
ReleaseNotesShare: 'ReleaseNotesShare',
@@ -373,6 +374,16 @@ export const TreeFollowSchema =
SchemaFactory.createForClass(TreeFollowDocument);
TreeFollowSchema.index({ userId: 1, issueId: 1 }, { unique: true });
@Schema({ ...LOOSE, collection: 'issue_visits' })
class IssueVisitDocument {
@Prop({ type: String }) userId!: string;
@Prop({ type: String }) issueId!: string;
@Prop({ type: String }) seenAt!: string;
}
export const IssueVisitSchema =
SchemaFactory.createForClass(IssueVisitDocument);
IssueVisitSchema.index({ userId: 1, issueId: 1 }, { unique: true });
@Schema({ ...LOOSE, collection: 'issue_reminders' })
class IssueReminderDocument {
@Prop({ type: String, required: true, unique: true }) id!: string;
@@ -511,6 +522,7 @@ export const READ_MODEL_SCHEMAS = [
{ name: ReadModels.AlertIntegration, schema: AlertIntegrationSchema },
{ name: ReadModels.AlertTrace, schema: AlertTraceSchema },
{ name: ReadModels.TreeFollow, schema: TreeFollowSchema },
{ name: ReadModels.IssueVisit, schema: IssueVisitSchema },
{ name: ReadModels.IssueReminder, schema: IssueReminderSchema },
{ name: ReadModels.MyDay, schema: MyDaySchema },
{ name: ReadModels.ReleaseNotesShare, schema: ReleaseNotesShareSchema },

View File

@@ -16,6 +16,7 @@ import {
PinsRepository,
AlertIntegrationsRepository,
TreeFollowsRepository,
IssueVisitsRepository,
IssueRemindersRepository,
MyDaysRepository,
ReleaseNoteSharesRepository,
@@ -87,6 +88,7 @@ import { MongoMyViewsRepository } from './mongo/my-views.repository';
import { MongoPinsRepository } from './mongo/pins.repository';
import { MongoAlertIntegrationsRepository } from './mongo/alert-integrations.repository';
import { MongoTreeFollowsRepository } from './mongo/tree-follows.repository';
import { MongoIssueVisitsRepository } from './mongo/issue-visits.repository';
import { MongoIssueRemindersRepository } from './mongo/issue-reminders.repository';
import { MongoMyDaysRepository } from './mongo/my-days.repository';
import { MongoReleaseNoteSharesRepository } from './mongo/release-note-shares.repository';
@@ -172,6 +174,7 @@ const PORTS = [
useClass: MongoAlertIntegrationsRepository,
},
{ provide: TreeFollowsRepository, useClass: MongoTreeFollowsRepository },
{ provide: IssueVisitsRepository, useClass: MongoIssueVisitsRepository },
{
provide: IssueRemindersRepository,
useClass: MongoIssueRemindersRepository,