fix(saved-devices): preserve targeted transfer direction

This commit is contained in:
2026-08-14 09:17:50 +02:00
parent 0ea9a8e49c
commit ce8f19b9ff
15 changed files with 103 additions and 24 deletions

View File

@@ -44,6 +44,8 @@ class SavedDeviceCoreContractBindingHygieneTest {
assertTrue(
source.contains("public expect fun `savedDeviceCapabilities`(): SavedDeviceCapabilities"),
)
assertTrue(source.contains("var `role`: TargetedTransferRole"))
assertTrue(source.contains("public enum class TargetedTransferRole"))
assertTrue(
source.contains("SavedDevice"),
"SavedDevice model must remain on the public surface",

View File

@@ -33,6 +33,7 @@ import uniffi.vnidrop.SourceKind
import uniffi.vnidrop.StoredTransfer
import uniffi.vnidrop.TargetedOfferResponse
import uniffi.vnidrop.TargetedTransfer
import uniffi.vnidrop.TargetedTransferRole
import uniffi.vnidrop.TargetedTransferState
import uniffi.vnidrop.TicketInspection
import uniffi.vnidrop.TransferMetadata
@@ -643,6 +644,7 @@ private fun PendingTargetedOffer.toModel(): PendingTargetedOfferModel = PendingT
private fun TargetedTransfer.toModel(): TargetedTransferModel = TargetedTransferModel(
id = id,
role = role.toModel(),
senderEndpointId = senderEndpointId,
receiverEndpointId = receiverEndpointId,
manifestId = manifestId,
@@ -655,6 +657,11 @@ private fun TargetedTransfer.toModel(): TargetedTransferModel = TargetedTransfer
updatedAt = updatedAt,
)
private fun TargetedTransferRole.toModel(): TargetedTransferRoleModel = when (this) {
TargetedTransferRole.SENDER -> TargetedTransferRoleModel.Sender
TargetedTransferRole.RECEIVER -> TargetedTransferRoleModel.Receiver
}
private fun TargetedTransferState.toModel(): TargetedTransferStateModel = when (this) {
TargetedTransferState.PREPARING -> TargetedTransferStateModel.Preparing
TargetedTransferState.OFFERING -> TargetedTransferStateModel.Offering

View File

@@ -68,8 +68,14 @@ enum class TargetedTransferStateModel {
Deleted,
}
enum class TargetedTransferRoleModel {
Sender,
Receiver,
}
data class TargetedTransferModel(
val id: String,
val role: TargetedTransferRoleModel,
val senderEndpointId: String,
val receiverEndpointId: String,
val manifestId: String,

View File

@@ -12,6 +12,7 @@ import com.vnidrop.app.core.ReceiveFolder
import com.vnidrop.app.core.SavedDeviceModel
import com.vnidrop.app.core.TargetedOfferResponseModel
import com.vnidrop.app.core.TargetedTransferModel
import com.vnidrop.app.core.TargetedTransferRoleModel
import com.vnidrop.app.core.TargetedTransferStateModel
import com.vnidrop.app.preferences.PreferencesRepository
import com.vnidrop.app.ui.feedback.UiMessage
@@ -365,8 +366,7 @@ class SavedDevicesViewModel(
}
private fun TargetedTransferModel.toExperienceItem(savedNames: Map<String, String>): SavedDeviceTransferItem {
val localEndpointId = repository.state.value.status?.endpointId
val outgoing = senderEndpointId == localEndpointId
val outgoing = role == TargetedTransferRoleModel.Sender
val peerEndpointId = if (outgoing) receiverEndpointId else senderEndpointId
return SavedDeviceTransferItem(
id = id,

View File

@@ -10,6 +10,7 @@ import com.vnidrop.app.core.ReceiveFolderKind
import com.vnidrop.app.core.SavedDeviceModel
import com.vnidrop.app.core.TargetedOfferResponseModel
import com.vnidrop.app.core.TargetedTransferModel
import com.vnidrop.app.core.TargetedTransferRoleModel
import com.vnidrop.app.core.TargetedTransferStateModel
import com.vnidrop.app.preferences.AppPreferences
import com.vnidrop.app.support.FakeCoreGateway
@@ -49,7 +50,15 @@ class SavedDevicesViewModelTest {
deviceRelationships = listOf(incoming("incoming"))
savedDevices = listOf(device("peer", "Office PC", "Authenticated PC"))
pendingTargetedOffers = listOf(offer("offer", "peer"))
targetedTransfers = listOf(transfer("history", "local", "peer", TargetedTransferStateModel.AwaitingApproval))
targetedTransfers = listOf(
transfer(
"history",
TargetedTransferRoleModel.Sender,
"local",
"peer",
TargetedTransferStateModel.AwaitingApproval,
),
)
}
val viewModel = createViewModel(core)
runCurrent()
@@ -74,6 +83,29 @@ class SavedDevicesViewModelTest {
assertEquals(false, state.loadFailed)
}
@Test
fun persistedOutgoingTransferKeepsDirectionAfterIdentityReset() = runTest {
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
val core = initializedCore().apply {
targetedTransfers = listOf(
transfer(
id = "past-send",
role = TargetedTransferRoleModel.Sender,
sender = "retired-local-identity",
receiver = "peer",
state = TargetedTransferStateModel.Completed,
),
)
}
val viewModel = createViewModel(core)
runCurrent()
advanceUntilIdle()
val transfer = viewModel.state.value.targetedTransfers.single()
assertEquals(SavedDeviceTransferDirection.Outgoing, transfer.direction)
assertEquals("peer", transfer.peerEndpointId)
}
@Test
fun pairingPromptCommandsAndDismissalUseTheSameDurableLists() = runTest {
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
@@ -158,7 +190,15 @@ class SavedDevicesViewModelTest {
fun interruptedTransferResumeUsesThePlatformSinkWhenAvailable() = runTest {
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
val core = initializedCore().apply {
targetedTransfers = listOf(transfer("resume-sink", "peer", "local", TargetedTransferStateModel.Interrupted))
targetedTransfers = listOf(
transfer(
"resume-sink",
TargetedTransferRoleModel.Receiver,
"peer",
"local",
TargetedTransferStateModel.Interrupted,
),
)
}
val viewModel = createViewModel(
core,
@@ -180,9 +220,9 @@ class SavedDevicesViewModelTest {
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
val core = initializedCore().apply {
targetedTransfers = listOf(
transfer("resume", "peer", "local", TargetedTransferStateModel.Interrupted),
transfer("cancel", "local", "peer", TargetedTransferStateModel.AwaitingApproval),
transfer("delete", "peer", "local", TargetedTransferStateModel.Completed),
transfer("resume", TargetedTransferRoleModel.Receiver, "peer", "local", TargetedTransferStateModel.Interrupted),
transfer("cancel", TargetedTransferRoleModel.Sender, "local", "peer", TargetedTransferStateModel.AwaitingApproval),
transfer("delete", TargetedTransferRoleModel.Receiver, "peer", "local", TargetedTransferStateModel.Completed),
)
}
val viewModel = createViewModel(core)
@@ -336,11 +376,13 @@ class SavedDevicesViewModelTest {
private fun transfer(
id: String,
role: TargetedTransferRoleModel,
sender: String,
receiver: String,
state: TargetedTransferStateModel,
) = TargetedTransferModel(
id = id,
role = role,
senderEndpointId = sender,
receiverEndpointId = receiver,
manifestId = "manifest-$id",

View File

@@ -6,6 +6,7 @@ import com.vnidrop.app.core.SavedDeviceModel
import com.vnidrop.app.core.Share
import com.vnidrop.app.core.ShareAccessPolicy
import com.vnidrop.app.core.TargetedTransferModel
import com.vnidrop.app.core.TargetedTransferRoleModel
import com.vnidrop.app.core.TargetedTransferStateModel
import com.vnidrop.app.support.FakeCoreGateway
import com.vnidrop.app.support.FakeFilePreviewRepository
@@ -235,6 +236,7 @@ class TransferDraftViewModelTest {
private fun targeted(id: String, peerId: String) = TargetedTransferModel(
id = id,
role = TargetedTransferRoleModel.Sender,
senderEndpointId = "me",
receiverEndpointId = peerId,
manifestId = "manifest",

View File

@@ -44,6 +44,8 @@ class SavedDeviceCoreContractBindingHygieneTest {
assertTrue(
source.contains("public expect fun `savedDeviceCapabilities`(): SavedDeviceCapabilities"),
)
assertTrue(source.contains("var `role`: TargetedTransferRole"))
assertTrue(source.contains("public enum class TargetedTransferRole"))
assertTrue(
source.contains("SavedDevice"),
"SavedDevice model must remain on the public surface",