Skip to content

Touch devices fix, scroll to fist unread update#1788

Merged
prxt6529 merged 11 commits intomainfrom
touch-fix-scroll-to-unread-fix
Jan 22, 2026
Merged

Touch devices fix, scroll to fist unread update#1788
prxt6529 merged 11 commits intomainfrom
touch-fix-scroll-to-unread-fix

Conversation

@prxt6529
Copy link
Copy Markdown
Collaborator

@prxt6529 prxt6529 commented Jan 22, 2026

Summary by CodeRabbit

  • New Features

    • Floating button to jump to the first unread message; scroll-to-unread handler exposed.
    • Unread-divider provider added and alternate unread-aware drop list export for wrapped views.
  • Bug Fixes & Improvements

    • Cleaner unread-divider lifecycle (preserved across views, cleared after posting).
    • Navigation no longer exposes internal serial identifiers.
    • Prevents redundant reinitialization of message lists.
    • More robust, deterministic touch/device detection.
  • Tests

    • Test suites updated and stabilized with improved mocks and deterministic touch/unread scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

Signed-off-by: prxt6529 <prxt@6529.io>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 22, 2026

📝 Walkthrough

Walkthrough

Removes serialNo from navigation/state, adds UnreadDividerProvider and a scroll-to-unread FAB (WaveDropsScrollToUnreadButton) wired through drops list, refactors touch detection to guarded globalThis and event-driven listeners, and updates tests/mocks accordingly.

Changes

Cohort / File(s) Summary
Wave navigation changes
components/brain/left-sidebar/waves/BrainLeftSidebarWave.tsx, components/brain/left-sidebar/web/.../useWaveNavigation.ts
Removed serialNo from generated route hrefs and from setActiveWave options; divider remains for first-unread handling.
Unread-divider integration
components/brain/my-stream/MyStreamWaveChat.tsx, components/waves/CreateDrop.tsx
Wraps MyStream content in UnreadDividerProvider, adds a per-wave captured divider ref, and clears unread-divider in CreateDrop after submit via useUnreadDividerOptional.
Scroll-to-unread feature
components/waves/drops/WaveDropsScrollToUnreadButton.tsx, components/waves/drops/wave-drops-all/subcomponents/WaveDropsMessageListSection.tsx, components/waves/drops/wave-drops-all/subcomponents/WaveDropsContent.tsx, components/waves/drops/wave-drops-all/index.tsx
New WaveDropsScrollToUnreadButton component plus plumbing: new onScrollToUnread prop passed content→message-list, conditional FAB rendering, WaveDropsAllWithoutProvider export, and queue-target handler propagation.
Touch detection refactor
hooks/useIsTouchDevice.ts, hooks/useDeviceInfo.ts
Replace direct window/navigator use with guarded globalThis aliases, short-circuit on (pointer: fine) matchMedia, add one-time touchstart listener that flips state and self-removes, and persist one-time touch detection.
Tests — touch & device
__tests__/hooks/useIsTouchDevice.test.ts, __tests__/hooks/useDeviceInfo.test.ts
Reworked to spy on add/removeEventListener, capture/trigger touchstart handler, use per-test matchMedia mocks, and validate listener cleanup and state transitions.
Tests — component updates & mocks
__tests__/components/.../WaveDrop.test.tsx, __tests__/components/.../DropMobileMenuHandler.test.tsx, __tests__/components/.../WaveDropsAll.test.tsx, __tests__/components/brain/left-sidebar/waves/BrainLeftSidebarWave.test.tsx, __tests__/components/brain/my-stream/MyStreamWaveChat.test.tsx
Updated expectations to omit serialNo; added useIsTouchDevice mocks; added WaveDropsScrollToUnreadButton test mock; refactored capturedProps pattern and UnreadDividerProvider mock in MyStream tests.
Minor safety / refactors
components/nextGen/.../NextGenMintWidget.tsx
Guarded activeProof usage in effect with optional chaining to avoid non-null assertions.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WaveDropsContent
    participant WaveDropsMessageListSection
    participant WaveDropsScrollToUnreadButton
    participant UnreadDividerContext

    User->>WaveDropsContent: scrolls / views drops
    WaveDropsContent->>WaveDropsMessageListSection: pass scrollContainerRef, unreadDividerSerialNo, onScrollToUnread
    WaveDropsMessageListSection->>WaveDropsScrollToUnreadButton: render FAB when unread visible/above/below
    User->>WaveDropsScrollToUnreadButton: click
    WaveDropsScrollToUnreadButton->>WaveDropsMessageListSection: onScrollToUnread(serialNo)
    WaveDropsMessageListSection->>WaveDropsMessageListSection: scroll to "#drop-{serial}"
    WaveDropsMessageListSection->>UnreadDividerContext: update/persist divider state
Loading
sequenceDiagram
    participant Browser
    participant useIsTouchDevice
    participant matchMedia
    participant EventTarget as addEventListener
    participant User

    Browser->>useIsTouchDevice: mount hook
    useIsTouchDevice->>matchMedia: query "(pointer: fine)"
    matchMedia-->>useIsTouchDevice: matches? (true/false)
    alt matches true
        useIsTouchDevice-->>Browser: return false (no touch)
    else matches false
        useIsTouchDevice->>EventTarget: register one-time touchstart handler
        useIsTouchDevice-->>Browser: initial false
        User->>Browser: touchstart event
        EventTarget->>useIsTouchDevice: notify handler
        useIsTouchDevice->>useIsTouchDevice: set state true, remove handler
    end
    Browser->>useIsTouchDevice: unmount
    useIsTouchDevice->>EventTarget: ensure handler removed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Drop serialNo code revert #1619 — Removes/changes serialNo propagation in navigation and handlers; strong overlap with serialNo removals here.
  • Boosted drops #1706 — Modifies wave-drops-all and subcomponent wiring; related to the new onScrollToUnread plumbing.
  • Reactions dialog #1718 — Touch-detection hook and tests updates; overlaps useIsTouchDevice/useDeviceInfo refactors and test adjustments.

Suggested reviewers

  • simo6529
  • ragnep

"🐰
I hop through code with eager paws,
Removing serials, tending dividers' laws.
I watch for touch and scroll the unread,
A tiny button leaps where I lead —
Hooray for tidy routes and fewer flaws!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Touch devices fix, scroll to fist unread update' is partially related to the changeset. It references touch device handling (evident in multiple hook and test changes) and scroll-to-unread functionality (new WaveDropsScrollToUnreadButton component), but contains a typo ('fist' instead of 'first') and doesn't capture the broader scope including serialNo removal from navigation and test refactoring.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/waves/drops/wave-drops-all/index.tsx (1)

118-126: Stale divider state when dividerSerialNo changes for the same wave.

The guard on lines 119-121 prevents re-initialization when waveId hasn't changed, but dividerSerialNo is also in the dependency array. If a user navigates to the same wave with a different dividerSerialNo, the effect exits early without calling setUnreadDividerSerialNo(dividerSerialNo ?? null).

Suggested fix: track both waveId and dividerSerialNo
-  const initializedWaveRef = useRef<string | null>(null);
+  const initializedRef = useRef<{ waveId: string; dividerSerialNo: number | null | undefined } | null>(null);

   useEffect(() => {
-    if (initializedWaveRef.current === waveId) {
+    if (
+      initializedRef.current?.waveId === waveId &&
+      initializedRef.current?.dividerSerialNo === dividerSerialNo
+    ) {
       return;
     }
-    initializedWaveRef.current = waveId;
+    initializedRef.current = { waveId, dividerSerialNo };
     setVisibleLatestSerial(null);
     prevLatestSerialNoRef.current = null;
     setUnreadDividerSerialNo(dividerSerialNo ?? null);
   }, [waveId, dividerSerialNo, setUnreadDividerSerialNo]);
🤖 Fix all issues with AI agents
In `@components/nextGen/collections/collectionParts/mint/NextGenMintWidget.tsx`:
- Around line 333-340: Guard against empty or undefined proof objects returned
by findActiveProof: when computing activeProof from originalProofs, only call
setCurrentProof({ ...activeProof, proof: activeProof.proof }) if activeProof
exists and activeProof.proof is defined; otherwise do nothing (or reset to a
safe default). Update the useEffect that references findActiveProof,
originalProofs, setCurrentProof and currentProof to short‑circuit when
activeProof?.proof is falsy so state remains consistent.

In `@hooks/useDeviceInfo.ts`:
- Around line 37-46: The check for viewport width uses win.matchMedia without
guarding for lack of support—make widthMobile use optional chaining and a safe
fallback like the earlier hasFinePointer line; update the expression that sets
widthMobile (which references win.matchMedia("(max-width: 768px)").matches) to
something that first verifies win.matchMedia exists (e.g.,
win.matchMedia?.("(max-width: 768px)")?.matches ?? false) so it won't throw in
environments where matchMedia is undefined while keeping the same boolean
semantics alongside hasFinePointer and hasTouchScreen.
🧹 Nitpick comments (3)
__tests__/components/brain/left-sidebar/waves/BrainLeftSidebarWave.test.tsx (1)

88-93: LGTM! Test expectation correctly updated for divider-based navigation.

The change from serialNo to divider: null in the setActiveWave call expectation aligns with the PR's architectural shift.

Consider adding an additional test case to verify the setActiveWave behavior when firstUnreadDropSerialNo is present (non-null), ensuring the divider value is correctly passed on click:

💡 Suggested test case
it('passes divider to setActiveWave on click when firstUnreadDropSerialNo is present', async () => {
  const waveWithUnread = { ...baseWave, id: '3', firstUnreadDropSerialNo: 42 };
  render(<BrainLeftSidebarWave wave={waveWithUnread} onHover={onHover} />);
  const link = screen.getByRole('link');
  await userEvent.click(link);
  expect(setActiveWave).toHaveBeenCalledWith('3', { isDirectMessage: false, divider: 42 });
});
hooks/useIsTouchDevice.ts (1)

13-17: Redundant state update on line 15.

setIsTouchDevice(false) is unnecessary since the initial state is already false. This call triggers a no-op re-render cycle.

Suggested simplification
     const hasFinePointer = window.matchMedia?.("(pointer: fine)")?.matches;
     if (hasFinePointer) {
-      setIsTouchDevice(false);
       return;
     }
components/brain/my-stream/MyStreamWaveChat.tsx (1)

51-56: Render-time ref mutation is unconventional but acceptable here.

The pattern of mutating capturedDividerRef during render works for lazy initialization but can be confusing. An alternative is using a state initializer or useMemo. However, since this captures a value once per wave change and doesn't cause re-renders, it's acceptable.

Alternative using useMemo for clarity
-  const capturedDividerRef = useRef<{ waveId: string; serialNo: number | null } | null>(null);
-  
-  if (!capturedDividerRef.current || capturedDividerRef.current.waveId !== wave.id) {
-    capturedDividerRef.current = { waveId: wave.id, serialNo: firstUnreadSerialNo };
-  }
+  const capturedDivider = useMemo(
+    () => ({ waveId: wave.id, serialNo: firstUnreadSerialNo }),
+    // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally capture only on wave.id change
+    [wave.id]
+  );

Then use capturedDivider.serialNo instead of capturedDividerRef.current.serialNo.

Comment thread hooks/useDeviceInfo.ts Outdated
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@components/brain/my-stream/MyStreamWaveChat.tsx`:
- Around line 68-72: The capturedDividerRef is only initialized from
firstUnreadSerialNo and never updated from initialDropState, causing
dividerTarget to revert when URL params are cleared; add a useEffect that runs
when initialDropState or wave.id changes and, if
initialDropState?.dividerSerialNo is defined, set capturedDividerRef.current = {
waveId: wave.id, serialNo: initialDropState.dividerSerialNo } so the ref stays
synced with the actual divider value (ensure you reference capturedDividerRef,
initialDropState, wave.id, and firstUnreadSerialNo in the effect and guard
nulls); apply the same sync logic for the other occurrence noted around lines
104-108.
♻️ Duplicate comments (1)
hooks/useDeviceInfo.ts (1)

46-46: Guard matchMedia usage consistently.

Line 46 does not use optional chaining for matchMedia, while line 37 does. This could throw in test environments or browsers without matchMedia support.

-      const widthMobile = win.matchMedia("(max-width: 768px)").matches;
+      const widthMobile = win.matchMedia?.("(max-width: 768px)")?.matches ?? false;
🧹 Nitpick comments (2)
hooks/useDeviceInfo.ts (1)

64-92: Consider guarding globalThis in useEffect for consistency.

The getInfo function guards against undefined globalThis (line 19), but the useEffect block directly calls globalThis.addEventListener/removeEventListener without a similar guard. This inconsistency could cause issues in certain test environments.

Suggested guard
  useEffect(() => {
+   if (typeof globalThis === "undefined") {
+     return;
+   }
+
    const update = () =>
      setInfo((prev) => {
        const next = getInfo(touchDetectedRef.current);
        // ...
      });

    globalThis.addEventListener("resize", update);
    // ...
  }, [getInfo]);
__tests__/hooks/useIsTouchDevice.test.ts (1)

18-23: Consider cleaning up matchMedia property definition in afterEach.

Each test uses Object.defineProperty to set globalThis.matchMedia, but this isn't cleaned up in afterEach. While jest.restoreAllMocks() restores spies, it doesn't revert property definitions. This could cause test pollution if the order of tests changes or new tests are added.

Suggested cleanup
+ let originalMatchMedia: typeof globalThis.matchMedia | undefined;
+
  beforeEach(() => {
+   originalMatchMedia = globalThis.matchMedia;
    addEventListenerSpy = jest.spyOn(globalThis, "addEventListener").mockImplementation((event, handler) => {
      if (event === "touchstart") {
        touchStartHandler = handler as EventListener;
      }
    });
    removeEventListenerSpy = jest.spyOn(globalThis, "removeEventListener");
  });

  afterEach(() => {
    addEventListenerSpy.mockRestore();
    removeEventListenerSpy.mockRestore();
    touchStartHandler = null;
    jest.restoreAllMocks();
+   if (originalMatchMedia !== undefined) {
+     globalThis.matchMedia = originalMatchMedia;
+   } else {
+     delete (globalThis as any).matchMedia;
+   }
  });

Comment thread components/brain/my-stream/MyStreamWaveChat.tsx
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@hooks/useDeviceInfo.ts`:
- Around line 79-90: The hook currently calls
globalThis.addEventListener/removeEventListener directly for "resize" and
"touchstart" (handlers update and onceTouch, toggling touchDetectedRef.current),
which can throw in non-DOM runtimes; add a guard checking that
globalThis.addEventListener and globalThis.removeEventListener are functions
before registering or removing events (similar to the pattern in
useWebSocketHealth.ts), and only call globalThis.addEventListener("touchstart",
onceTouch, { passive: true }) if that API exists; ensure the cleanup return also
checks for removeEventListener before attempting to remove resize and touchstart
listeners.

Comment thread hooks/useDeviceInfo.ts Outdated
simo6529
simo6529 previously approved these changes Jan 22, 2026
Signed-off-by: prxt6529 <prxt@6529.io>
Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@hooks/useDeviceInfo.ts`:
- Around line 28-38: The current hasTouchScreen calculation in useDeviceInfo
incorrectly forces false when hasFinePointer is true; update the logic that
computes hasTouchScreen (referencing hasFinePointer,
touchDetected/touchDetectedRef, and nav.maxTouchPoints) so hybrid devices are
detected: treat hasTouchScreen as true if touchDetected (or
touchDetectedRef.current) is true OR navigator.maxTouchPoints > 0, regardless of
hasFinePointer, while still using matchMedia to detect fine pointers for
hasFinePointer itself.

Comment thread hooks/useDeviceInfo.ts Outdated
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
@sonarqubecloud
Copy link
Copy Markdown

@prxt6529 prxt6529 merged commit 32a60b1 into main Jan 22, 2026
7 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Feb 3, 2026
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.

3 participants