Report focused media from the pager rather than the thumbnail rail in media-send.

This commit is contained in:
Greyson Parrelli
2026-08-10 16:32:54 -04:00
parent 35f87898f2
commit d73d1bb741
2 changed files with 26 additions and 29 deletions
@@ -30,7 +30,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -40,6 +42,8 @@ import androidx.compose.ui.unit.dp
import androidx.fragment.compose.AndroidFragment
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import org.signal.core.ui.WindowBreakpoint
import org.signal.core.ui.compose.AllDevicePreviews
@@ -100,6 +104,24 @@ internal fun MediaEditScreen(
}
}
// Read through the latest selection rather than the one captured when the effect below started, since removals and
// reordering change which media a settled page refers to without restarting it.
val currentSelectedMedia by rememberUpdatedState(state.selectedMedia)
// Focus belongs to the pager rather than to any one piece of chrome: the thumbnail rail is not composed for
// documents, and a swipe still has to be reported from there.
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.isScrollInProgress }
.filter { !it }
.drop(1)
.collect {
val settledMedia = currentSelectedMedia.getOrNull(pagerState.currentPage)
if (settledMedia != null) {
onEvent(MediaEditScreenEvents.FocusedMediaChanged(settledMedia))
}
}
}
// During a camera-first flow, backing out of edit when the only selection is the capture itself should discard the
// capture and return to the camera rather than leaving the empty editor on the back stack.
val isOnlyCameraFirstCapture = state.cameraFirstCapture != null &&
@@ -281,9 +303,6 @@ internal fun MediaEditScreen(
selectedMedia = state.selectedMedia,
pagerState = pagerState,
enabled = !isInteracting,
onFocusedMediaChange = {
onEvent(MediaEditScreenEvents.FocusedMediaChanged(it))
},
onThumbnailClick = { index ->
if (pagerState.currentPage == index) {
onEvent(MediaEditScreenEvents.RemoveMedia(state.selectedMedia[index]))
@@ -29,7 +29,6 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -42,8 +41,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import org.signal.core.models.media.Media
import org.signal.core.ui.compose.DayNightPreviews
@@ -67,12 +64,14 @@ private val MAX_PADDING = 8.dp
/**
* Horizontally scrollable thumbnail strip that syncs with [pagerState].
* Features fish-eye padding effect where the centered item has more padding.
*
* Dragging the strip scrolls the pager, and which media the settled page focuses is reported by the caller from
* [pagerState] rather than from here, since this row is only one of the chromes the pager can be swiped beneath.
*/
@Composable
internal fun ThumbnailRow(
selectedMedia: List<Media>,
pagerState: PagerState,
onFocusedMediaChange: (Media) -> Unit,
onThumbnailClick: (Int) -> Unit = {},
onReorder: (fromIndex: Int, toIndex: Int) -> Unit = { _, _ -> },
enabled: Boolean = true
@@ -102,22 +101,6 @@ internal fun ThumbnailRow(
pagerState.dispatchRawDelta(-scaledDelta)
}
// Read through the latest selection rather than the one captured when the effect started, since reordering changes
// which media a settled page refers to without restarting the effect.
val currentSelectedMedia by rememberUpdatedState(selectedMedia)
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.isScrollInProgress }
.filter { !it }
.drop(1)
.collectLatest {
val settledPage = pagerState.currentPage
if (settledPage in currentSelectedMedia.indices) {
onFocusedMediaChange(currentSelectedMedia[settledPage])
}
}
}
// The rail's scroll position belongs to the drag rather than the pager from the moment an item is picked up until the
// reorder it produced has landed in state. Resuming any earlier means syncing to the pre-drag order and then having to
// correct once the new order arrives. The single catch-up afterwards is animated so the rail glides to the dropped
@@ -169,10 +152,6 @@ internal fun ThumbnailRow(
else -> pagerState.currentPage
}
pagerState.animateScrollToPage(targetPage)
if (targetPage in selectedMedia.indices) {
onFocusedMediaChange(selectedMedia[targetPage])
}
}
}
)
@@ -275,8 +254,7 @@ private fun ThumbnailRowPreview() {
Previews.Preview {
ThumbnailRow(
selectedMedia = media,
pagerState = pagerState,
onFocusedMediaChange = { }
pagerState = pagerState
)
}
}