Skip to content

Timeline: jump-to-unread FAB and mark-as-read shortcut#6694

Open
jennaharris7 wants to merge 14 commits intoelement-hq:developfrom
jennaharris7:feature/scroll-to-unread-messages
Open

Timeline: jump-to-unread FAB and mark-as-read shortcut#6694
jennaharris7 wants to merge 14 commits intoelement-hq:developfrom
jennaharris7:feature/scroll-to-unread-messages

Conversation

@jennaharris7
Copy link
Copy Markdown

@jennaharris7 jennaharris7 commented Apr 29, 2026

Timeline: jump-to-unread + mark-as-read on the timeline FABs

Content

Two additions to the room timeline, gated behind a new JumpToUnread feature flag (off by default; toggleable in developer options):

  • Jump-to-unread FAB — chevron-up button that appears stacked above the scroll-to-bottom FAB whenever the read marker is above the visible viewport. Tapping centres the timeline on the marker.
  • Mark as read — long-pressing either FAB opens a small popup with a "Mark as read" action that marks the room fully read at the latest event.

Both FABs show a small green dot when there are unreads in their direction:

  • Top-of-FAB dot on the up-arrow when there are messages above the viewport.
  • Bottom-of-FAB dot on the down-arrow when new messages have arrived since you scrolled away.

The dot is binary (visible / not visible), not a count.

Motivation

When you scroll up in a busy room, the read marker leaves the screen and there's no quick way back. The chevron-up FAB takes you back in one tap. The green dots act as lightweight "there's something here" cues without cluttering the FAB with a number. The long-press shortcut lets you clear unread state without scrolling all the way back to the bottom.

Screenshots / GIFs

*style updated to match figma:


Screenshot 2026-05-05 at 10 22 56 AM ******

screenshot1screenshot2

Edge case not covered

If the read marker isn't in the loaded timeline window (e.g. focused-event timeline, paginated gaps), the chevron-up FAB won't appear. Closing this requires SDK plumbing to surface the fully-read event id so we can paginate-and-centre on it. Reasonable to land as a follow-up; this PR is gated behind FeatureFlags.JumpToUnread so the limited form can soak.

Tests

Enable the Jump to unread messages feature flag in developer options before exercising the manual cases below.

  • Open a busy room with unreads → scroll up → chevron-up FAB appears stacked above chevron-down at bottom-right with a green dot above it; tap → timeline animates the read marker into view.
  • Scrolled away from the bottom; receive a message from another user → green dot appears below the chevron-down FAB.
  • Tap the chevron-down FAB or scroll back to the bottom → green dot clears.
  • Long-press either FAB → "Mark as read" popup appears → tap → all unreads clear.
  • Disable the feature flag → chevron-up FAB never appears; chevron-down FAB shows no green dot regardless of state. (Long-press menu still works.)
  • Set the system font scale to maximum → "Mark as read" popup text wraps cleanly.
  • Enable TalkBack → both FABs render and remain tappable; jump-to-unread FAB is announced.
  • Unit tests pass: ./gradlew :features:messages:impl:testDebugUnitTest.
  • Konsist tests pass: ./gradlew :tests:konsist:testDebugUnitTest.

Tested devices

  • Physical
  • Emulator
  • OS version(s): Samsung Galaxy S24 on Android 16

Checklist

  • This PR was made with the help of AI:
    • Yes. In this case, please request a review by Copilot.
    • No.
  • Changes have been tested on an Android device or Android emulator with API 24
  • UI change has been tested on both light and dark themes
  • Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
  • Pull request is based on the develop branch
  • Pull request title will be used in the release note, it clearly defines what will change for the user
  • Pull request includes screenshots or videos if containing UI changes
  • You've made a self review of your PR

@jennaharris7 jennaharris7 requested a review from a team as a code owner April 29, 2026 23:42
@jennaharris7 jennaharris7 requested review from ganfra and removed request for a team April 29, 2026 23:42
@github-actions
Copy link
Copy Markdown
Contributor

Thank you for your contribution! Here are a few things to check in the PR to ensure it's reviewed as quickly as possible:

  • If your pull request adds a feature or modifies the UI, this should have an equivalent pull request in the Element X iOS repo unless it only affects an Android-only behaviour or is behind a disabled feature flag, since we need parity in both clients to consider a feature done. It will also need to be approved by our product and design teams before being merged, so it's usually a good idea to discuss the changes in a Github issue first and then start working on them once the approach has been validated.
  • Your branch should be based on origin/develop, at least when it was created.
  • The title of the PR will be used for release notes, so it needs to describe the change visible to the user.
  • The test pass locally running ./gradlew test.
  • The code quality check suite pass locally running ./gradlew runQualityChecks.
  • If you modified anything related to the UI, including previews, you'll have to run the Record screenshots GH action in your forked repo: that will generate compatible new screenshots. However, given Github Actions limitations, it will prevent the CI from running temporarily, until you upload a new commit after that one. To do so, just pull the latest changes and push an empty commit.

@github-actions github-actions Bot added the Z-Community-PR Issue is solved by a community member's PR label Apr 29, 2026
Copy link
Copy Markdown
Member

@ganfra ganfra left a comment

Choose a reason for hiding this comment

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

Thanks for the PR, first comments after a quick look, I'll get back on this next week.

val prevMostRecentItemId = rememberSaveable { mutableStateOf<UniqueId?>(null) }

val newEventState = remember { mutableStateOf(NewEventState.None) }
val newMessagesCount = remember { mutableIntStateOf(0) }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should probably be part of NewEventState.FromOther instead

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in commit 78aa500

withContext(dispatchers.computation) {
var markerIdx = -1
var unread = 0
for ((i, item) in items.withIndex()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This logic won't work when the marker is not in memory, which can happens pretty easily (gaps, event-cache pagination)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The iOS sibling PR (element-hq/element-x-ios#5506) hit the same limitation — they landed on the conclusion to ship behind a feature flag and follow up with SDK-supported scrolling. Proposing the same here. The proper fix needs an SDK accessor for the fully-read marker event id (it's in m.fully_read account data but not currently surfaced when the marker is outside the loaded window), plus rewiring the FAB to use Timeline.Mode.FocusedOnEvent on click. I’ve added an edge case not covered in the pr description, apologies for not including it initially!

} else {
stringResource(id = CommonStrings.a11y_jump_to_unread_messages)
}
TimelineFab(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TimelineFab name is not precise enough on the meaning of the component, maybe something like JumpToPositionButton

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 7148f2f

count: Int,
modifier: Modifier = Modifier,
) {
if (count <= 0) return
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With a when would be better.
Also needs inputs from Design team

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Refactored to use when in 7148f2f, will wait for design team feedback for other updates


<resources>
<string name="common_black">"Black"</string>
<string name="a11y_jump_to_unread_messages">"Jump to unread messages"</string>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TODO: move to localazy

}

fun jumpToReadMarker() {
if (readMarkerIndex < 0) return
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So like said previously, this won't work if the read marker is not loaded in memory

lazyListState.firstVisibleItemIndex < 3 && isLive
}
}
val isReadMarkerOffTop by remember {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should probably be name isJumpToUnreadVisibile

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 7148f2f

derivedStateOf {
if (!displayJumpToUnread || readMarkerIndex < 0) {
false
} else if (forceJumpToReadMarkerVisibility) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we simplify this check? Also forceJumpToReadMarkerVisibility should win over anything else.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The forceJumpToReadMarkerVisibility win is fixed in 7148f2f and the check refactor is in 200b98b

@jennaharris7 jennaharris7 changed the title Timeline: add a Jump to Unread button and a new-message badge on the scroll-to-bottom button Timeline: jump-to-unread FAB and mark-as-read shortcut May 5, 2026
@jennaharris7 jennaharris7 force-pushed the feature/scroll-to-unread-messages branch from 7e82ede to 0daf7ec Compare May 6, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Z-Community-PR Issue is solved by a community member's PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants