Merge pull request #2638 from paperclipai/fix/inbox-last-activity-ordering
fix(inbox): prefer canonical last activity
This commit is contained in:
commit
422dd51a87
5 changed files with 20 additions and 12 deletions
|
|
@ -143,6 +143,7 @@ export interface Issue {
|
||||||
mentionedProjects?: Project[];
|
mentionedProjects?: Project[];
|
||||||
myLastTouchAt?: Date | null;
|
myLastTouchAt?: Date | null;
|
||||||
lastExternalCommentAt?: Date | null;
|
lastExternalCommentAt?: Date | null;
|
||||||
|
lastActivityAt?: Date | null;
|
||||||
isUnreadForMe?: boolean;
|
isUnreadForMe?: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,7 @@ function makeIssue(id: string, isUnreadForMe: boolean): Issue {
|
||||||
labelIds: [],
|
labelIds: [],
|
||||||
myLastTouchAt: new Date("2026-03-11T00:00:00.000Z"),
|
myLastTouchAt: new Date("2026-03-11T00:00:00.000Z"),
|
||||||
lastExternalCommentAt: new Date("2026-03-11T01:00:00.000Z"),
|
lastExternalCommentAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||||
|
lastActivityAt: new Date("2026-03-11T01:00:00.000Z"),
|
||||||
isUnreadForMe,
|
isUnreadForMe,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -357,10 +358,10 @@ describe("inbox helpers", () => {
|
||||||
|
|
||||||
it("mixes approvals into the inbox feed by most recent activity", () => {
|
it("mixes approvals into the inbox feed by most recent activity", () => {
|
||||||
const newerIssue = makeIssue("1", true);
|
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);
|
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(
|
const approval = makeApprovalWithTimestamps(
|
||||||
"approval-between",
|
"approval-between",
|
||||||
|
|
@ -385,19 +386,21 @@ describe("inbox helpers", () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sorts touched issues by latest external comment timestamp", () => {
|
it("prefers canonical lastActivityAt over comment-only timestamps", () => {
|
||||||
const newerIssue = makeIssue("1", true);
|
const activityIssue = makeIssue("1", true);
|
||||||
newerIssue.lastExternalCommentAt = new Date("2026-03-11T05:00:00.000Z");
|
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);
|
const commentIssue = makeIssue("2", true);
|
||||||
olderIssue.lastExternalCommentAt = new Date("2026-03-11T04:00:00.000Z");
|
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", () => {
|
it("mixes join requests into the inbox feed by most recent activity", () => {
|
||||||
const issue = makeIssue("1", true);
|
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");
|
const joinRequest = makeJoinRequest("join-1");
|
||||||
joinRequest.createdAt = new Date("2026-03-11T03:00:00.000Z");
|
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", () => {
|
it("limits recent touched issues before unread badge counting", () => {
|
||||||
const issues = Array.from({ length: RECENT_ISSUES_LIMIT + 5 }, (_, index) => {
|
const issues = Array.from({ length: RECENT_ISSUES_LIMIT + 5 }, (_, index) => {
|
||||||
const issue = makeIssue(String(index + 1), index < 3);
|
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;
|
return issue;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,9 @@ export function normalizeTimestamp(value: string | Date | null | undefined): num
|
||||||
}
|
}
|
||||||
|
|
||||||
export function issueLastActivityTimestamp(issue: Issue): number {
|
export function issueLastActivityTimestamp(issue: Issue): number {
|
||||||
|
const lastActivityAt = normalizeTimestamp(issue.lastActivityAt);
|
||||||
|
if (lastActivityAt > 0) return lastActivityAt;
|
||||||
|
|
||||||
const lastExternalCommentAt = normalizeTimestamp(issue.lastExternalCommentAt);
|
const lastExternalCommentAt = normalizeTimestamp(issue.lastExternalCommentAt);
|
||||||
if (lastExternalCommentAt > 0) return lastExternalCommentAt;
|
if (lastExternalCommentAt > 0) return lastExternalCommentAt;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ function createIssue(overrides: Partial<Issue> = {}): Issue {
|
||||||
labelIds: [],
|
labelIds: [],
|
||||||
myLastTouchAt: null,
|
myLastTouchAt: null,
|
||||||
lastExternalCommentAt: null,
|
lastExternalCommentAt: null,
|
||||||
|
lastActivityAt: new Date("2026-03-11T00:00:00.000Z"),
|
||||||
isUnreadForMe: false,
|
isUnreadForMe: false,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ export function InboxIssueMetaLeading({
|
||||||
}
|
}
|
||||||
|
|
||||||
function issueActivityText(issue: Issue): string {
|
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 {
|
function issueTrailingGridTemplate(columns: InboxIssueColumn[]): string {
|
||||||
|
|
@ -246,7 +246,7 @@ export function InboxIssueTrailingColumns({
|
||||||
assigneeName: string | null;
|
assigneeName: string | null;
|
||||||
currentUserId: 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";
|
const userLabel = formatAssigneeUserLabel(issue.assigneeUserId, currentUserId) ?? "User";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue