Skip to content

feat/improve show more btn#155

Merged
matheusfillipe merged 3 commits into
mainfrom
feat/improve-show-more-btn
Mar 10, 2026
Merged

feat/improve show more btn#155
matheusfillipe merged 3 commits into
mainfrom
feat/improve-show-more-btn

Conversation

@matheusfillipe
Copy link
Copy Markdown
Contributor

@matheusfillipe matheusfillipe commented Mar 10, 2026

  • Make it discrete: Base improvement for expand messages button
  • animate on hover
CopyQ iaiAhQ CopyQ ILsgLK

added Small animations for hover buttons
In mobile chevron down is always shown
in mobile tapping a message also expands it
in desktop chevron down is only shown on hover together with other buttons with its own animation.

Fix: Expanding a message when in the bottom of the chat area scroll automatically scrolls down to keep it stuck to the bottom.

Summary by CodeRabbit

  • New Features

    • Added collapsible/expandable message functionality with toggle controls
    • Enabled tap-to-collapse for lengthy messages on touch devices
  • Improvements

    • Simplified emoji reaction interaction with single-button UI
    • Enhanced message action visibility animations and transitions
    • Improved scroll anchoring during message expansion

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

Refactors CollapsibleMessage to forwardRef with an imperative toggle, adds hoverOnly and onNeedsCollapsing props, replaces animation state with measurement/layout effects, anchors scroll during expansion, exposes tap-to-toggle behavior via SwipeableMessage for touch devices, updates CSS hover reveal rules, and adds tests.

Changes

Cohort / File(s) Summary
CollapsibleMessage component
src/components/message/CollapsibleMessage.tsx
Converted to forwardRef and exported CollapsibleMessageHandle with toggle(); added hoverOnly and onNeedsCollapsing props; replaced animation flags with measured collapsedMaxHeight and layout effects; added scroll-anchoring via ResizeObserver; updated render (rotating chevron, divider, hover visibility).
Touch/tap wiring & swipe handling
src/components/message/MessageItem.tsx, src/components/message/SwipeableMessage.tsx
Added onTap?: () => void to SwipeableMessageProps; precise tap detection (touch start/pos, movement, long-press gating); MessageItem wires a CollapsibleMessage ref and uses onNeedsCollapsing to toggle on tap for touch devices.
Action & reaction UI
src/components/message/MessageActions.tsx, src/components/message/MessageReactions.tsx
Replaced group-hover utility with message-actions-container class (removes implicit hover transition); changed reaction chip container to a single button per emoji and removed inner interactive remove-button.
Styling / animations
src/index.css
Removed legacy arrow-flip and truncation styles; added hover-reveal keyframes and selectors for .message-actions-container and .collapsible-expand-btn.
Tests
tests/components/CollapsibleMessage.test.tsx
New tests: mock computed styles / scrollHeight, assert toggle button presence based on 5-line threshold, validate maxHeight px values and tooltip text switching between "Read more" and "Show less".

Sequence Diagram

sequenceDiagram
    participant User
    participant SwipeableMessage
    participant CollapsibleMessage
    participant ResizeObserver

    User->>SwipeableMessage: touchstart (record target & pos)
    User->>SwipeableMessage: touchmove (detect movement)
    alt minimal movement & not long-press
        User->>SwipeableMessage: touchend
        SwipeableMessage->>SwipeableMessage: validate tap (not in button)
        SwipeableMessage->>CollapsibleMessage: onTap -> toggle() via ref
        CollapsibleMessage->>CollapsibleMessage: measure content, set collapsedMaxHeight
        CollapsibleMessage->>ResizeObserver: attach/respect observer if anchored
        ResizeObserver->>CollapsibleMessage: notify size changes to maintain anchor
        CollapsibleMessage->>User: animate expand/collapse (max-height change)
    else movement -> swipe/long-press
        SwipeableMessage->>User: handle swipe/long-press flow
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • ValwareIRC

Poem

🐰 I nudged the message, soft and light,
Tap gave a toggle, day or night,
Anchored the scroll so nothing leaps,
Buttons peek on hover, secrets it keeps,
Hooray — messages fold tidy and bright! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat/improve show more btn' directly relates to the main objective of improving the expand/show-more button visibility and behavior across desktop and mobile platforms.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/improve-show-more-btn

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.

@github-actions
Copy link
Copy Markdown

Pages Preview
Preview URL: https://feat-improve-show-more-btn.obsidianirc.pages.dev

Automated deployment preview for the PR in the Cloudflare Pages.

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: 2

🧹 Nitpick comments (2)
src/components/message/CollapsibleMessage.tsx (1)

79-79: useImperativeHandle missing dependency array may cause stale closure.

Without a dependency array, the handle is recreated on every render, which is wasteful. However, adding [toggleExpanded] would also recreate frequently since toggleExpanded isn't memoized. Consider wrapping with useCallback or using a ref-based approach.

🔧 Option 1: Add dependency array with memoized toggle
+  const toggleExpanded = useCallback(() => {
+    setIsExpanded((prev) => {
+      const willExpand = !prev;
+      if (willExpand && contentRef.current) {
+        // ... scroll anchoring logic
+      }
+      return willExpand;
+    });
+  }, []);

-  useImperativeHandle(ref, () => ({ toggle: toggleExpanded }));
+  useImperativeHandle(ref, () => ({ toggle: toggleExpanded }), [toggleExpanded]);
🔧 Option 2: Use functional update to avoid stale state
-  useImperativeHandle(ref, () => ({ toggle: toggleExpanded }));
+  useImperativeHandle(ref, () => ({
+    toggle: () => setIsExpanded((prev) => !prev),
+  }), []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/message/CollapsibleMessage.tsx` at line 79, The
useImperativeHandle call currently has no dependency array which recreates the
handle every render and risks stale closure; update the code so the exposed
toggle is stable by either memoizing the toggleExpanded function with
useCallback (e.g., wrap toggleExpanded in useCallback and then call
useImperativeHandle(ref, () => ({ toggle: toggleExpanded }), [toggleExpanded]))
or implement toggleExpanded as a stable function using a ref + functional state
update and pass an empty or appropriate dependency array to useImperativeHandle;
reference the useImperativeHandle and toggleExpanded symbols when making the
change.
tests/components/CollapsibleMessage.test.tsx (1)

17-28: Consider restoring scrollHeight property in afterEach to prevent test pollution.

The Object.defineProperty on HTMLElement.prototype.scrollHeight persists after the test completes because only mockScrollHeight is reset to 0, but the property descriptor remains. While configurable: true allows re-definition, it's safer to explicitly delete or restore the original descriptor.

🔧 Suggested cleanup
+  let originalScrollHeightDescriptor: PropertyDescriptor | undefined;
+
   beforeEach(() => {
     vi.spyOn(window, "getComputedStyle").mockImplementation(
       () =>
         ({
           lineHeight: `${LINE_HEIGHT}px`,
         }) as unknown as CSSStyleDeclaration,
     );
 
+    originalScrollHeightDescriptor = Object.getOwnPropertyDescriptor(
+      HTMLElement.prototype,
+      "scrollHeight",
+    );
     Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
       configurable: true,
       get() {
         return mockScrollHeight;
       },
     });
   });
 
   afterEach(() => {
     vi.restoreAllMocks();
     mockScrollHeight = 0;
+    if (originalScrollHeightDescriptor) {
+      Object.defineProperty(
+        HTMLElement.prototype,
+        "scrollHeight",
+        originalScrollHeightDescriptor,
+      );
+    } else {
+      delete (HTMLElement.prototype as any).scrollHeight;
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/components/CollapsibleMessage.test.tsx` around lines 17 - 28, The test
sets a custom descriptor on HTMLElement.prototype.scrollHeight but only resets
mockScrollHeight in afterEach, leaving the descriptor defined and causing test
pollution; capture the original descriptor (e.g., const originalScrollHeightDesc
= Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight")) before
calling Object.defineProperty in the setup and then restore it in afterEach
(either by re-defining the original descriptor if it exists or deleting the
property to fall back to the prototype chain) so that tests clean up the
prototype change; reference the existing mockScrollHeight and the prototype
modification to find where to save/restore the descriptor.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/message/CollapsibleMessage.tsx`:
- Around line 33-48: The useLayoutEffect in CollapsibleMessage currently only
depends on maxLines and onNeedsCollapsing so it won't re-measure when the
rendered message content changes; update the dependency array for the effect
(the useLayoutEffect that reads contentRef, computes lineHeight, fullHeight,
calls setContentHeight, setCollapsedMaxHeight, setNeedsCollapsing and
onNeedsCollapsing) to also include a signal for content changes (e.g., the
content prop or a stable contentHash/key) so the effect reruns and recalculates
collapsing whenever the message content updates.

In `@src/components/message/SwipeableMessage.tsx`:
- Around line 145-150: The tap handler currently blocks only native buttons
(closest("button")) which lets clicks on interactive non-button elements (e.g.,
the clickable divs in MessageReactions) also trigger onTap; update the guard in
the onTap logic (where wasLongPress, hasMovedRef.current, onTap and
touchStartTargetRef.current are used) to check for a wider set of interactive
targets (e.g., button, [role="button"], a[href], input, textarea, select, and
elements with a non-negative tabindex) so taps on reaction badges won’t fire
onTap, or alternatively mark the reaction divs in MessageReactions with
role="button" and keep the selector to include [role="button"] so the existing
guard covers them.

---

Nitpick comments:
In `@src/components/message/CollapsibleMessage.tsx`:
- Line 79: The useImperativeHandle call currently has no dependency array which
recreates the handle every render and risks stale closure; update the code so
the exposed toggle is stable by either memoizing the toggleExpanded function
with useCallback (e.g., wrap toggleExpanded in useCallback and then call
useImperativeHandle(ref, () => ({ toggle: toggleExpanded }), [toggleExpanded]))
or implement toggleExpanded as a stable function using a ref + functional state
update and pass an empty or appropriate dependency array to useImperativeHandle;
reference the useImperativeHandle and toggleExpanded symbols when making the
change.

In `@tests/components/CollapsibleMessage.test.tsx`:
- Around line 17-28: The test sets a custom descriptor on
HTMLElement.prototype.scrollHeight but only resets mockScrollHeight in
afterEach, leaving the descriptor defined and causing test pollution; capture
the original descriptor (e.g., const originalScrollHeightDesc =
Object.getOwnPropertyDescriptor(HTMLElement.prototype, "scrollHeight")) before
calling Object.defineProperty in the setup and then restore it in afterEach
(either by re-defining the original descriptor if it exists or deleting the
property to fall back to the prototype chain) so that tests clean up the
prototype change; reference the existing mockScrollHeight and the prototype
modification to find where to save/restore the descriptor.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4eb0a5b4-c635-4c14-86cd-4f7dcd90be4d

📥 Commits

Reviewing files that changed from the base of the PR and between 218e0d1 and a1978e8.

📒 Files selected for processing (6)
  • src/components/message/CollapsibleMessage.tsx
  • src/components/message/MessageActions.tsx
  • src/components/message/MessageItem.tsx
  • src/components/message/SwipeableMessage.tsx
  • src/index.css
  • tests/components/CollapsibleMessage.test.tsx

Comment thread src/components/message/CollapsibleMessage.tsx
Comment thread src/components/message/SwipeableMessage.tsx
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.

♻️ Duplicate comments (1)
src/components/message/SwipeableMessage.tsx (1)

35-37: ⚠️ Potential issue | 🟡 Minor

Broaden the tap guard beyond buttons and links.

children is arbitrary ReactNode, so limiting the exclusion to button, [role='button'], and a still lets taps on other interactive descendants toggle collapse/expand. This is the same root issue as the earlier tap-conflict comment, just with fewer remaining cases now that reaction chips are native buttons.

🛡️ Suggested fix
-  const touchStartTargetRef = useRef<EventTarget | null>(null);
+  const touchStartTargetRef = useRef<Element | null>(null);
...
-          touchStartTargetRef.current = e.target;
+          touchStartTargetRef.current =
+            e.target instanceof Element ? e.target : e.currentTarget;
...
-            const target = touchStartTargetRef.current as Element | null;
-            if (!target?.closest("button, [role='button'], a")) {
+            if (
+              !touchStartTargetRef.current?.closest(
+                "button, [role='button'], a[href], input, textarea, select, summary, [tabindex]:not([tabindex='-1'])",
+              )
+            ) {
               onTap();
             }

Also applies to: 121-125, 145-148

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/message/SwipeableMessage.tsx` around lines 35 - 37, The tap
guard currently only excludes button, [role='button'], and a elements, so update
the touch handlers (the functions using touchStartTargetRef, touchStartPosRef,
and hasMovedRef in SwipeableMessage.tsx) to detect any interactive descendant:
create a small helper (e.g., isInteractiveElement) that checks if the event
target is an HTMLElement and uses closest()/matches() for a broader selector
like "a, button, input, textarea, select, summary, [role], [tabindex]" or checks
for focusable/tabbable attributes, then early-return from the tap/click collapse
logic when that helper returns true; replace the current tag-specific checks in
all handlers that reference touchStartTargetRef (also update the corresponding
logic at the other occurrences you noted around lines ~121-125 and ~145-148).
🧹 Nitpick comments (1)
src/components/message/MessageReactions.tsx (1)

54-68: Expose the reaction chip as an actual toggle control.

Now that each chip is a single <button>, it should expose its pressed state explicitly. Right now the hidden × still contributes to the accessible name, while title is hover-only and not reliably announced on touch/screen readers.

♿ Suggested fix
           <button
             key={emoji}
             type="button"
             className="bg-discord-dark-300 hover:bg-discord-dark-200 text-white px-1.5 py-0.5 rounded text-xs flex items-center gap-1 transition-colors cursor-pointer group"
             title={`${emoji} ${reactionData.count} ${reactionData.count === 1 ? "reaction" : "reactions"} by ${reactionData.users.join(", ")}`}
+            aria-pressed={reactionData.currentUserReacted}
+            aria-label={`${reactionData.currentUserReacted ? "Remove" : "Add"} ${emoji} reaction. ${reactionData.count} ${reactionData.count === 1 ? "reaction" : "reactions"}.`}
             onClick={() =>
               onReactionClick(emoji, reactionData.currentUserReacted)
             }
           >
             <span>{emoji}</span>
             <span className="text-xs font-medium">{reactionData.count}</span>
             {reactionData.currentUserReacted && (
-              <span className="ml-1 text-red-400 opacity-0 group-hover:opacity-100 transition-opacity text-xs">
+              <span
+                aria-hidden="true"
+                className="ml-1 text-red-400 opacity-0 group-hover:opacity-100 transition-opacity text-xs"
+              >
                 ×
               </span>
             )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/message/MessageReactions.tsx` around lines 54 - 68, The
reaction chip should be an explicit toggle control: in the MessageReactions
component, set the button's aria-pressed to reactionData.currentUserReacted and
replace the hover-only title with an aria-label containing the emoji, count and
users (so screen readers get the same info); also mark the decorative × span as
aria-hidden="true" so it doesn't become part of the accessible name. Ensure
onReactionClick(emoji, reactionData.currentUserReacted) remains the click
handler and that the semantic button now correctly exposes pressed state and a
persistent accessible label.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/components/message/SwipeableMessage.tsx`:
- Around line 35-37: The tap guard currently only excludes button,
[role='button'], and a elements, so update the touch handlers (the functions
using touchStartTargetRef, touchStartPosRef, and hasMovedRef in
SwipeableMessage.tsx) to detect any interactive descendant: create a small
helper (e.g., isInteractiveElement) that checks if the event target is an
HTMLElement and uses closest()/matches() for a broader selector like "a, button,
input, textarea, select, summary, [role], [tabindex]" or checks for
focusable/tabbable attributes, then early-return from the tap/click collapse
logic when that helper returns true; replace the current tag-specific checks in
all handlers that reference touchStartTargetRef (also update the corresponding
logic at the other occurrences you noted around lines ~121-125 and ~145-148).

---

Nitpick comments:
In `@src/components/message/MessageReactions.tsx`:
- Around line 54-68: The reaction chip should be an explicit toggle control: in
the MessageReactions component, set the button's aria-pressed to
reactionData.currentUserReacted and replace the hover-only title with an
aria-label containing the emoji, count and users (so screen readers get the same
info); also mark the decorative × span as aria-hidden="true" so it doesn't
become part of the accessible name. Ensure onReactionClick(emoji,
reactionData.currentUserReacted) remains the click handler and that the semantic
button now correctly exposes pressed state and a persistent accessible label.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29550a54-7f29-495e-8e03-281757df5df6

📥 Commits

Reviewing files that changed from the base of the PR and between a1978e8 and 4b6259e.

📒 Files selected for processing (2)
  • src/components/message/MessageReactions.tsx
  • src/components/message/SwipeableMessage.tsx

@matheusfillipe matheusfillipe merged commit fbc9093 into main Mar 10, 2026
4 checks passed
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.

2 participants