android, desktop: open correct image in fullscreen viewer (#6869)

* android, desktop: open correct image in fullscreen viewer

Fullscreen image viewer occasionally opened a different image than
the one clicked. Root cause: when the LaunchedEffect probe at
ImageFullScreenView.kt:48-55 calls getMedia(initialIndex - 1) to check
whether a previous media item exists, getMedia returns null for both
"no item" and "item found but failed to load" (e.g. undecodable bytes,
missing file, crypto error). The probe treated null as "no previous
item" and called scrollToStart(), which rewrote initialChatId to the
chat's oldest media item - making the viewer display that oldest item
instead of the clicked one.

Fix: scrollToStart() no longer rewrites initialChatId. The pager is
still repositioned to page 0; getMedia(0) resolves against the
already-set initialChatId (the clicked item) and renders it correctly.

* android, desktop: regression test for fullscreen viewer anchor preservation

Drives the public providerForGallery interface: moves the anchor away from
cItemId via currentPageChanged, calls scrollToStart, then reads the anchor
back through onDismiss's scrollTo callback. The pre-fix code rewrote
initialChatId to the chat's oldest showable, which would surface as
scrollTo(2); the fix preserves the anchor and produces scrollTo(1).

* plan: design doc for fullscreen viewer wrong-image fix

Documents the pager state model, the root cause of the wrong-image bug,
why the one-line deletion in scrollToStart fixes it for both call sites,
and why the wider getMedia null-overload refactor is deliberately out of
scope for this fix.
This commit is contained in:
Narasimha-sc
2026-05-08 11:18:45 +00:00
committed by GitHub
parent 4d43f2d41c
commit da9b69ca0b
3 changed files with 202 additions and 1 deletions
@@ -3597,7 +3597,6 @@ fun providerForGallery(
override fun scrollToStart() {
initialIndex = 0
initialChatId = chatItems.firstOrNull { canShowMedia(it) }?.id ?: return
}
override fun onDismiss(index: Int) {
@@ -0,0 +1,67 @@
package chat.simplex.app
import chat.simplex.common.model.*
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chat.providerForGallery
import kotlinx.datetime.Clock
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
// Regression for PR #6869: scrollToStart() must not rewrite initialChatId.
class ProviderForGalleryTest {
// Synthetic items pass canShowMedia only when chatModel.connectedToRemote() is true.
@BeforeTest
fun connectChatModelToRemote() {
chatModel.currentRemoteHost.value = RemoteHostInfo(
remoteHostId = 0L,
hostDeviceName = "",
storePath = "",
bindAddress_ = null,
bindPort_ = null,
sessionState = null,
)
}
@AfterTest
fun resetChatModel() {
chatModel.currentRemoteHost.value = null
}
@Test
fun testScrollToStartPreservesAnchor() {
val items = listOf(imageItem(1L), imageItem(2L), imageItem(3L))
var scrolledTo: Int? = null
val provider = providerForGallery(items, cItemId = 3L) { scrolledTo = it }
provider.currentPageChanged(provider.initialIndex - 1)
provider.scrollToStart()
provider.onDismiss(0)
assertEquals(1, scrolledTo)
}
// Pins the onDismiss early-return contract that testScrollToStartPreservesAnchor
// relies on to read the anchor back through the scrollTo callback.
@Test
fun testOnDismissOnActiveItemDoesNotScroll() {
val items = listOf(imageItem(1L), imageItem(2L), imageItem(3L))
var scrolledTo: Int? = null
val provider = providerForGallery(items, cItemId = 3L) { scrolledTo = it }
provider.onDismiss(provider.initialIndex)
assertEquals(null, scrolledTo)
}
private fun imageItem(id: Long): ChatItem =
ChatItem(
chatDir = CIDirection.DirectRcv(),
meta = CIMeta.getSample(id, Clock.System.now(), text = ""),
content = CIContent.RcvMsgContent(MsgContent.MCImage(text = "", image = "")),
reactions = emptyList(),
file = CIFile.getSample(fileId = id, fileName = "img-$id.jpg", filePath = "img-$id.jpg"),
)
}