diff --git a/packages/shared/src/types/issue.ts b/packages/shared/src/types/issue.ts index d55f8e15..9ef7f2d4 100644 --- a/packages/shared/src/types/issue.ts +++ b/packages/shared/src/types/issue.ts @@ -143,6 +143,7 @@ export interface Issue { mentionedProjects?: Project[]; myLastTouchAt?: Date | null; lastExternalCommentAt?: Date | null; + lastActivityAt?: Date | null; isUnreadForMe?: boolean; createdAt: Date; updatedAt: Date; diff --git a/ui/src/lib/inbox.test.ts b/ui/src/lib/inbox.test.ts index 2cdd8721..784e38cb 100644 --- a/ui/src/lib/inbox.test.ts +++ b/ui/src/lib/inbox.test.ts @@ -180,6 +180,7 @@ function makeIssue(id: string, isUnreadForMe: boolean): Issue { labelIds: [], myLastTouchAt: new Date("2026-03-11T00:00:00.000Z"), lastExternalCommentAt: new Date("2026-03-11T01:00:00.000Z"), + lastActivityAt: new Date("2026-03-11T01:00:00.000Z"), isUnreadForMe, }; } @@ -357,10 +358,10 @@ describe("inbox helpers", () => { it("mixes approvals into the inbox feed by most recent activity", () => { const newerIssue = makeIssue("1", true); - newerIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z"); + newerIssue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z"); const olderIssue = makeIssue("2", false); - olderIssue.lastExternalCommentAt = new Date("2026-03-11T02:00:00.000Z"); + olderIssue.lastActivityAt = new Date("2026-03-11T02:00:00.000Z"); const approval = makeApprovalWithTimestamps( "approval-between", @@ -385,19 +386,21 @@ describe("inbox helpers", () => { ]); }); - it("sorts touched issues by latest external comment timestamp", () => { - const newerIssue = makeIssue("1", true); - newerIssue.lastExternalCommentAt = new Date("2026-03-11T05:00:00.000Z"); + it("prefers canonical lastActivityAt over comment-only timestamps", () => { + const activityIssue = makeIssue("1", true); + activityIssue.lastExternalCommentAt = new Date("2026-03-11T01:00:00.000Z"); + activityIssue.lastActivityAt = new Date("2026-03-11T05:00:00.000Z"); - const olderIssue = makeIssue("2", true); - olderIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z"); + const commentIssue = makeIssue("2", true); + commentIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z"); + commentIssue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z"); - expect(getRecentTouchedIssues([olderIssue, newerIssue]).map((issue) => issue.id)).toEqual(["1", "2"]); + expect(getRecentTouchedIssues([commentIssue, activityIssue]).map((issue) => issue.id)).toEqual(["1", "2"]); }); it("mixes join requests into the inbox feed by most recent activity", () => { const issue = makeIssue("1", true); - issue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z"); + issue.lastActivityAt = new Date("2026-03-11T04:00:00.000Z"); const joinRequest = makeJoinRequest("join-1"); joinRequest.createdAt = new Date("2026-03-11T03:00:00.000Z"); @@ -482,7 +485,7 @@ describe("inbox helpers", () => { it("limits recent touched issues before unread badge counting", () => { const issues = Array.from({ length: RECENT_ISSUES_LIMIT + 5 }, (_, index) => { const issue = makeIssue(String(index + 1), index < 3); - issue.lastExternalCommentAt = new Date(Date.UTC(2026, 2, 31, 0, 0, 0, 0) - index * 60_000); + issue.lastActivityAt = new Date(Date.UTC(2026, 2, 31, 0, 0, 0, 0) - index * 60_000); return issue; }); diff --git a/ui/src/lib/inbox.ts b/ui/src/lib/inbox.ts index 647b6bea..9f26e998 100644 --- a/ui/src/lib/inbox.ts +++ b/ui/src/lib/inbox.ts @@ -217,6 +217,9 @@ export function normalizeTimestamp(value: string | Date | null | undefined): num } export function issueLastActivityTimestamp(issue: Issue): number { + const lastActivityAt = normalizeTimestamp(issue.lastActivityAt); + if (lastActivityAt > 0) return lastActivityAt; + const lastExternalCommentAt = normalizeTimestamp(issue.lastExternalCommentAt); if (lastExternalCommentAt > 0) return lastExternalCommentAt; diff --git a/ui/src/pages/Inbox.test.tsx b/ui/src/pages/Inbox.test.tsx index 10ac47d7..adb161c2 100644 --- a/ui/src/pages/Inbox.test.tsx +++ b/ui/src/pages/Inbox.test.tsx @@ -56,6 +56,7 @@ function createIssue(overrides: Partial = {}): Issue { labelIds: [], myLastTouchAt: null, lastExternalCommentAt: null, + lastActivityAt: new Date("2026-03-11T00:00:00.000Z"), isUnreadForMe: false, ...overrides, }; diff --git a/ui/src/pages/Inbox.tsx b/ui/src/pages/Inbox.tsx index d8bbccfa..b0898f40 100644 --- a/ui/src/pages/Inbox.tsx +++ b/ui/src/pages/Inbox.tsx @@ -214,7 +214,7 @@ export function InboxIssueMetaLeading({ } function issueActivityText(issue: Issue): string { - return `Updated ${timeAgo(issue.lastExternalCommentAt ?? issue.updatedAt)}`; + return `Updated ${timeAgo(issue.lastActivityAt ?? issue.lastExternalCommentAt ?? issue.updatedAt)}`; } function issueTrailingGridTemplate(columns: InboxIssueColumn[]): string { @@ -246,7 +246,7 @@ export function InboxIssueTrailingColumns({ assigneeName: string | null; currentUserId: string | null; }) { - const activityText = timeAgo(issue.lastExternalCommentAt ?? issue.updatedAt); + const activityText = timeAgo(issue.lastActivityAt ?? issue.lastExternalCommentAt ?? issue.updatedAt); const userLabel = formatAssigneeUserLabel(issue.assigneeUserId, currentUserId) ?? "User"; return (