diff --git a/src/adapters/__tests__/threadedAnnotationsAdapters-test.ts b/src/adapters/__tests__/threadedAnnotationsAdapters-test.ts index 08cc0a047..44607d2c4 100644 --- a/src/adapters/__tests__/threadedAnnotationsAdapters-test.ts +++ b/src/adapters/__tests__/threadedAnnotationsAdapters-test.ts @@ -194,6 +194,22 @@ describe('threadedAnnotationsAdapters', () => { expect(result[0].permissions.canEdit).toBe(true); }); + test.each([ + ['identical timestamps', '2026-01-01T00:00:00Z', undefined], + ['equivalent ISO formats with fractional precision', '2026-01-01T00:00:00.000Z', undefined], + ['unparseable modified_at', 'not-a-date', undefined], + ['edited later', '2026-02-01T00:00:00Z', new Date('2026-02-01T00:00:00Z').getTime()], + ])('should map updatedAt for %s', (_label, modifiedAt, expected) => { + const annotation: Annotation = { + ...baseAnnotation, + description: { message: 'Root' } as unknown as Reply, + modified_at: modifiedAt, + }; + const result = annotationToMessages(annotation); + + expect(result[0].updatedAt).toBe(expected); + }); + test('should include description and replies in order', () => { const annotation: Annotation = { ...baseAnnotation, @@ -217,7 +233,6 @@ describe('threadedAnnotationsAdapters', () => { expect(result[0].author.name).toBe('User'); expect(result[1].author.name).toBe('Other'); }); - }); describe('collaboratorToUserContact', () => { diff --git a/src/adapters/threadedAnnotationsAdapters.ts b/src/adapters/threadedAnnotationsAdapters.ts index 47e3a0434..0029056ab 100644 --- a/src/adapters/threadedAnnotationsAdapters.ts +++ b/src/adapters/threadedAnnotationsAdapters.ts @@ -89,6 +89,19 @@ export const replyToTextMessage = (reply: Reply): TextMessageTypeV2 => ({ }, }); +/** + * Returns the edit timestamp consumers use to render an edited indicator. + * Compares parsed instants, not raw strings, so equivalent ISO formats + * (Z vs +00:00, fractional precision) are treated as unedited. + */ +const toUpdatedAt = (createdAt: string, modifiedAt: string): number | undefined => { + const modifiedMs = new Date(modifiedAt).getTime(); + if (Number.isNaN(modifiedMs)) return undefined; + const createdMs = new Date(createdAt).getTime(); + if (modifiedMs === createdMs) return undefined; + return modifiedMs; +}; + // The root message shares the annotation's author and permissions; description // comes back sparse ({ message } only) from the list endpoint. const descriptionToTextMessage = (annotation: Annotation): TextMessageTypeV2 => ({ @@ -106,6 +119,7 @@ const descriptionToTextMessage = (annotation: Annotation): TextMessageTypeV2 => canReply: annotation.permissions?.can_reply ?? false, canResolve: annotation.permissions?.can_resolve ?? false, }, + updatedAt: toUpdatedAt(annotation.created_at, annotation.modified_at), }); /**