Skip to content
Open
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 @@ -236,5 +236,19 @@ describe('components/form-elements/draft-js-mention-selector/utils', () => {

expect(getFormattedCommentText(dummyEditorState)).toEqual(expected);
});

test.each`
input | expected
${' hello '} | ${'hello'}
${'\n\nhello\n\n'} | ${'hello'}
${'\thello\t'} | ${'hello'}
${' \t\nhello\n '} | ${'hello'}
${'hello world'} | ${'hello world'}
${' hello world '} | ${'hello world'}
`('should trim leading and trailing whitespace from "$input"', ({ input, expected }) => {
const dummyEditorState = EditorState.createWithContent(ContentState.createFromText(input));

expect(getFormattedCommentText(dummyEditorState)).toEqual({ text: expected, hasMention: false });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ function getFormattedCommentText(editorState: EditorState): { hasMention: boolea

// Concatenate the array of block strings with newlines
// (Each block represents a paragraph)
return { text: resultStringArr.join('\n'), hasMention };
return { text: resultStringArr.join('\n').trim(), hasMention };
}

export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,8 @@
button svg {
pointer-events: auto;
}

&-mentionEmpty {
padding: var(--bp-space-030) var(--bp-space-040);
}
}
54 changes: 43 additions & 11 deletions src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

import * as React from 'react';
import noop from 'lodash/noop';
import { useIntl } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';

import { ActivityFeed, useActivityFeedScroll } from '@box/activity-feed';
import { serializeMentionMarkup } from '@box/threaded-annotations';

import TaskModal from '../TaskModal';

import FeedItemRow from './FeedItemRow';
import { serializeEditorContent } from './helpers';
import { transformFeedItem } from './transformers';

import type { ActivityFeedV2Props, TransformedFeedItem, UserContact } from './types';
Expand All @@ -24,6 +24,7 @@ import type { TaskAssigneeCollection, TaskNew } from '../../../common/types/task
import { TASK_COMPLETION_RULE_ALL, TASK_EDIT_MODE_EDIT, TASK_TYPE_APPROVAL } from '../../../constants';

import commonMessages from '../../common/messages';
import draftJsMentionSelectorMessages from '../../../components/form-elements/draft-js-mention-selector/messages';
import messages from '../messages';

import './ActivityFeedV2.scss';
Expand Down Expand Up @@ -68,14 +69,16 @@ const ActivityFeedV2 = ({

const scrolledEntryIdRef = React.useRef<string | null>(null);
const hasScrolledToEndRef = React.useRef(false);
const knownIdsBeforePostRef = React.useRef<Set<string> | null>(null);

const fetchUsers = React.useCallback(
async (inputValue: string): Promise<UserContact[]> => {
if (!getMentionAsync) {
const trimmed = inputValue.trim();
if (!trimmed || !getMentionAsync) {
return [];
}
try {
const entries = await getMentionAsync(inputValue);
const entries = await getMentionAsync(trimmed);
return entries.map((c: Record<string, unknown>) => ({
email: (c.email as string) ?? (c.login as string) ?? '',
id: Number(c.id) || 0,
Expand Down Expand Up @@ -114,10 +117,20 @@ const ActivityFeedV2 = ({

const userSelectorProps = React.useMemo(
() => ({
allowEmptyQuery: true,
ariaRoleDescription: intl.formatMessage(messages.mentionUserSelectorRoleDescription),
fetchAvatarUrls,
fetchUsers,
loadingAriaLabel: intl.formatMessage(messages.mentionUserSelectorLoading),
renderEmpty: (value: string) => (
<div className="bcs-NewActivityFeed-mentionEmpty">
<FormattedMessage
{...(value.trim()
? draftJsMentionSelectorMessages.noUsersFound
: draftJsMentionSelectorMessages.startMention)}
/>
</div>
),
}),
[fetchAvatarUrls, fetchUsers, intl],
);
Expand Down Expand Up @@ -247,19 +260,38 @@ const ActivityFeedV2 = ({
}
}, [activeFeedEntryId, filteredItems, scrollHandle]);

// Scroll only to comments/annotations the current user authored after the post
// snapshot, so a concurrent push from another user doesn't hijack the viewport.
React.useEffect(() => {
const knownIds = knownIdsBeforePostRef.current;
if (!knownIds || !scrollHandle || !currentUserId) return;
const newItem = filteredItems.find(item => {
if (knownIds.has(item.id)) return false;
if (item.type !== 'comment' && item.type !== 'annotation') return false;
const author = item.messages[0]?.author;
return author ? String(author.id) === currentUserId : false;
});
if (!newItem) return;
if (scrollHandle.scrollTo(newItem.id)) {
knownIdsBeforePostRef.current = null;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}, [currentUserId, filteredItems, scrollHandle]);

const handleCommentPost = React.useCallback(
async (content: unknown) => {
if (!onCommentCreate) return;
let serialized;
const serialized = serializeEditorContent(content);
if (!serialized || !serialized.text) return;
try {
serialized = serializeMentionMarkup(content as Parameters<typeof serializeMentionMarkup>[0]);
} catch {
return;
const snapshot = new Set(filteredItems.map(item => item.id));
await onCommentCreate(serialized.text, serialized.hasMention);
knownIdsBeforePostRef.current = snapshot;
} catch (error) {
// eslint-disable-next-line no-console
console.error('ActivityFeedV2: failed to post comment', error);
}
if (!serialized.text.trim()) return;
onCommentCreate(serialized.text, serialized.hasMention);
},
[onCommentCreate],
[filteredItems, onCommentCreate],
);

return (
Expand Down
Loading
Loading