Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ const FeedItemRow = ({
return (
<ActivityFeed.List.ThreadedAnnotation
key={item.id}
annotationTarget={item.annotationTarget}
isAnnotations={false}
isEditDisabled={isDisabled || item.isResolved}
isResolved={item.isResolved}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { TaskNew } from '../../../../common/types/tasks';

import {
annotationTargetToBadge,
extractTimestampMarkup,
textToDocumentNode,
transformAnnotationToMessages,
transformAppActivityToProps,
Expand Down Expand Up @@ -675,6 +676,108 @@ describe('elements/content-sidebar/activity-feed-v2/transformers', () => {
});
});

describe('extractTimestampMarkup()', () => {
test('should return text unchanged when no timestamp markup is present', () => {
expect(extractTimestampMarkup('regular comment')).toEqual({ cleanText: 'regular comment' });
});

test('should return empty input unchanged', () => {
expect(extractTimestampMarkup('')).toEqual({ cleanText: '' });
});

test('should extract timestamp markup with versionId and return a frame badge target', () => {
const result = extractTimestampMarkup('#[timestamp:8055,versionId:2390295731268] wowo');
expect(result.cleanText).toBe('wowo');
expect(result.target).toEqual({ timestamp: '0:08', type: 'frame' });
});

test('should extract timestamp markup without versionId', () => {
const result = extractTimestampMarkup('#[timestamp:65000] hello');
expect(result.cleanText).toBe('hello');
expect(result.target).toEqual({ timestamp: '1:05', type: 'frame' });
});

test('should leave timestamp markup that does not anchor at the start of the message untouched', () => {
const result = extractTimestampMarkup('see #[timestamp:8055,versionId:1] this');
expect(result.cleanText).toBe('see #[timestamp:8055,versionId:1] this');
expect(result.target).toBeUndefined();
});

test('should return empty cleanText when the message is only timestamp markup', () => {
const result = extractTimestampMarkup('#[timestamp:8055,versionId:1]');
expect(result.cleanText).toBe('');
expect(result.target).toEqual({ timestamp: '0:08', type: 'frame' });
});

test('should leave malformed timestamp markup untouched', () => {
const result = extractTimestampMarkup('#[timestamp:abc] hello');
expect(result.cleanText).toBe('#[timestamp:abc] hello');
expect(result.target).toBeUndefined();
});

test('should drop the badge when the timestamp value exceeds Number.MAX_SAFE_INTEGER', () => {
const result = extractTimestampMarkup('#[timestamp:99999999999999999999] hello');
expect(result.cleanText).toBe('hello');
expect(result.target).toBeUndefined();
});
});

describe('transformCommentToMessages() with timestamp markup', () => {
test('should strip #[timestamp:...] markup from rendered message text', () => {
const comment = {
created_at: '2024-01-01T00:00:00Z',
created_by: { id: '1', name: 'User', type: 'user' },
id: 'c1',
message: '#[timestamp:8055,versionId:2390295731268] wowo',
modified_at: '2024-01-01T00:00:00Z',
permissions: { can_delete: true, can_edit: true, can_reply: true, can_resolve: true },
tagged_message: '',
type: 'comment',
};
const messages = transformCommentToMessages(comment as unknown as Comment);
expect(messages[0].message.content[0].content).toEqual([{ type: 'text', text: 'wowo' }]);
});
});

describe('transformFeedItem() with enhanced_comment timestamp markup', () => {
test('should populate annotationTarget with frame badge for a comment containing timestamp markup', () => {
const comment = {
created_at: '2024-01-01T00:00:00Z',
created_by: { id: '1', name: 'User', type: 'user' },
id: 'c1',
message: '#[timestamp:8055,versionId:2390295731268] wowo',
modified_at: '2024-01-01T00:00:00Z',
permissions: {},
status: 'open',
tagged_message: '',
type: 'comment',
};
const result = transformFeedItem(comment as unknown as FeedItem);
expect(result!.type).toBe('comment');
if (result!.type === 'comment') {
expect(result.annotationTarget).toEqual({ timestamp: '0:08', type: 'frame' });
}
});

test('should leave annotationTarget undefined for a regular comment without timestamp markup', () => {
const comment = {
created_at: '2024-01-01T00:00:00Z',
created_by: { id: '1', name: 'User', type: 'user' },
id: 'c1',
message: 'regular comment',
modified_at: '2024-01-01T00:00:00Z',
permissions: {},
status: 'open',
tagged_message: '',
type: 'comment',
};
const result = transformFeedItem(comment as unknown as FeedItem);
if (result!.type === 'comment') {
expect(result.annotationTarget).toBeUndefined();
}
});
});

describe('annotationTargetToBadge()', () => {
test('should return undefined when target is missing', () => {
expect(annotationTargetToBadge()).toBeUndefined();
Expand Down
47 changes: 37 additions & 10 deletions src/elements/content-sidebar/activity-feed-v2/transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,24 @@ import {
} from '../../../constants';

const MENTION_REGEX = /@\[(\d+):([^\]]+)\]/g;
const TIMESTAMP_MARKUP_REGEX = /^#\[timestamp:(\d+)(?:,versionId:\d+)?\]\s*/;

export const extractTimestampMarkup = (text: string): { cleanText: string; target?: AnnotationBadgeTargetType } => {
if (!text) return { cleanText: '' };
const match = text.match(TIMESTAMP_MARKUP_REGEX);
if (!match) return { cleanText: text };
Comment thread
jackiejou marked this conversation as resolved.

const [fullMatch, timestampMs] = match;
const cleanText = text.slice(fullMatch.length);
const ms = Number(timestampMs);
if (!Number.isSafeInteger(ms) || ms < 0) return { cleanText };

const target: AnnotationBadgeTargetType = {
timestamp: convertMillisecondsToTimestamp(ms),
type: AnnotationBadgeType.Frame,
};
return { cleanText, target };
};

const parseLine = (line: string, authorId: string): (MentionNode | TextNode)[] => {
const nodes: (MentionNode | TextNode)[] = [];
Expand Down Expand Up @@ -123,14 +141,18 @@ const toPermissions = (
canResolve: permissions?.can_resolve ?? false,
});

const commentToTextMessage = (comment: Comment): TextMessageType => ({
author: toUserAuthor(comment.created_by),
createdAt: toUnixMs(comment.created_at) ?? 0,
id: comment.id,
message: textToDocumentNode(comment.tagged_message || comment.message || '', comment.created_by?.id ?? ''),
permissions: toPermissions(comment.permissions),
updatedAt: toUpdatedAt(comment.created_at, comment.modified_at),
});
const commentToTextMessage = (comment: Comment, prebuiltCleanText?: string): TextMessageType => {
Comment thread
jackiejou marked this conversation as resolved.
const cleanText =
prebuiltCleanText ?? extractTimestampMarkup(comment.tagged_message || comment.message || '').cleanText;
return {
author: toUserAuthor(comment.created_by),
createdAt: toUnixMs(comment.created_at) ?? 0,
id: comment.id,
message: textToDocumentNode(cleanText, comment.created_by?.id ?? ''),
permissions: toPermissions(comment.permissions),
updatedAt: toUpdatedAt(comment.created_at, comment.modified_at),
};
};

export const transformCommentToMessages = (comment: Comment): TextMessageType[] => {
const root = commentToTextMessage(comment);
Expand Down Expand Up @@ -267,11 +289,16 @@ export const transformFeedItem = (item: FeedItem, currentUserId?: string): Trans
case FEED_ITEM_TYPE_COMMENT: {
const comment = item as unknown as Comment;
const commentIsResolved = comment.status === 'resolved';
const rawText = comment.tagged_message || comment.message || '';
const { cleanText, target: annotationTarget } = extractTimestampMarkup(rawText);
const root = commentToTextMessage(comment, cleanText);
const replies = (comment.replies ?? []).map(reply => commentToTextMessage(reply));
return {
annotationTarget,
id: comment.id,
isResolved: commentIsResolved,
messages: transformCommentToMessages(comment),
originalText: comment.tagged_message || comment.message || '',
messages: [root, ...replies],
originalText: rawText,
permissions: comment.permissions ?? {},
resolvedAt: commentIsResolved ? toUnixMs(comment.resolution?.resolved_at) : undefined,
resolvedBy: commentIsResolved ? comment.resolution?.resolved_by?.name : undefined,
Expand Down
3 changes: 2 additions & 1 deletion src/elements/content-sidebar/activity-feed-v2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import type { AppActivityItemProps, TaskItemProps, VersionItemProps } from '@box/activity-feed';
import type { TextMessageTypeV2 as TextMessageType } from '@box/threaded-annotations';
import type { AnnotationBadgeTargetType, TextMessageTypeV2 as TextMessageType } from '@box/threaded-annotations';

import type { Annotation, AnnotationPermission } from '../../../common/types/annotations';
import type { BoxCommentPermission, CommentFeedItemType, FeedItems, FeedItemStatus } from '../../../common/types/feed';
Expand Down Expand Up @@ -46,6 +46,7 @@ type ResolvedInfo = {
};

export type TransformedCommentItem = {
annotationTarget?: AnnotationBadgeTargetType;
id: string;
messages: TextMessageType[];
originalText: string;
Expand Down
Loading