diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/CreateGroupViewModel.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/CreateGroupViewModel.kt index ff9fafea64..f9ad7c9b71 100644 --- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/CreateGroupViewModel.kt +++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/CreateGroupViewModel.kt @@ -375,8 +375,11 @@ internal class CreateGroupViewModel @Inject constructor( ).onSuccess { chat -> // The chat exists, so the key has done its job — a later Create is a second group. attempt.clear() - // The cached local copy is redundant now that the blob is the chat's picture. - discardPendingImage() + // The cached local copy is redundant now that the blob is the chat's picture, but the + // form is still on screen for the length of the hold below and its tile is still + // drawing from that file. Discarding it here crossfaded the picture back to the empty + // camera tile a beat before the flow left for the chat, so the pick read as dropped at + // the moment it was accepted. `onCleared` deletes it once the flow entry has popped. dispatchSuccessThen(Event.UpdateProcessingState(success = true)) { dispatchEvent(Event.ChatCreated(chat)) dispatchEvent(Event.UpdateProcessingState()) diff --git a/apps/flipcash/features/tipping/src/test/kotlin/com/flipcash/app/tipping/internal/CreateGroupViewModelErrorTest.kt b/apps/flipcash/features/tipping/src/test/kotlin/com/flipcash/app/tipping/internal/CreateGroupViewModelErrorTest.kt index da55e51811..aaa69ce648 100644 --- a/apps/flipcash/features/tipping/src/test/kotlin/com/flipcash/app/tipping/internal/CreateGroupViewModelErrorTest.kt +++ b/apps/flipcash/features/tipping/src/test/kotlin/com/flipcash/app/tipping/internal/CreateGroupViewModelErrorTest.kt @@ -28,6 +28,7 @@ import com.getcode.solana.keys.Mint import com.getcode.solana.keys.PublicKey import com.getcode.util.resources.ContentReader import com.getcode.util.resources.FakeResourceHelper +import com.getcode.view.SuccessHoldDuration import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -36,6 +37,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.After @@ -308,4 +310,43 @@ class CreateGroupViewModelErrorTest { assertEquals(2, keys.size) assertTrue(!keys[0].bytes.contentEquals(keys[1].bytes)) } + + /** + * The checkmark is held for half a second before the flow leaves for the chat, and the form is + * on screen for all of it. The create's own cache cleanup used to run at the top of that hold, + * which crossfaded the picture back to the empty camera tile a beat before the screen went + * away — the pick reading as dropped at the moment it was accepted. + * + * So the local copy outlives the screen that shows it, exactly as the photo-selection step + * leaves its own pick to `onCleared`: the flow entry pops, the shared view model clears, and + * the file goes with it. + */ + @Test + fun `a created group keeps its picture until the form is gone`() = + runTest(mainCoroutineRule.dispatcher) { + dispatchers = TestDispatchers(testScheduler) + val vm = createViewModel() + vm.completeDraft() + + val uri = mockk(relaxed = true) + every { contentReader.readBytes(uri) } returns ByteArray(8) + coEvery { blobStorage.upload(any(), any()) } returns + Result.success(BlobId(ByteArray(32) { 5 })) + vm.dispatchEvent(CreateGroupViewModel.Event.OnImageCached(uri, "image/jpeg")) + coEvery { chatCoordinator.create(any(), any()) } returns Result.success(created()) + + vm.dispatchEvent(CreateGroupViewModel.Event.CreateRequested) + advanceTimeBy(SuccessHoldDuration / 2) + + // Mid-hold: the button is showing success and the picture is still under it. + assertTrue(vm.stateFlow.value.processingState.success) + assertEquals(uri, vm.stateFlow.value.image.dataOrNull) + // And the file it points at is still there — it is what the tile is drawing from. + coVerify(exactly = 0) { contentReader.removeFromCache(uri) } + + advanceUntilIdle() + + assertEquals(created(), vm.stateFlow.value.created) + assertEquals(uri, vm.stateFlow.value.image.dataOrNull) + } }