fix(transfer): finalize delivery completion

This commit is contained in:
2026-07-22 21:34:33 +02:00
parent ad9ffd63f5
commit f0b06ad1cf
20 changed files with 580 additions and 85 deletions

View File

@@ -50,7 +50,7 @@ class CoreRepository(
val transferId = model.transferId
if (transferId != null) {
when (model.phase) {
"approval" -> _signals.tryEmit(CoreSignal.ApprovalChanged(transferId))
"approval", "access" -> _signals.tryEmit(CoreSignal.ApprovalChanged(transferId))
"delivery" -> _signals.tryEmit(CoreSignal.ReceiverHistoryChanged(transferId))
}
if (model.shouldRefreshTransfers()) {

View File

@@ -35,6 +35,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.vnidrop.app.core.CoreEventModel
import com.vnidrop.app.core.ReceiverDeliveryStatus
import com.vnidrop.app.core.ReceiverRequestModel
import com.vnidrop.app.core.Transfer
import com.vnidrop.app.core.TransferStatus
import com.vnidrop.app.ui.components.PillTone
@@ -79,6 +81,7 @@ internal fun SendFloatingAction(onClick: () -> Unit, modifier: Modifier = Modifi
internal fun TransferCatalog(
transfers: List<Transfer>,
transferThumbnails: Map<ULong, ByteArray>,
receiversByTransfer: Map<ULong, List<ReceiverRequestModel>> = emptyMap(),
events: List<CoreEventModel> = emptyList(),
windowClass: WindowClass,
onOpenComposer: () -> Unit,
@@ -107,9 +110,18 @@ internal fun TransferCatalog(
)
}
items(transfers, key = Transfer::localId) { transfer ->
val activeReceiverEndpointIds = receiversByTransfer[transfer.transferId]
.orEmpty()
.filter { it.status == ReceiverDeliveryStatus.Accepted }
.mapTo(mutableSetOf()) { it.remoteEndpointId }
val progress = when (transfer.status) {
TransferStatus.Importing -> progressForTransfer(events, transfer.transferId)
TransferStatus.Sharing -> activeSendProgress(events, transfer.transferId, transfer.totalSize)
TransferStatus.Sharing -> activeSendProgress(
events,
transfer.transferId,
activeReceiverEndpointIds,
transfer.totalSize,
)
else -> null
}
TransferListItem(

View File

@@ -69,6 +69,7 @@ fun SendScreen(
TransferCatalog(
transfers = outgoingTransfers,
transferThumbnails = state.transferThumbnails,
receiversByTransfer = state.receiversByTransfer,
events = coreState.events,
windowClass = windowClass,
onOpenComposer = onOpenComposer,

View File

@@ -8,6 +8,8 @@ import com.vnidrop.app.core.FileSystemService
import com.vnidrop.app.core.PickedShareFile
import com.vnidrop.app.core.ShareAccessPolicy
import com.vnidrop.app.core.ReceiverRequestModel
import com.vnidrop.app.core.TransferDirection
import com.vnidrop.app.core.TransferStatus
import com.vnidrop.app.preferences.PreferencesRepository
import com.vnidrop.app.ui.feedback.UiMessage
import com.vnidrop.app.ui.feedback.UiMessageController
@@ -39,6 +41,7 @@ data class SendState(
val transferThumbnails: Map<ULong, ByteArray> = emptyMap(),
val detailPanel: TransferDetailPanel? = null,
val receiverHistory: List<ReceiverRequestModel> = emptyList(),
val receiversByTransfer: Map<ULong, List<ReceiverRequestModel>> = emptyMap(),
val isLoadingReceivers: Boolean = false,
val isDeleteConfirmationOpen: Boolean = false,
val isDeleting: Boolean = false,
@@ -82,15 +85,24 @@ class SendViewModel(
if (signal.transferId == _state.value.selectedTransferId) {
refreshReceivers(signal.transferId)
}
refreshReceiverStatuses(signal.transferId)
}
is CoreSignal.ApprovalChanged -> {
if (signal.transferId == _state.value.selectedTransferId) {
refreshReceivers(signal.transferId)
}
refreshReceiverStatuses(signal.transferId)
}
}
}
}
viewModelScope.launch {
coreState.map { core ->
core.transfers
.filter { it.direction == TransferDirection.Send && it.status in setOf(TransferStatus.Importing, TransferStatus.Sharing) }
.mapTo(mutableSetOf()) { it.transferId }
}.distinctUntilChanged().collect(::syncSharingReceivers)
}
viewModelScope.launch {
filePreviewRepository.previews.collect { previews ->
_state.update { it.copy(transferThumbnails = previews) }
@@ -316,4 +328,21 @@ class SendViewModel(
)
}
}
private fun syncSharingReceivers(transferIds: Set<ULong>) {
_state.update { current ->
current.copy(receiversByTransfer = current.receiversByTransfer.filterKeys { it in transferIds })
}
transferIds.forEach(::refreshReceiverStatuses)
}
private fun refreshReceiverStatuses(transferId: ULong) {
viewModelScope.launch {
repository.receiverRequests(transferId).onSuccess { requests ->
_state.update { current ->
current.copy(receiversByTransfer = current.receiversByTransfer + (transferId to requests))
}
}
}
}
}

View File

@@ -34,13 +34,6 @@ internal fun SettingsOverview(
fontWeight = FontWeight.Bold,
)
SettingsGroup {
SettingsRow(
icon = AppIcon.Storage,
title = stringResource(Res.string.storage_title),
selected = state.selectedSection == SettingsSection.Storage,
onClick = { onSectionSelected(SettingsSection.Storage) },
)
SettingsDivider()
SettingsRow(
icon = AppIcon.User,
title = stringResource(Res.string.preferences_title),
@@ -65,6 +58,13 @@ internal fun SettingsOverview(
onClick = { onSectionSelected(SettingsSection.Notifications) },
)
SettingsDivider()
SettingsRow(
icon = AppIcon.Storage,
title = stringResource(Res.string.storage_title),
selected = state.selectedSection == SettingsSection.Storage,
onClick = { onSectionSelected(SettingsSection.Storage) },
)
SettingsDivider()
SettingsRow(
icon = AppIcon.Info,
title = stringResource(Res.string.about_title),

View File

@@ -138,7 +138,8 @@ fun progressForReceiver(
detail = null,
)
}
if (latest.kind == "completed" && transferEvents.none { it.kind == "progress" || it.kind == "started" }) {
val progress = aggregateReceiverProgress(transferEvents, totalSizeHint)
if (latest.kind == "completed" && (progress == null || progress >= 0.999f)) {
return TransferProgress(
transferId = transferId,
phase = "transfer",
@@ -149,7 +150,6 @@ fun progressForReceiver(
)
}
val progress = aggregateReceiverProgress(transferEvents, totalSizeHint)
return TransferProgress(
transferId = transferId,
phase = "transfer",
@@ -167,36 +167,21 @@ fun progressForReceiver(
fun activeSendProgress(
events: List<CoreEventModel>,
transferId: ULong,
activeReceiverEndpointIds: Set<String>,
totalSizeHint: ULong? = null,
): TransferProgress? {
if (activeReceiverEndpointIds.isEmpty()) return null
val endpointIds = events
.asSequence()
.filter { it.transferId == transferId && it.direction == "send" && it.phase == "transfer" }
.mapNotNull { findString(it.dataJson, "endpoint_id") }
.filter { it in activeReceiverEndpointIds }
.distinct()
.toList()
if (endpointIds.isEmpty()) {
// Fall back to connection-scoped events without endpoint attribution.
val relevant = events.filter {
it.transferId == transferId &&
it.direction == "send" &&
it.phase == "transfer" &&
it.kind in setOf("started", "progress")
}
if (relevant.isEmpty()) return null
return TransferProgress(
transferId = transferId,
phase = "transfer",
kind = relevant.first().kind,
label = Res.string.progress_sending,
progress = aggregateReceiverProgress(relevant, totalSizeHint),
detail = progressDetail(relevant.first()),
)
}
if (endpointIds.isEmpty()) return null
return endpointIds
.mapNotNull { progressForReceiver(events, transferId, it, totalSizeHint) }
.firstOrNull { it.kind == "progress" || it.kind == "started" }
?: endpointIds.mapNotNull { progressForReceiver(events, transferId, it, totalSizeHint) }.firstOrNull()
}
fun summarizeProgress(events: List<CoreEventModel>): List<TransferProgress> =