Skip to content

Add scroll-to-first-unread pill to the message view#6409

Open
andremion wants to merge 13 commits intodevelopfrom
feat/scroll-to-first-unread-indicator
Open

Add scroll-to-first-unread pill to the message view#6409
andremion wants to merge 13 commits intodevelopfrom
feat/scroll-to-first-unread-indicator

Conversation

@andremion
Copy link
Copy Markdown
Contributor

@andremion andremion commented May 4, 2026

Goal

When a channel is opened with pre-existing unread messages and the unread boundary sits above the viewport, the message view did not surface any way to jump to it. The Figma "Message View — Opened With Unread Messages (Unread Above)" spec defines a floating pill at the top of the list that shows the unread count with an upward arrow, scrolls to the first unread on tap, and dismisses with a close icon.

This PR adds that pill, surfaces the unread label on MessageListState so Compose UIs can observe it, and fixes a related stickiness gap in disableUnreadLabelButton that would otherwise let the pill reappear after the auto-mark-read fires post-scroll.

As per https://www.figma.com/design/Us73erK1xFNcB5EH3hyq6Y/Chat-SDK-Design-System?node-id=19496-477662&m=dev

Implementation

  • State. MessageListState.unreadLabel mirrors MessageListController.unreadLabelState, same pattern as unreadCount. UnreadLabel is aliased into the state package.
  • Controller / VM. disableUnreadLabelButton is now sticky against subsequent read-state updates (suppression is per-controller, resets on channel re-entry) and exposed as a pass-through on MessageListViewModel.
  • UI. New ScrollToFirstUnreadButton ChatComponentFactory slot — pill with up arrow, count, vertical divider, and close icon. Tap label = scroll, tap X = dismiss. The internal Modifier.clickable helper gains a defaulted role parameter so the pill rides the SDK's standard ripple.
  • Wiring. Pill renders in DefaultMessagesHelperContent at Alignment.TopCenter. Visibility matches the unread separator by its LazyListItemInfo.key (raw index drifts when non-message items precede itemsIndexed). Callbacks are trailing defaulted parameters on the public MessageList overloads.
  • Tests. Standalone and wired snapshots, controller regression for sticky dismissal, VM action test.

🎨 UI Changes

Before After
Screen_recording_20260504_165209.webm
Screen_recording_20260504_165107.webm

Testing

  1. Build and run the Compose sample app on this branch.
  2. Sign in as a user who has a channel with several unread messages where the first unread sits above the initial landing position (typical case: someone sent many messages while you were away).
  3. Open that channel.
    • Expected: the pill appears at the top center, showing "↑ N unread" with a close icon.
  4. Tap the pill body (the label area, not the X).
    • Expected: the list animates or loads to the first unread message, and the pill goes away and stays away for the rest of the session.
  5. Re-open the channel from the channel list, then tap the close (X) icon on the pill.
    • Expected: the pill goes away without scrolling, and stays away for the rest of the session.
  6. Leave the channel, then re-open it.
    • Expected: if there are still pre-existing unreads with the boundary above the viewport, the pill returns.
  7. Open a thread.
    • Expected: the pill never appears in thread mode.

andremion added 11 commits May 4, 2026 14:42
Adds an UnreadLabel field on MessageListState so UI layers can react to the
sticky unread-boundary signal without depending on the live unreadCount, which
collapses to 0 once the SDK auto-marks the latest message as read on chat
open. The new field mirrors MessageListController.unreadLabelState via a
collector.

Declares UnreadLabel as a typealias in the state package
(state/messages/list/UnreadLabel.kt) pointing at MessageListController's
nested data class, so MessageListState references the type from its own
package without reaching into feature/messages/list. Both names resolve to
the same JVM class — existing consumers of MessageListController.UnreadLabel
keep compiling.
Lets the UI dismiss the floating unread-label button (e.g. from a close
affordance on the scroll-to-first-unread pill) without affecting the inline
unread separator.
Introduces the floating pill primitive used to jump to the first unread message
when the unread boundary sits outside the viewport. The pill exposes two
distinct interactions: tapping the label area scrolls to the boundary, while
tapping the trailing close icon dismisses the pill without scrolling.

Adds the ScrollToFirstUnreadButtonParams holder, the ChatComponentFactory slot,
the default composable, and the supporting string resources. The slot is
unwired in this commit; the wiring lands in the follow-up.
Renders the floating pill inside DefaultMessagesHelperContent using the sticky
unread label exposed on MessageListState. Visibility derives from
unreadLabel.buttonVisibility plus whether the inline unread separator is in
the visible viewport, so the pill stays correct even after the SDK
auto-marks the latest visible message as read on chat open.

Tap on the pill body invokes MessageListViewModel.scrollToFirstUnreadMessage
(loading the boundary if needed); tap on the close affordance invokes
MessageListViewModel.disableUnreadLabelButton. Both actions are exposed as
trailing defaulted callbacks on the public MessageList overloads so that
state-only consumers can opt in.

The visibility derivedState observes only lazyListState.layoutInfo (a State)
and re-keys on unreadSeparatorIndex; buttonVisibility is read on each
recomposition from the parameter so it never relies on a stale capture.
disableUnreadLabelButton() flipped buttonVisibility on the current label, but
observeUnreadLabelState recomputes the label whenever lastReadMessageId
changes and the calculator restored buttonVisibility=true. After
scrollToFirstUnreadMessage, the auto-mark-read on the now-visible boundary
triggered exactly that recomputation and the pill returned.

Also tryEmit(false) on showUnreadButtonState so the suppression flows through
the calculator on subsequent recomputes. The suppression is per-controller, so
it resets when the user leaves and re-enters the channel — matching the Figma
spec.

A regression test in MessageListControllerTests locks the new contract: after
disableUnreadLabelButton, pushing a new lastReadMessageId on channelState.read
must not flip buttonVisibility back to true.
Cleanup-only follow-up to the pill UI:
- Replace the hardcoded 1.dp stroke with StreamTokens.borderStrokeSubtle so
  the border respects the design-system token.
- Pass role=Role.Button directly to clickable instead of layering a separate
  semantics modifier; the role lands on the same semantics node either way.
- Split each four-edge padding into a vertical+horizontal pair for symmetry
  with the rest of the codebase's padding patterns.

No behaviour or API change.
Expands the KDoc on MessageListController.unreadLabelState to describe its
stickiness, the dismissal contract, and to point Compose-style consumers at
MessageListState.unreadLabel (the aggregated mirror) so granular subscribers
and UI consumers each pick the better entry point.
The internal Modifier.clickable helper centralises the SDK's clickable surface
(explicit Material 3 ripple, optional bounded clipping). The pill was
side-stepping it by calling foundation's clickable directly, drifting from the
codebase convention. Add a defaulted role parameter to the helper so the pill
can ride the shared ripple while still announcing Role.Button.

Two pre-existing trailing-lambda call sites (MediaGalleryPage,
MediaGalleryPhotosMenu) import both the foundation and internal clickables;
the new role parameter makes the simplified ".clickable { … }" form
ambiguous between the two. Disambiguate by passing bounded = true (foundation
has no bounded), which preserves the existing ripple-bearing behaviour.
Mirrors the Figma "Message View — Opened With Unread Messages" mock, which
shows a thin vertical rule between the unread count and the dismiss affordance.
Reuses the same border token already applied to the pill outline so the inner
rule and the outline read as a single design.
Three Paparazzi snapshots covering the unread-count surface area: 1, 9, and
999. Each renders both light and dark variants via snapshotWithDarkMode so the
divider, border, and ripple-bearing label area are all visible against the
themed background.
Two related fixes on the pill composable:

- Bound the inner Row to Modifier.height(IntrinsicSize.Min). VerticalDivider
  uses fillMaxHeight() internally, which was unbounded inside the Surface
  preview and stretched the pill across the rendered surface. With
  IntrinsicSize.Min, the divider's height resolves to the tallest
  non-flexible child (the label row or the close icon).

- Move the Stream_ScrollToFirstUnreadButton testTag from the Surface to the
  inner clickable Row. The Surface is not interactive, so a UI test that
  finds the node by tag and performs a click would land on a non-clickable
  node. With the tag on the click target, finding by tag and clicking now
  invokes the scroll handler, mirroring the dismiss tag on the X icon.
@andremion andremion added the pr:new-feature New feature label May 4, 2026
@andremion
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled.

🎉 Great job! This PR is ready for review.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Walkthrough

The PR introduces a "scroll to first unread" button UI element with supporting state management. It adds a new ScrollToFirstUnreadButton composable, extends MessageList and Messages to accept unread-related callbacks, updates MessageListController and MessageListState to track and propagate unread label state, adds a disableUnreadLabelButton() API to MessageListViewModel, and includes localized string resources and snapshot tests.

Changes

Scroll to First Unread Feature

Layer / File(s) Summary
State Shape
stream-chat-android-ui-common/.../state/messages/list/MessageListState.kt, stream-chat-android-ui-common/.../state/messages/list/UnreadLabel.kt, stream-chat-android-compose/.../theme/ChatComponentFactoryParams.kt
Add unreadLabel: UnreadLabel? property to MessageListState; introduce UnreadLabel typealias; add ScrollToFirstUnreadButtonParams data class with visibility, unread count, callbacks, and modifier.
Business Logic / Controller
stream-chat-android-ui-common/.../feature/messages/list/MessageListController.kt, stream-chat-android-compose/.../viewmodel/messages/MessageListViewModel.kt
Update MessageListController to propagate unreadLabelState into messageListState via updateUnreadLabel helper; update disableUnreadLabelButton() to suppress button visibility before mutating label state; add public disableUnreadLabelButton() method to MessageListViewModel that delegates to controller.
UI Component
stream-chat-android-compose/.../components/messages/ScrollToFirstUnreadButton.kt
Create new ScrollToFirstUnreadButton composable with pill-shaped surface containing clickable label (arrow + pluralized unread count) and separate dismiss button (X icon); add preview and no-arg overload.
Component Factory Integration
stream-chat-android-compose/.../theme/ChatComponentFactory.kt
Add ScrollToFirstUnreadButton composable hook to ChatComponentFactory with conditional rendering via FadingVisibility; wire new unread callbacks through MessageListHelperContent.
Composable Wiring
stream-chat-android-compose/.../messages/list/MessageList.kt, stream-chat-android-compose/.../messages/list/Messages.kt
Add onScrollToFirstUnreadClicked and onDismissUnreadLabel parameters to both ViewModel- and state-backed MessageList overloads; add matching callbacks to Messages and DefaultMessagesHelperContent; compute unread separator visibility and conditionally render button with isScrollToFirstUnreadVisible helper.
Utilities & Semantics
stream-chat-android-compose/.../util/ModifierUtils.kt
Extend Modifier.clickable with optional role: Role? parameter for accessibility semantics.
API Surface
stream-chat-android-compose/api/stream-chat-android-compose.api, stream-chat-android-ui-common/api/stream-chat-android-ui-common.api
Update public API signatures for MessageList, ChatComponentFactory, MessageListHelperContentParams, MessageListState, and MessageListViewModel to reflect new unread-related parameters and methods.
Resources & Tests
stream-chat-android-compose/.../res/values/strings.xml, stream-chat-android-compose/.../components/messages/ScrollToFirstUnreadButtonTest.kt, stream-chat-android-ui-common/.../feature/messages/list/MessageListControllerTests.kt
Add localized strings for button label, dismiss action, and pluralized unread count; add Paparazzi snapshot test for button rendering; add controller test verifying disableUnreadLabelButton() keeps visibility suppressed after read-state updates.
Gallery Item Modifications
stream-chat-android-compose/.../attachments/preview/internal/MediaGalleryPage.kt, stream-chat-android-compose/.../attachments/preview/internal/MediaGalleryPhotosMenu.kt
Update clickable modifiers to use bounded = true to constrain ripple bounds to item area.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as ScrollToFirstUnreadButton
    participant Compose as MessageList
    participant ViewModel as MessageListViewModel
    participant Controller as MessageListController
    participant State as MessageListState
    
    User->>UI: Click scroll to first unread
    UI->>Compose: onScrollToFirstUnread()
    Compose->>ViewModel: scrollToFirstUnreadMessage()
    ViewModel->>Controller: scrollToFirstUnreadMessage()
    Controller->>State: Update unreadLabel visibility
    
    User->>UI: Click dismiss (X)
    UI->>Compose: onDismissUnreadLabel()
    Compose->>ViewModel: disableUnreadLabelButton()
    ViewModel->>Controller: disableUnreadLabelButton()
    Controller->>Controller: Emit false to showUnreadButtonState
    Controller->>Controller: Set unreadLabel.buttonVisibility = false
    Controller->>State: updateUnreadLabel()
    State->>UI: unreadLabel state updated
    UI->>UI: Re-render (button hidden)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A button springs forth, unread and bright,
Guiding the eye to messages of light,
Click it, dismiss it with a flick of the paw,
State flows through layers without a flaw! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title accurately describes the main change in the changeset—adding a scroll-to-first-unread pill/button UI element to the message view, which is the primary feature across all modified files.
Description check ✅ Passed The PR description includes all major template sections with comprehensive technical implementation details, UI comparison videos, and explicit testing steps.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/scroll-to-first-unread-indicator

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

Caution

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

⚠️ Outside diff range comments (1)
stream-chat-android-ui-common/api/stream-chat-android-ui-common.api (1)

2740-2755: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Binary-incompatible constructor/copy signature change — confirm SDK consumers are aware

The MessageListState primary constructor and copy method have changed from 11 to 12 parameters (adding unreadLabel). This is a binary-incompatible change for:

  • Java callers invoking the 11-arg constructor directly.
  • Kotlin callers using positional (non-named) arguments on copy.
  • Any consumer compiled against the old API — they will need to recompile with updated call sites.

The no-arg constructor (Line 2739) is preserved, and the DefaultConstructorMarker on the synthetic constructor confirms unreadLabel has a default value, so Kotlin callers using named parameters or defaults are unaffected. This is expected behaviour for data-class extension, but worth communicating explicitly in the release notes / changelog if this module is published as a versioned library.

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

In `@stream-chat-android-ui-common/api/stream-chat-android-ui-common.api` around
lines 2740 - 2755, MessageListState's primary constructor and copy method gained
a 12th parameter unreadLabel (symbols: MessageListState constructor, copy,
copy$default, unreadLabel), which is a binary-incompatible change for consumers
compiled against the previous 11-arg signatures; to fix, either: (a) add a
binary-compatible 11-argument overload/compat shim that delegates to the new
constructor/copy (preserving old signature for Java callers and positional
Kotlin usage), or (b) if you intentionally break binary compatibility, update
the release notes/changelog and migration guide to call out the added
unreadLabel parameter and instruct consumers to recompile, and add a `@Deprecated`
note on the old usages if you remove the shim so callers are clearly warned.
🧹 Nitpick comments (1)
stream-chat-android-ui-common/api/stream-chat-android-ui-common.api (1)

2740-2741: ⚖️ Poor tradeoff

MessageListController$UnreadLabel leaks the feature layer into the state layer's public API

The type MessageListController$UnreadLabel (package ...feature.messages.list) now appears throughout MessageListState's public surface (constructor, copy, component12, getUnreadLabel). A Kotlin typealias in UnreadLabel.kt helps source-level ergonomics but is transparent at the bytecode level — the API dump (and therefore Java callers and binary consumers) still sees MessageListController$UnreadLabel. This forces any consumer of MessageListState to take a transitive dependency on MessageListController just to read/pass the label value.

Relocating the UnreadLabel data class to the state.messages.list package (rather than nesting it inside MessageListController) would cleanly sever this cross-layer coupling without requiring any typealias workaround. The typealias approach works today but can be confusing to new contributors and adds an unnecessary indirection.

Also applies to: 2754-2755, 2765-2765

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

In `@stream-chat-android-ui-common/api/stream-chat-android-ui-common.api` around
lines 2740 - 2741, The public API is leaking the feature-layer nested type
MessageListController$UnreadLabel into MessageListState (seen in the
constructor, copy, component12, getUnreadLabel); move the UnreadLabel data class
out of MessageListController into the state.messages.list package so
MessageListState can reference a state-level
io.getstream.chat.android.ui.common.state.messages.list.UnreadLabel type. Create
the UnreadLabel data class in the state.messages.list package (preserving
fields, equals/hashCode/copy semantics), update imports/usages in
MessageListState and any callers to use the new UnreadLabel, remove or replace
the nested class inside MessageListController (and any typealias) to avoid the
transitive dependency, and run consumers/serialization checks to ensure binary
compatibility is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@stream-chat-android-compose/api/stream-chat-android-compose.api`:
- Line 3465: The public ChatComponentFactory interface was extended with new
methods ScrollToFirstUnreadButton and ScrollToBottomButton which breaks binary
compatibility for precompiled custom implementors; to fix, enable JVM default
method generation by adding the Kotlin compiler option -Xjvm-default=all to the
stream-chat-android-compose module so these new members are emitted as
JVM-default methods, or alternatively document this as a breaking change
requiring consumers to recompile custom ChatComponentFactory implementations;
update build configuration accordingly and ensure CI/build docs note the change.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/Messages.kt`:
- Around line 389-401: The visibility check uses the numeric
unreadSeparatorIndex which can be invalid when the lazy list contains
non-message items; instead compute the unique key for the UnreadSeparator item
(e.g., derive unreadSeparatorKey from messages.first { it is
UnreadSeparatorItemState } or from the same keying logic used in itemsIndexed)
and change isUnreadSeparatorVisible (currently using
lazyListState.layoutInfo.visibleItemsInfo.any { it.index == unreadSeparatorIndex
}) to check visibleItemsInfo.any { it.key == unreadSeparatorKey }; update the
variables unreadSeparatorIndex and isUnreadSeparatorVisible and keep the call to
isScrollToFirstUnreadVisible unchanged so visibility is driven by the keyed
check.

In
`@stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt`:
- Around line 638-640: The disableUnreadLabelButton currently forces
showUnreadButtonState.tryEmit(false) even when unreadLabelState.value is null,
which latches suppression; update disableUnreadLabelButton so it first checks
unreadLabelState.value != null and only then calls
showUnreadButtonState.tryEmit(false) and sets unreadLabelState.value =
unreadLabelState.value.copy(buttonVisibility = false); avoid emitting or
mutating state when unreadLabelState.value is null to prevent suppressing future
unread-pill visibility.

In
`@stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListControllerTests.kt`:
- Around line 1048-1050: The assertion uses a safe-call that can hide a null
unreadLabelState and produce false positives; change the test to assert that
controller.unreadLabelState.value is non-null before asserting its
buttonVisibility is false — specifically, add an explicit non-null check on
controller.unreadLabelState.value (e.g., assertNotNull or equivalent) and then
assert on unreadLabelState.value.buttonVisibility in MessageListControllerTests
so the post-update assertion cannot pass when the state is null.

---

Outside diff comments:
In `@stream-chat-android-ui-common/api/stream-chat-android-ui-common.api`:
- Around line 2740-2755: MessageListState's primary constructor and copy method
gained a 12th parameter unreadLabel (symbols: MessageListState constructor,
copy, copy$default, unreadLabel), which is a binary-incompatible change for
consumers compiled against the previous 11-arg signatures; to fix, either: (a)
add a binary-compatible 11-argument overload/compat shim that delegates to the
new constructor/copy (preserving old signature for Java callers and positional
Kotlin usage), or (b) if you intentionally break binary compatibility, update
the release notes/changelog and migration guide to call out the added
unreadLabel parameter and instruct consumers to recompile, and add a `@Deprecated`
note on the old usages if you remove the shim so callers are clearly warned.

---

Nitpick comments:
In `@stream-chat-android-ui-common/api/stream-chat-android-ui-common.api`:
- Around line 2740-2741: The public API is leaking the feature-layer nested type
MessageListController$UnreadLabel into MessageListState (seen in the
constructor, copy, component12, getUnreadLabel); move the UnreadLabel data class
out of MessageListController into the state.messages.list package so
MessageListState can reference a state-level
io.getstream.chat.android.ui.common.state.messages.list.UnreadLabel type. Create
the UnreadLabel data class in the state.messages.list package (preserving
fields, equals/hashCode/copy semantics), update imports/usages in
MessageListState and any callers to use the new UnreadLabel, remove or replace
the nested class inside MessageListController (and any typealias) to avoid the
transitive dependency, and run consumers/serialization checks to ensure binary
compatibility is preserved.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 41d8a675-f966-4dff-8333-f88667be786e

📥 Commits

Reviewing files that changed from the base of the PR and between 84b9b96 and 51386f9.

⛔ Files ignored due to path filters (1)
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.messages_ScrollToFirstUnreadButtonTest_pill_with_unread_count.png is excluded by !**/*.png
📒 Files selected for processing (17)
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPage.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPhotosMenu.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/ScrollToFirstUnreadButton.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/Messages.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/ModifierUtils.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt
  • stream-chat-android-compose/src/main/res/values/strings.xml
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/messages/ScrollToFirstUnreadButtonTest.kt
  • stream-chat-android-ui-common/api/stream-chat-android-ui-common.api
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/list/MessageListState.kt
  • stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/list/UnreadLabel.kt
  • stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListControllerTests.kt

Comment thread stream-chat-android-compose/api/stream-chat-android-compose.api
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.82 MB 5.82 MB 0.00 MB 🟢
stream-chat-android-ui-components 11.02 MB 11.02 MB 0.00 MB 🟢
stream-chat-android-compose 12.37 MB 12.38 MB 0.01 MB 🟢

andremion added 2 commits May 4, 2026 16:02
- Messages.kt: switch the pill's separator-visibility check to compare
  LazyListItemInfo.key against the separator item's id, instead of comparing
  the message-list index against the LazyList index. The two diverge when
  non-message items (footerContent, the load-more indicator) precede
  itemsIndexed in the column, which would mis-judge pill visibility.

- MessageListController.disableUnreadLabelButton: early-return when there is
  no active unread label. The previous code emitted false on
  showUnreadButtonState even on a no-op call (e.g. scrollToFirstUnreadMessage
  with a null unreadLabelState, or the XML SDK's HideUnreadLabel event with
  no active label), permanently latching the suppression for the controller's
  lifetime and hiding any future unread pill.

- MessageListControllerTests: strengthen the post-read assertion to fail when
  unreadLabelState becomes null. The previous safe-call chain silently passed
  when the chain returned null, masking the regression the test is meant to
  guard.
Two coverage additions for the new unread-pill surface:

- MessageListViewModelTest gains a case for disableUnreadLabelButton on a
  fixture with no active unread label, asserting state stays untouched. This
  exercises the VM's delegating method and the controller's early-return
  guard end-to-end.

- MessageListTest gains a snapshot variant rendering MessageList with an
  active UnreadLabel and an inline UnreadSeparatorItemState. The snapshot
  exercises the wired pill through the public state-only MessageList,
  Messages.DefaultMessagesHelperContent, and the
  ChatComponentFactory.ScrollToFirstUnreadButton slot in inspection mode.
@andremion andremion force-pushed the feat/scroll-to-first-unread-indicator branch from 5cef529 to f7815c4 Compare May 4, 2026 15:47
@andremion andremion changed the title Feat/scroll to first unread indicator Add scroll-to-first-unread pill to the message view May 4, 2026
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 4, 2026

@andremion andremion marked this pull request as ready for review May 4, 2026 15:56
@andremion andremion requested a review from a team as a code owner May 4, 2026 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:new-feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant