Skip to content

Update media preview screen design#6247

Draft
gpunto wants to merge 3 commits intov7from
redesign/media-gallery
Draft

Update media preview screen design#6247
gpunto wants to merge 3 commits intov7from
redesign/media-gallery

Conversation

@gpunto
Copy link
Contributor

@gpunto gpunto commented Mar 13, 2026

Goal

Redesign the media gallery preview screen

Implementation

  • Filter out non-media attachments (e.g. files, audio) from the gallery, keeping only images and videos
  • Update the gallery options menu to a bottom sheet
  • Replace inline video controls with a new VideoPlaybackControls overlay
  • Extract MediaGalleryPlayerState to encapsulate player lifecycle and state management
  • Extract a reusable SpeedButton component from the audio recording speed control
  • Implement "immersive mode" on tap (hiding the toolbars)

🎨 UI Changes

Before After
Screenshot_20260313_111819 Screenshot_20260313_111024
Screenshot_20260313_111843 Screenshot_20260313_111059

Testing

  • Open the media gallery from a message with mixed attachments (images, files, audio) and verify only images/videos are shown
  • Play a video and verify the new playback controls (play/pause, seek, speed, mute, share)
  • Open the options menu and verify the updated icon-only layout

Summary by CodeRabbit

Release Notes

  • New Features

    • Added video playback controls with play/pause, progress seeking, and adjustable playback speed.
    • Enhanced media gallery with immersive mode toggling via media taps.
    • Improved options menu with bottom sheet layout for better UX.
  • Tests

    • Updated test interactions to use semantic-based click actions.

@gpunto gpunto added pr:breaking-change Breaking change pr:improvement Improvement labels Mar 13, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 13, 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.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 13, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.25 MB 5.70 MB 0.45 MB 🟡
stream-chat-android-ui-components 10.60 MB 11.00 MB 0.40 MB 🟡
stream-chat-android-compose 12.81 MB 12.05 MB -0.77 MB 🚀

@gpunto gpunto changed the title Update media gallery preview screen design Update media preview screen design Mar 13, 2026
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
37.8% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@gpunto gpunto marked this pull request as ready for review March 13, 2026 13:47
@gpunto gpunto requested a review from a team as a code owner March 13, 2026 13:47
@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

Walkthrough

This pull request integrates video playback capabilities into the media gallery preview system. Changes introduce a lifecycle-managed Player state, video playback controls, immersive mode toggling via media taps, and UI updates across the gallery components. A ModalBottomSheet replaces the previous overlay in options menu, and external component libraries are utilized for button controls.

Changes

Cohort / File(s) Summary
API Signatures
api/stream-chat-android-compose.api
Updated public function signatures with modified lambda arities (Function6 → Function7, Function5) and parameter type changes in MediaGalleryPreviewScreen and related composables to accommodate new Player and callback parameters.
Player State Management
src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPlayerState.kt
New file introducing lifecycle-aware player state holder (MediaGalleryPlayerState), rememberMediaGalleryPlayerState composable for player creation/lifecycle binding, and GalleryMediaEffect for reactive page-to-media synchronization and playback preparation.
Video Playback Controls
src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/VideoPlaybackControls.kt, src/main/java/io/getstream/chat/android/compose/ui/components/button/SpeedButton.kt
New VideoPlaybackControls composable with draggable slider, play/pause button, time display, and RTL support. New SpeedButton composable for playback speed display and cycling. Both integrated with theme tokens and Player state management.
Gallery Screen Updates
src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewScreen.kt
Major refactor introducing Player parameter threading through content/footer, onMediaClick callbacks for immersive mode toggling, video playback controls in footer topContent, attachment filtering for media-only items, and dynamic header/footer visibility via AnimatedVisibility.
Gallery Page Integration
src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPage.kt
Added onTap callback to MediaGalleryImagePage with drag-state tracking. Updated MediaGalleryVideoPage to accept onClick callback, removed showBuffering and onPlaybackError parameters, and integrated Player lifecycle listener for buffering state management.
Channel Attachments Preview
src/main/java/io/getstream/chat/android/compose/ui/channel/attachments/ChannelMediaAttachmentsPreviewScreen.kt
Updated to initialize playerState via rememberMediaGalleryPlayerState, invoke GalleryMediaEffect for media synchronization, and pass player instance to MediaGalleryPager for unified playback control.
Options Menu & Player Content
src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewOptionsMenu.kt, src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/StreamMediaPlayerContent.kt
Replaced Box-based overlay with ModalBottomSheet for options menu. Updated controller visibility in StreamMediaPlayerContent to use useController = false and theme-based background colors.
Audio Attachments
src/main/java/io/getstream/chat/android/compose/ui/attachments/content/AudioRecordAttachmentContent.kt
Removed internal SpeedButton implementation (34 lines) and replaced with external SpeedButton import, maintaining identical public API and control flow.
Tests
src/test/kotlin/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivityTest.kt
Updated test interactions from text-based performClick() to semantic-based performSemanticsAction(OnClick) with intermediate idle waits for improved test stability and semantics compliance.

Sequence Diagram

sequenceDiagram
    participant UI as MediaGalleryPreviewScreen
    participant PSM as rememberMediaGalleryPlayerState
    participant PS as MediaGalleryPlayerState
    participant GME as GalleryMediaEffect
    participant Pager as MediaGalleryPager
    participant VPC as VideoPlaybackControls
    participant Player as Player

    UI->>PSM: Initialize with onPlaybackError callback
    PSM->>PS: Create state holder
    PSM->>Player: Create & bind to lifecycle
    PSM-->>UI: Return playerState
    
    UI->>GME: Pass playerState, currentPage, attachments
    GME->>PS: Query current player
    GME->>Player: Pause previous attachment
    GME->>PS: Select attachment at currentPage
    alt Video attachment
        GME->>Player: Prepare MediaItem from URL
        GME->>Player: Seek to savedPosition
        GME->>Player: Start playback
    end
    
    UI->>Pager: Pass player & onMediaClick
    Pager->>VPC: Mount with player instance
    VPC->>Player: Attach listener for state changes
    VPC->>Player: Poll position while playing
    
    UI->>VPC: User interacts (play/pause, seek, speed)
    VPC->>Player: Execute command (play, seek, setPlaybackParameters)
    Player-->>VPC: Listener callback with state update
    VPC-->>UI: Render updated controls
    
    Pager->>UI: User taps media (onMediaClick)
    UI->>UI: Toggle immersive mode
    UI->>Pager: Update header/footer visibility
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A Compose Tale of Play and Pause

With players now shared through the gallery view,
Video controls dance—old lifecycle anew,
Taps spawn immersion, speeds twist and spin,
While bottomless sheets let the options slide in.
Hop hop, the media flows smooth as can be!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.84% 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
Title check ✅ Passed The title 'Update media preview screen design' directly reflects the main objective to redesign the media gallery preview screen with updated UI and components.
Description check ✅ Passed The PR description covers all required template sections: Goal, Implementation with detailed bullet points, UI Changes with before/after screenshots, and Testing instructions. All critical sections are complete and substantive.

✏️ 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 redesign/media-gallery
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can approve the review once all CodeRabbit's comments are resolved.

Enable the reviews.request_changes_workflow setting to automatically approve the review once all CodeRabbit's comments are resolved.

Copy link

@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.

Caution

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

⚠️ Outside diff range comments (4)
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/StreamMediaPlayerContent.kt (1)

223-229: ⚠️ Potential issue | 🟠 Major

Don't hard-disable the only controller on this player view.

StreamMediaPlayerContent is still used directly by stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaPreviewActivity.kt:64-84, and that screen does not render the new overlay controls. On Line 228, forcing useController = false removes pause/seek UI there, while the existing showController() call becomes a no-op.

🎛️ Suggested shape
 internal fun createPlayerView(
     context: Context,
     player: Player,
+    useController: Boolean = true,
 ): PlayerView {
     val playerView = LayoutInflater.from(context)
         .inflate(R.layout.stream_compose_player_view, null) as PlayerView
     return playerView.apply {
         this.player = player
-        useController = false
+        this.useController = useController
         setShowBuffering(PlayerView.SHOW_BUFFERING_NEVER)
     }
 }

Then keep the default in StreamMediaPlayerContent, and pass useController = false only from the redesigned gallery path.

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

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/StreamMediaPlayerContent.kt`
around lines 223 - 229, createPlayerView currently forces useController = false
which breaks controls for callers like MediaPreviewActivity and makes
showController() a no-op; remove the hard-disable (delete or stop setting
useController in createPlayerView inside StreamMediaPlayerContent) so the
PlayerView uses its default controller behavior, and instead set useController =
false only from the redesigned gallery code path that requires it (i.e., update
the caller that intentionally wants no controller to explicitly set
playerView.useController = false after createPlayerView).
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/attachments/ChannelMediaAttachmentsPreviewScreen.kt (1)

171-178: ⚠️ Potential issue | 🟠 Major

This pager wiring is only half of the new gallery flow.

By passing player = playerState.player here you opt into MediaGalleryVideoPage, which now uses a controllerless player view. Because this screen still keeps the old bottom bar and does not pass onMediaClick, videos here end up with no replacement playback controls and media taps cannot trigger immersive mode.

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

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/attachments/ChannelMediaAttachmentsPreviewScreen.kt`
around lines 171 - 178, The pager is being forced into the new controllerless
video page by passing playerState.player to MediaGalleryPager, which prevents
the old bottom bar controls and onMediaClick handling; remove the player =
playerState.player argument (or pass null) so
MediaGalleryPager/MediaGalleryVideoPage falls back to the controller-backed
view, and add/forward an onMediaClick callback from
ChannelMediaAttachmentsPreviewScreen into MediaGalleryPager (and ensure the
existing bottom bar code still renders) so taps trigger immersive mode and
playback controls are present.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPage.kt (1)

204-223: ⚠️ Potential issue | 🔴 Critical

Import withTimeoutOrNull before landing this.

Line 208 calls withTimeoutOrNull, but this file never imports kotlinx.coroutines.withTimeoutOrNull, so the new tap/double-tap block will not compile.

🔧 Proposed fix
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.withTimeoutOrNull
 import kotlin.math.abs
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPage.kt`
around lines 204 - 223, The new double-tap logic uses withTimeoutOrNull but the
file lacks the kotlinx.coroutines import, so add an import for
kotlinx.coroutines.withTimeoutOrNull at the top of the file; locate the
pointerInput block (contains coroutineScope, awaitEachGesture, awaitFirstDown,
DoubleTapTimeoutMs and withTimeoutOrNull) and add the missing import so the
withTimeoutOrNull call compiles.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewScreen.kt (1)

147-155: ⚠️ Potential issue | 🟡 Minor

Inconsistent header modifier between stateful and stateless overloads.

The stateful overload uses Modifier.fillMaxWidth() (line 149) while the stateless overload uses Modifier.fillMaxWidth().height(56.dp) (lines 296-298). This inconsistency means the header will have different default heights depending on which overload is used.

Consider aligning both defaults:

Proposed fix
     header: `@Composable` (attachments: List<Attachment>, currentPage: Int) -> Unit = { _, _ ->
         MediaGalleryPreviewHeader(
-            modifier = Modifier.fillMaxWidth(),
+            modifier = Modifier
+                .fillMaxWidth()
+                .height(56.dp),
             message = viewModel.message,
             connectionState = viewModel.connectionState,
             onLeadingContentClick = onHeaderLeadingContentClick,
             onTrailingContentClick = onHeaderTrailingContentClick,
         )
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewScreen.kt`
around lines 147 - 155, The stateful overload's header parameter uses
Modifier.fillMaxWidth() while the stateless overload sets
Modifier.fillMaxWidth().height(56.dp), causing different default header heights;
update the stateful overload's header default (the header: `@Composable`
(attachments: List<Attachment>, currentPage: Int) -> Unit lambda that calls
MediaGalleryPreviewHeader) to use the same modifier as the stateless version
(add .height(56.dp) to Modifier.fillMaxWidth()) so both overloads render the
header at the same default height, ensuring consistency for
MediaGalleryPreviewHeader across both implementations.
🧹 Nitpick comments (6)
stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivityTest.kt (3)

142-156: Test lacks behavior assertion.

Similar to the delete test, should save on save option click only exercises the click without verifying save behavior. The scenario variable is unused. Consider adding mock verification or renaming to clarify the test's actual scope.

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

In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivityTest.kt`
around lines 142 - 156, The test should assert that the save action occurs
rather than only performing the click: update the test `should save on save
option click` to either (A) verify the save flow by mocking or spying the
component that handles saves (e.g., inject a mock save handler into
`MediaGalleryPreviewActivity` or observe a result/Intent from
`ActivityScenario`), and assert that the handler was called or the expected
Intent/result was produced after clicking the "Save media" option, or (B) if you
intentionally only exercise UI interaction, rename the test to reflect that it
only performs the click; use the existing
`createIntent(message)`/`PreviewMessageData.messageWithUserAndAttachment` and
`ActivityScenario.launchActivityForResult<MediaGalleryPreviewActivity>(intent)`/`composeTestRule`
to obtain and assert the expected behavior.

124-140: Test lacks behavior assertion.

The test name should delete on delete option click suggests it verifies delete behavior, but the test only exercises the click without asserting any outcome. The scenario variable is unused. Consider either:

  1. Adding an assertion (e.g., verify delete API was called via mock verification)
  2. Renaming to clarify intent (e.g., should not crash on delete option click)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivityTest.kt`
around lines 124 - 140, The test function `should delete on delete option click`
in MediaGalleryPreviewActivityTest currently only performs a click and doesn't
assert behavior; update the test to either verify the delete flow was triggered
(for example, mock and verify the chat client's delete method or repository
method was called after clicking the "Delete" option—use the existing mocks like
`mockClientState`/client mock or whatever deletion API mock exists in the test
harness and assert it was invoked with
`PreviewMessageData.messageWithUserAndAttachment`), or rename the test to
something like `should not crash on delete option click` if no behavior should
be asserted; modify `should delete on delete option click` (or add verification)
and use the `scenario`/mocks to perform a verify assertion.

158-168: Inconsistent interaction pattern.

The should share on share option click test still uses performClick() directly (line 166) while all the other option click tests were migrated to the semantic action pattern. Consider aligning for consistency, or document why the share button differs (e.g., it's a toolbar icon vs. bottom sheet item).

♻️ Suggested alignment (if applicable)

If the Share button should follow the same pattern:

-            composeTestRule.onNodeWithContentDescription("Share").performClick()
+            composeTestRule.onNodeWithContentDescription("Share")
+                .performSemanticsAction(SemanticsActions.OnClick)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivityTest.kt`
around lines 158 - 168, The test MediaGalleryPreviewActivityTest.should share on
share option click uses
composeTestRule.onNodeWithContentDescription("Share").performClick(), which is
inconsistent with the other option-click tests that use the semantic-action
pattern; replace the direct performClick call with the same semantic-action
invocation used elsewhere (i.e., invoke the node's semantic action for click) so
the test aligns with the other option click tests, or if the Share control is
intentionally different (toolbar icon vs bottom-sheet item), add a short comment
in the test explaining why performClick is kept.
stream-chat-android-compose/api/stream-chat-android-compose.api (1)

864-865: Document this MediaGalleryPreviewScreen API break in the v7 migration notes.

These overloads change the public parameter model and the exposed slot callback shape, so custom preview integrations will need source updates. If this is not already covered elsewhere in the PR, please add an entry to MIGRATION_TO_V7.md or the changelog rather than introducing compatibility shims.

Based on learnings, PRs against major version branches (e.g., v7) are allowed to introduce breaking public API changes, and maintainers prefer documenting migration paths in MIGRATION_TO_V7.md/CHANGELOG for function signature changes.

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

In `@stream-chat-android-compose/api/stream-chat-android-compose.api` around lines
864 - 865, The public overloads of MediaGalleryPreviewScreen changed parameter
types and callback shapes; document this breaking change in the v7 migration
notes by adding an entry to MIGRATION_TO_V7.md (or the CHANGELOG) explaining the
new signatures (both MediaGalleryPreviewScreen overloads), what changed
(model-based parameters vs viewmodel-based parameters and updated FunctionX slot
types), and provide a minimal migration snippet showing how callers should adapt
custom preview integrations to the new parameter list and callback shapes;
reference the exact symbol name MediaGalleryPreviewScreen and list both overload
variants so users can find and update usages.
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewScreen.kt (2)

722-725: Consider updating content description to match the new icon.

The icon changed from a close (X) to a back arrow, but the content description still references "cancel" (R.string.stream_compose_cancel). For accessibility consistency, consider using a "back" or "navigate back" description.

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

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewScreen.kt`
around lines 722 - 725, The Icon in MediaGalleryPreviewScreen.kt now uses
R.drawable.stream_compose_ic_arrow_back but still sets contentDescription via
stringResource(R.string.stream_compose_cancel); update the contentDescription to
a more accurate string (e.g., R.string.stream_compose_back or
R.string.stream_compose_navigate_back) and replace the reference in the Icon's
contentDescription call so accessibility announces "back" instead of "cancel".

594-603: Consider showing a placeholder when player is null for video attachments.

When player is null (e.g., during initialization), video pages render nothing. This could result in a blank page momentarily. Consider showing the thumbnail as a placeholder until the player is ready.

Suggested improvement
             attachment.isVideo() -> {
-                player?.let {
+                if (player != null) {
                     MediaGalleryVideoPage(
                         modifier = Modifier.fillMaxSize(),
-                        player = it,
+                        player = player,
                         thumbnailUrl = attachment.thumbUrl,
                         onClick = onMediaClick,
                     )
+                } else {
+                    // Show thumbnail placeholder while player initializes
+                    MediaGalleryImagePage(
+                        attachment = attachment,
+                        pagerState = pagerState,
+                        page = page,
+                        onTap = onMediaClick,
+                    )
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewScreen.kt`
around lines 594 - 603, The video branch currently returns nothing when player
is null which causes blank pages; update the attachment.isVideo() branch to
render a thumbnail placeholder while the player initializes by either (a)
passing a nullable player into MediaGalleryVideoPage and have it show the
thumbnailUrl when player == null, or (b) render a small
MediaGalleryVideoPlaceholder composable that uses attachment.thumbUrl and
onMediaClick until player is available, then swap to MediaGalleryVideoPage with
the ready player; reference attachment.isVideo(), player, MediaGalleryVideoPage,
thumbnailUrl/thumbUrl, and onMediaClick to locate and implement the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/MediaGalleryPage.kt`:
- Around line 204-223: The new double-tap logic uses withTimeoutOrNull but the
file lacks the kotlinx.coroutines import, so add an import for
kotlinx.coroutines.withTimeoutOrNull at the top of the file; locate the
pointerInput block (contains coroutineScope, awaitEachGesture, awaitFirstDown,
DoubleTapTimeoutMs and withTimeoutOrNull) and add the missing import so the
withTimeoutOrNull call compiles.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/StreamMediaPlayerContent.kt`:
- Around line 223-229: createPlayerView currently forces useController = false
which breaks controls for callers like MediaPreviewActivity and makes
showController() a no-op; remove the hard-disable (delete or stop setting
useController in createPlayerView inside StreamMediaPlayerContent) so the
PlayerView uses its default controller behavior, and instead set useController =
false only from the redesigned gallery code path that requires it (i.e., update
the caller that intentionally wants no controller to explicitly set
playerView.useController = false after createPlayerView).

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewScreen.kt`:
- Around line 147-155: The stateful overload's header parameter uses
Modifier.fillMaxWidth() while the stateless overload sets
Modifier.fillMaxWidth().height(56.dp), causing different default header heights;
update the stateful overload's header default (the header: `@Composable`
(attachments: List<Attachment>, currentPage: Int) -> Unit lambda that calls
MediaGalleryPreviewHeader) to use the same modifier as the stateless version
(add .height(56.dp) to Modifier.fillMaxWidth()) so both overloads render the
header at the same default height, ensuring consistency for
MediaGalleryPreviewHeader across both implementations.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/attachments/ChannelMediaAttachmentsPreviewScreen.kt`:
- Around line 171-178: The pager is being forced into the new controllerless
video page by passing playerState.player to MediaGalleryPager, which prevents
the old bottom bar controls and onMediaClick handling; remove the player =
playerState.player argument (or pass null) so
MediaGalleryPager/MediaGalleryVideoPage falls back to the controller-backed
view, and add/forward an onMediaClick callback from
ChannelMediaAttachmentsPreviewScreen into MediaGalleryPager (and ensure the
existing bottom bar code still renders) so taps trigger immersive mode and
playback controls are present.

---

Nitpick comments:
In `@stream-chat-android-compose/api/stream-chat-android-compose.api`:
- Around line 864-865: The public overloads of MediaGalleryPreviewScreen changed
parameter types and callback shapes; document this breaking change in the v7
migration notes by adding an entry to MIGRATION_TO_V7.md (or the CHANGELOG)
explaining the new signatures (both MediaGalleryPreviewScreen overloads), what
changed (model-based parameters vs viewmodel-based parameters and updated
FunctionX slot types), and provide a minimal migration snippet showing how
callers should adapt custom preview integrations to the new parameter list and
callback shapes; reference the exact symbol name MediaGalleryPreviewScreen and
list both overload variants so users can find and update usages.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewScreen.kt`:
- Around line 722-725: The Icon in MediaGalleryPreviewScreen.kt now uses
R.drawable.stream_compose_ic_arrow_back but still sets contentDescription via
stringResource(R.string.stream_compose_cancel); update the contentDescription to
a more accurate string (e.g., R.string.stream_compose_back or
R.string.stream_compose_navigate_back) and replace the reference in the Icon's
contentDescription call so accessibility announces "back" instead of "cancel".
- Around line 594-603: The video branch currently returns nothing when player is
null which causes blank pages; update the attachment.isVideo() branch to render
a thumbnail placeholder while the player initializes by either (a) passing a
nullable player into MediaGalleryVideoPage and have it show the thumbnailUrl
when player == null, or (b) render a small MediaGalleryVideoPlaceholder
composable that uses attachment.thumbUrl and onMediaClick until player is
available, then swap to MediaGalleryVideoPage with the ready player; reference
attachment.isVideo(), player, MediaGalleryVideoPage, thumbnailUrl/thumbUrl, and
onMediaClick to locate and implement the change.

In
`@stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivityTest.kt`:
- Around line 142-156: The test should assert that the save action occurs rather
than only performing the click: update the test `should save on save option
click` to either (A) verify the save flow by mocking or spying the component
that handles saves (e.g., inject a mock save handler into
`MediaGalleryPreviewActivity` or observe a result/Intent from
`ActivityScenario`), and assert that the handler was called or the expected
Intent/result was produced after clicking the "Save media" option, or (B) if you
intentionally only exercise UI interaction, rename the test to reflect that it
only performs the click; use the existing
`createIntent(message)`/`PreviewMessageData.messageWithUserAndAttachment` and
`ActivityScenario.launchActivityForResult<MediaGalleryPreviewActivity>(intent)`/`composeTestRule`
to obtain and assert the expected behavior.
- Around line 124-140: The test function `should delete on delete option click`
in MediaGalleryPreviewActivityTest currently only performs a click and doesn't
assert behavior; update the test to either verify the delete flow was triggered
(for example, mock and verify the chat client's delete method or repository
method was called after clicking the "Delete" option—use the existing mocks like
`mockClientState`/client mock or whatever deletion API mock exists in the test
harness and assert it was invoked with
`PreviewMessageData.messageWithUserAndAttachment`), or rename the test to
something like `should not crash on delete option click` if no behavior should
be asserted; modify `should delete on delete option click` (or add verification)
and use the `scenario`/mocks to perform a verify assertion.
- Around line 158-168: The test MediaGalleryPreviewActivityTest.should share on
share option click uses
composeTestRule.onNodeWithContentDescription("Share").performClick(), which is
inconsistent with the other option-click tests that use the semantic-action
pattern; replace the direct performClick call with the same semantic-action
invocation used elsewhere (i.e., invoke the node's semantic action for click) so
the test aligns with the other option click tests, or if the Share control is
intentionally different (toolbar icon vs bottom-sheet item), add a short comment
in the test explaining why performClick is kept.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9519d05c-80e6-46e8-92d8-85a853212021

📥 Commits

Reviewing files that changed from the base of the PR and between 8c51a2d and 6bfc276.

⛔ Files ignored due to path filters (11)
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_other_user.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewOptionsMenuTest_media_gallery_options_menu_for_own_user.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_connecting.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_message_without_id.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_offline.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_header_online.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_connected.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_offline.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_gallery_bottom_sheet.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_options_menu.png is excluded by !**/*.png
  • stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.attachments.preview_MediaGalleryPreviewScreenTest_media_gallery_screen_with_share_large_file_prompt.png is excluded by !**/*.png
📒 Files selected for processing (11)
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/AudioRecordAttachmentContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewOptionsMenu.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewScreen.kt
  • 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/MediaGalleryPlayerState.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/StreamMediaPlayerContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/preview/internal/VideoPlaybackControls.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/attachments/ChannelMediaAttachmentsPreviewScreen.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/button/SpeedButton.kt
  • stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/attachments/preview/MediaGalleryPreviewActivityTest.kt

@gpunto gpunto marked this pull request as draft March 13, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:breaking-change Breaking change pr:improvement Improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant