Skip to content

feat(activity-feed-v2): improve comment posting UX#4591

Open
jackiejou wants to merge 3 commits into
box:masterfrom
jackiejou:feat/activity-feed-v2-improvements
Open

feat(activity-feed-v2): improve comment posting UX#4591
jackiejou wants to merge 3 commits into
box:masterfrom
jackiejou:feat/activity-feed-v2-improvements

Conversation

@jackiejou
Copy link
Copy Markdown
Contributor

@jackiejou jackiejou commented May 26, 2026

Summary

Improves comment posting UX in the activity feed across V1 and V2.

V2 mention popover - Open the popover immediately on @ (parity with box-annotations and V1) and show V1-style copy:

  • "Mention someone to notify them" while the input is empty
  • "No users found" once the user types
  • No API round-trip on empty or whitespace-only queries

V1 + V2 trim - Strip leading and trailing whitespace from comment text before sending to the API. Inner whitespace is preserved.

V1 disable post button - The post button is now disabled when the editor is empty or whitespace-only, matching the form validation that already blocks submit. Visible feedback instead of a clickable button that does nothing.

V2 auto-scroll on post - After a successful post, scroll to the new item using a snapshot of feed item ids taken at post time, so a concurrent push from another user does not hijack the viewport. onCommentCreate is awaited so rejections are logged and do not arm the scroll for an unrelated update.

V1 mention selector is otherwise unchanged. The trim change to getFormattedCommentText is intentional and applies to all V1 comment surfaces.

Test plan

  • V2 sidebar: type @, popover opens with "Mention someone to notify them"; type a letter, popover shows matches or "No users found"
  • V2 sidebar: post a comment with surrounding whitespace, confirm the saved comment is trimmed
  • V2 sidebar: post a comment, confirm the feed scrolls to the new comment
  • V2 sidebar: with another tab posting, confirm the local user is scrolled only to their own post (not the stranger post)
  • V1 sidebar: open the comment form with an empty editor, confirm the Post button is visibly disabled
  • V1 sidebar: type only spaces, confirm Post stays disabled; type a real character, confirm it enables
  • V1 sidebar: post with surrounding whitespace, confirm the saved comment is trimmed
  • Unit tests: yarn test --watchAll=false --testPathPattern="(activity-feed-v2|comment-form|draft-js-mention-selector)" passes

Summary by CodeRabbit

  • New Features

    • Post button now disables when comment input is empty.
    • Mention selector supports empty queries and shows clearer empty-state messaging.
    • Feed reliably scrolls to newly created user comments without jumping on concurrent updates.
  • Bug Fixes

    • Serialized comment text is trimmed to remove leading/trailing whitespace before posting.
  • Style

    • Added padding for the mention-empty activity feed state.
  • Tests

    • Expanded coverage for mentions, posting, trimming, disabled post state, and scrolling behavior.

Review Change Stack

Mention popover (V2):
- Open the mention popover on @ before any character is typed
- Show the start prompt while empty and "No users found" once the user types
- Skip the API round-trip on empty or whitespace-only queries

Comment text (V1 + V2):
- Trim leading and trailing whitespace from comment text before sending
  to the API. Inner whitespace is preserved.

V1 post button:
- Disable the post button when the editor is empty or whitespace-only,
  matching the validation already enforced on submit.

V2 auto-scroll after posting:
- After a successful post, scroll to the new item using a snapshot of
  ids taken at post time, so a concurrent push from another user does
  not hijack the viewport.
- Await onCommentCreate so rejections are logged and do not arm the
  scroll for an unrelated update.
@jackiejou jackiejou requested review from a team as code owners May 26, 2026 23:34
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d77aa11-459a-4a81-acfa-306ef392f8fa

📥 Commits

Reviewing files that changed from the base of the PR and between d485c37 and 1d50228.

📒 Files selected for processing (2)
  • src/elements/content-sidebar/activity-feed/comment/__tests__/BaseComment.test.js
  • src/elements/content-sidebar/activity-feed/comment/__tests__/CreateReply.test.js

Walkthrough

Whitespace trimming added to comment serialization and utilities, post button is disabled for empty editor content, ActivityFeedV2 trims mention queries, adds localized empty-state rendering and styling, switches posting to trimmed serialization, snapshots feed items to scroll only to newly-created user posts, and expands tests accordingly.

Changes

Comment trimming and post button UX

Layer / File(s) Summary
Text trimming foundation
src/components/form-elements/draft-js-mention-selector/utils.js, src/components/form-elements/draft-js-mention-selector/__tests__/utils.test.js
getFormattedCommentText() now trims formatted text before returning; tests verify whitespace removal across spaces, tabs, newlines, and mixed variants.
Legacy comment form post button disabled state
src/elements/content-sidebar/activity-feed/comment-form/CommentForm.js, src/elements/content-sidebar/activity-feed/comment-form/CommentFormControls.js, src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js, src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentFormControls.test.js
CommentForm computes isPostDisabled from trimmed editor plain text and passes it to CommentFormControls; CommentFormControls accepts isDisabled and disables the Post button; tests assert enabled/disabled states and isolate focus-related EditorState behavior.
Activity Feed V2 serialization trimming
src/elements/content-sidebar/activity-feed-v2/helpers.ts, src/elements/content-sidebar/activity-feed-v2/__tests__/helpers.test.ts
serializeEditorContent trims the returned text; tests confirm whitespace normalization while preserving hasMention.
Activity Feed V2 mention selector improvements
src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx, src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss
Adds localized empty-state messages and imports, trims mention queries before calling getMentionAsync, enables allowEmptyQuery with a renderEmpty that shows “start mention” or “no users found,” and adds .bcs-NewActivityFeed-mentionEmpty padding.
Activity Feed V2 comment posting and scroll-to-new-post
src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx, src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx
Posting uses serializeEditorContent and requires non-empty trimmed text; after a successful post a snapshot of known item ids is stored, and a scroll effect scrolls to the first newly-created user item not in that snapshot; tests cover mention popover behavior, posting, and scroll scenarios including retry and concurrent posts.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Editor as ActivityFeedV2 Editor
  participant Query as Query Trim/Fetch
  participant Selector as User Selector
  participant Empty as renderEmpty UI
  User->>Editor: Focus mention input
  Editor->>Query: User types query text
  Query->>Query: Trim query
  alt Query is empty or whitespace
    Query->>Empty: Show "start mention" state
    Empty-->>Selector: Render localized message
  else Query is non-empty
    Query->>Selector: Call getMentionAsync(trimmed)
    Selector->>Selector: Fetch user results
    alt Results found
      Selector-->>Editor: Display user list
    else No results
      Selector->>Empty: Show "no users found" state
      Empty-->>Editor: Render localized message
    end
  end
Loading
sequenceDiagram
  participant User
  participant Editor as handleCommentPost
  participant Serialize as serializeEditorContent
  participant API as Comment API
  participant State as feedItems
  participant Scroll as scrollEffect
  User->>Editor: Click post button
  Editor->>Serialize: Serialize editor content
  Serialize-->>Editor: Return trimmed text + mention flag
  alt Text is empty or whitespace
    Editor->>User: Skip post (text empty)
  else Text is non-empty
    Editor->>API: Create comment
    API-->>State: feedItems updates
    Editor->>Editor: Store snapshot of known ids
    State->>Scroll: Detect new items
    Scroll->>Scroll: Find first item not in snapshot
    Scroll->>Scroll: Call scrollTo(foundItemId)
    alt scrollTo succeeds
      Scroll-->>User: Scroll to new user comment
    else scrollTo fails
      Scroll->>Scroll: Retry once
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • box/box-ui-elements#4460: Touches ActivityFeedV2 implementation and feature wiring, overlapping with mention/placeholder behavior changes.
  • box/box-ui-elements#4244: Modifies getFormattedCommentText behavior (timestamp/entity handling) related to the trimming changes here.
  • box/box-ui-elements#4557: Adjusts ActivityFeedV2 scroll/permalink behavior, related to the scroll-to-new-post updates.

Suggested labels

ready-to-merge

Suggested reviewers

  • ahorowitz123
  • kduncanhsu
  • jmcbgaston

Poem

🐰 I nibble spaces, tabs, and stray newlines bright,
Mentions tidy, posts sleep till content's right,
New feeds scroll true to the user's own light,
Tests hop along to keep behavior tight,
Hooray — clean text, and the UI feels light.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(activity-feed-v2): improve comment posting UX' accurately summarizes the main changes: UX improvements to comment posting across activity feed V1 and V2, including mention popover, trimming, post button state, and auto-scroll.
Description check ✅ Passed The description is comprehensive and well-structured, covering all major changes (V2 mention popover, V1+V2 trimming, V1 post button, V2 auto-scroll), implementation details, and a detailed test plan with manual and unit test instructions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js (1)

173-187: ⚡ Quick win

Add an explicit whitespace-only test for post disablement.

Current coverage checks empty and non-whitespace text, but not " \n\t" input, which is the trim-dependent edge this change relies on.

Proposed test addition
 describe('post button disabled state', () => {
     const findControls = wrapper => wrapper.find('CommentInputControls');

     test('should pass isDisabled=true to the controls when the editor is empty', () => {
         const wrapper = getWrapper({ isOpen: true });

         expect(findControls(wrapper).prop('isDisabled')).toBe(true);
     });

+    test('should pass isDisabled=true to the controls when the editor has only whitespace', () => {
+        const wrapper = getWrapper({ isOpen: true, tagged_message: '   \n\t   ' });
+
+        expect(findControls(wrapper).prop('isDisabled')).toBe(true);
+    });
+
     test('should pass isDisabled=false to the controls when the editor has non-whitespace content', () => {
         const wrapper = getWrapper({ isOpen: true, tagged_message: 'hello' });

         expect(findControls(wrapper).prop('isDisabled')).toBe(false);
     });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js`
around lines 173 - 187, The tests for post button disabled state are missing a
case for whitespace-only editor content; add a test inside the existing "post
button disabled state" describe block that uses getWrapper({ isOpen: true,
tagged_message: '   \n\t' }) and asserts
findControls(wrapper).prop('isDisabled') is true so the CommentInputControls
receives isDisabled=true for trim-only input. Use the same helper findControls
and test naming style as the other two tests to keep consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx`:
- Around line 265-271: The current logic uses filteredItems.find(item =>
!knownIds.has(item.id)) which can pick a concurrent insert; instead locate the
exact pending post before scrolling: use the pending-post identity (e.g. a
pendingPostId or pendingPostRef that tracks the just-submitted comment) to find
the matching item in filteredItems (e.g. const newItem = filteredItems.find(item
=> item.id === pendingPostIdRef.current)); only call
scrollHandle.scrollTo(newItem.id) if that exact match exists, and then clear
knownIdsBeforePostRef.current as before; if no pending-post id is available, do
not fall back to “first unknown id.”

---

Nitpick comments:
In
`@src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js`:
- Around line 173-187: The tests for post button disabled state are missing a
case for whitespace-only editor content; add a test inside the existing "post
button disabled state" describe block that uses getWrapper({ isOpen: true,
tagged_message: '   \n\t' }) and asserts
findControls(wrapper).prop('isDisabled') is true so the CommentInputControls
receives isDisabled=true for trim-only input. Use the same helper findControls
and test naming style as the other two tests to keep consistency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77e3b531-7d29-432c-b60f-b74dd432b757

📥 Commits

Reviewing files that changed from the base of the PR and between 49bffba and 7d400fc.

📒 Files selected for processing (11)
  • src/components/form-elements/draft-js-mention-selector/__tests__/utils.test.js
  • src/components/form-elements/draft-js-mention-selector/utils.js
  • src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.scss
  • src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx
  • src/elements/content-sidebar/activity-feed-v2/__tests__/ActivityFeedV2.test.tsx
  • src/elements/content-sidebar/activity-feed-v2/__tests__/helpers.test.ts
  • src/elements/content-sidebar/activity-feed-v2/helpers.ts
  • src/elements/content-sidebar/activity-feed/comment-form/CommentForm.js
  • src/elements/content-sidebar/activity-feed/comment-form/CommentFormControls.js
  • src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js
  • src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentFormControls.test.js

Comment on lines +265 to +271
React.useEffect(() => {
const knownIds = knownIdsBeforePostRef.current;
if (!knownIds || !scrollHandle) return;
const newItem = filteredItems.find(item => !knownIds.has(item.id));
if (!newItem) return;
if (scrollHandle.scrollTo(newItem.id)) {
knownIdsBeforePostRef.current = null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't scroll to the first unseen item here.

find(item => !knownIds.has(item.id)) still picks whichever concurrent insert sorts first. If another user's update lands ahead of the comment that was just posted, the viewport will jump to the wrong row again. Match against a stronger identity for the pending post before calling scrollTo instead of using “first unknown id” as the selector.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/elements/content-sidebar/activity-feed-v2/ActivityFeedV2.tsx` around
lines 265 - 271, The current logic uses filteredItems.find(item =>
!knownIds.has(item.id)) which can pick a concurrent insert; instead locate the
exact pending post before scrolling: use the pending-post identity (e.g. a
pendingPostId or pendingPostRef that tracks the just-submitted comment) to find
the matching item in filteredItems (e.g. const newItem = filteredItems.find(item
=> item.id === pendingPostIdRef.current)); only call
scrollHandle.scrollTo(newItem.id) if that exact match exists, and then clear
knownIdsBeforePostRef.current as before; if no pending-post id is available, do
not fall back to “first unknown id.”

commentEditorState is initialized in the constructor and is never
undefined in production. The optional chain was a workaround for a
test that mocked EditorState.moveFocusToEnd globally and leaked the
mock across tests. Restore the original after each test instead.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js (1)

185-199: ⚡ Quick win

Add an explicit whitespace-only disabled test case.

This suite should also assert isDisabled=true for tagged_message like ' \n\t ' to directly lock the trim-based behavior promised by this PR.

✅ Suggested test addition
 describe('post button disabled state', () => {
     const findControls = wrapper => wrapper.find('CommentInputControls');

     test('should pass isDisabled=true to the controls when the editor is empty', () => {
         const wrapper = getWrapper({ isOpen: true });

         expect(findControls(wrapper).prop('isDisabled')).toBe(true);
     });

+    test('should pass isDisabled=true to the controls when the editor has only whitespace', () => {
+        const wrapper = getWrapper({ isOpen: true, tagged_message: '   \n\t  ' });
+
+        expect(findControls(wrapper).prop('isDisabled')).toBe(true);
+    });
+
     test('should pass isDisabled=false to the controls when the editor has non-whitespace content', () => {
         const wrapper = getWrapper({ isOpen: true, tagged_message: 'hello' });

         expect(findControls(wrapper).prop('isDisabled')).toBe(false);
     });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js`
around lines 185 - 199, Add a new test in the "post button disabled state" suite
that uses getWrapper({ isOpen: true, tagged_message: '   \n\t  ' }) and asserts
findControls(wrapper).prop('isDisabled') === true to cover the whitespace-only
case; place it alongside the existing tests referencing CommentInputControls,
findControls, getWrapper, and the isDisabled prop so the trim-based behavior is
explicitly validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js`:
- Around line 185-199: Add a new test in the "post button disabled state" suite
that uses getWrapper({ isOpen: true, tagged_message: '   \n\t  ' }) and asserts
findControls(wrapper).prop('isDisabled') === true to cover the whitespace-only
case; place it alongside the existing tests referencing CommentInputControls,
findControls, getWrapper, and the isDisabled prop so the trim-based behavior is
explicitly validated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0c57716-fcb5-493e-89b4-9e2fbdc03e20

📥 Commits

Reviewing files that changed from the base of the PR and between 7d400fc and d485c37.

📒 Files selected for processing (2)
  • src/elements/content-sidebar/activity-feed/comment-form/CommentForm.js
  • src/elements/content-sidebar/activity-feed/comment-form/__tests__/CommentForm.test.js

The existing tests overwrote EditorState.moveFocusToEnd with a bare
jest.fn() that returns undefined. Once CommentForm reads the editor
state during render to compute isPostDisabled, the undefined return
crashed the component. Make the mock return its input so the editor
state stays valid while the spy still tracks calls.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant