From 9ffdaeaa5088698173e5e307cde6c7ff637d2190 Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Sun, 12 Jul 2026 16:33:48 +0200 Subject: [PATCH] fix(ui): drop redundant stop-sharing UI and fix username field Remove the transfer-details progress bar and Stop sharing control now that delete already revokes a share. Fix preferences username typing by keeping local draft state and debouncing DataStore writes so trim/races no longer fight the caret. --- .../com/vnidrop/app/feature/send/SendRoute.kt | 1 - .../vnidrop/app/feature/send/SendScreen.kt | 6 ---- .../vnidrop/app/feature/send/SendViewModel.kt | 16 ----------- .../app/feature/send/TransferDetails.kt | 28 ------------------- .../app/feature/settings/SettingsViewModel.kt | 28 +++++++++++++++---- .../com/vnidrop/app/feature/ViewModelsTest.kt | 25 +++++++++++++++++ .../kotlin/com/vnidrop/app/support/Fakes.kt | 4 ++- 7 files changed, 51 insertions(+), 57 deletions(-) diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendRoute.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendRoute.kt index 180837a..2312276 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendRoute.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendRoute.kt @@ -53,6 +53,5 @@ fun SendRoute( onRequestDelete = viewModel::requestDeleteTransfer, onDismissDelete = viewModel::dismissDeleteTransfer, onConfirmDelete = viewModel::confirmDeleteTransfer, - onCancelTransfer = viewModel::cancelSelectedTransfer, ) } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendScreen.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendScreen.kt index 9e52afd..5b9d9d8 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendScreen.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendScreen.kt @@ -14,8 +14,6 @@ import com.vnidrop.app.core.ShareAccessPolicy import com.vnidrop.app.core.TransferDirection import com.vnidrop.app.ui.components.AdaptiveDrawer import com.vnidrop.app.ui.state.WindowClass -import com.vnidrop.app.ui.state.canCancelTransfer -import com.vnidrop.app.ui.state.progressForTransfer @Composable fun SendScreen( @@ -42,7 +40,6 @@ fun SendScreen( onRequestDelete: () -> Unit = {}, onDismissDelete: () -> Unit = {}, onConfirmDelete: () -> Unit = {}, - onCancelTransfer: () -> Unit = {}, ) { val outgoingTransfers = coreState.transfers.filter { it.direction == TransferDirection.Send } val selectedTransfer = state.selectedTransferId?.let { id -> outgoingTransfers.firstOrNull { it.transferId == id } } @@ -56,18 +53,15 @@ fun SendScreen( TransferDetails( transfer = selectedTransfer, events = coreState.events, - progress = progressForTransfer(coreState.events, selectedTransfer.transferId), pendingReceivers = state.receiverHistory.count { it.status == ReceiverDeliveryStatus.Requested || it.status == ReceiverDeliveryStatus.Accepted }, completedReceivers = state.receiverHistory.count { it.status == ReceiverDeliveryStatus.Completed }, - canCancel = selectedTransfer.canCancelTransfer(), onBack = onCloseTransferDetails, onActivity = onActivity, onReceivers = onReceivers, onShare = onShare, onDelete = onRequestDelete, - onCancel = onCancelTransfer, ) } else { TransferCatalog( diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendViewModel.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendViewModel.kt index 4f25dc7..ce274e1 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendViewModel.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendViewModel.kt @@ -25,7 +25,6 @@ import kotlinx.coroutines.launch import vnidrop.shared.generated.resources.Res import vnidrop.shared.generated.resources.send_transfer_created import vnidrop.shared.generated.resources.transfer_deleted -import vnidrop.shared.generated.resources.transfer_event_stopped import vnidrop.shared.generated.resources.transfer_nfc_written data class SendState( @@ -252,21 +251,6 @@ class SendViewModel( } } - fun cancelSelectedTransfer() { - val transferId = _state.value.selectedTransferId ?: return - viewModelScope.launch { - repository.cancel(transferId).fold( - onSuccess = { - repository.refresh() - messages.tryShow( - UiMessage(UiText.Resource(Res.string.transfer_event_stopped), UiMessageTone.Info), - ) - }, - onFailure = messages::error, - ) - } - } - private fun sendEffect(effect: SendEffect) { viewModelScope.launch { effects.send(effect) } } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt index 702e2a7..db61e0e 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt @@ -50,9 +50,7 @@ import com.vnidrop.app.core.Transfer import com.vnidrop.app.ui.components.AppCard import com.vnidrop.app.ui.components.DestructiveButton import com.vnidrop.app.ui.components.PrimaryButton -import com.vnidrop.app.ui.components.ProgressRow import com.vnidrop.app.ui.components.SecondaryButton -import com.vnidrop.app.ui.state.TransferProgress import com.vnidrop.app.ui.state.displayNameForStatus import com.vnidrop.app.ui.state.formatBytes import com.vnidrop.app.ui.theme.LocalVniDropColors @@ -69,16 +67,13 @@ enum class InvitationAction { Export, Share, Nfc } internal fun TransferDetails( transfer: Transfer, events: List, - progress: TransferProgress? = null, pendingReceivers: Int = 0, completedReceivers: Int, - canCancel: Boolean = false, onBack: () -> Unit, onActivity: () -> Unit, onReceivers: () -> Unit, onShare: () -> Unit, onDelete: () -> Unit, - onCancel: () -> Unit = {}, ) { LazyColumn( modifier = Modifier.fillMaxSize().statusBarsPadding(), @@ -106,24 +101,6 @@ internal fun TransferDetails( DetailValue(stringResource(Res.string.metadata_size), formatBytes(transfer.totalSize)) HorizontalDivider(color = LocalVniDropColors.current.borderDefault) DetailValue(stringResource(Res.string.send_access_title), accessPolicyLabel(transfer.accessPolicy)) - if (progress != null && transfer.status.isLiveProgressStatus()) { - HorizontalDivider(color = LocalVniDropColors.current.borderDefault) - ProgressRow( - label = progress.label, - progress = progress.progress, - detail = progress.detail, - modifier = Modifier.padding(top = 8.dp), - ) - } - } - } - if (canCancel) { - item { - SecondaryButton( - stringResource(Res.string.button_stop_sharing), - onClick = onCancel, - modifier = Modifier.fillMaxWidth(), - ) } } item { @@ -163,11 +140,6 @@ private fun receiversDescription(pending: Int, completed: Int): String = when { else -> stringResource(Res.string.transfer_receivers_description) } -private fun com.vnidrop.app.core.TransferStatus.isLiveProgressStatus(): Boolean = - this == com.vnidrop.app.core.TransferStatus.Importing || - this == com.vnidrop.app.core.TransferStatus.Sharing || - this == com.vnidrop.app.core.TransferStatus.Receiving - @Composable private fun DetailDestination(title: String, description: String, count: Int? = null, onClick: () -> Unit) { Row( diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt index 042473a..377763c 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt @@ -16,8 +16,10 @@ import com.vnidrop.app.ui.feedback.UiMessageController import com.vnidrop.app.ui.feedback.UiMessageTone import com.vnidrop.app.ui.feedback.UiText import com.vnidrop.app.ui.theme.ThemeMode +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -71,19 +73,26 @@ class SettingsViewModel( private val effects = Channel(Channel.BUFFERED) val effectFlow = effects.receiveAsFlow() private var enableNotificationsAfterSettings = false + private var usernamePersistJob: Job? = null init { viewModelScope.launch { preferencesRepository.preferences.collect { preferences -> - _state.update { - it.copy( - username = preferences.username, + val previousFolder = _state.value.receiveFolder + // While the user is typing, keep the in-progress value. DataStore + // echoes can race keystrokes and trim trailing spaces mid-edit. + val editingUsername = usernamePersistJob?.isActive == true + _state.update { current -> + current.copy( + username = if (editingUsername) current.username else preferences.username, receiveFolder = preferences.receiveFolder, themeMode = preferences.themeMode, notificationsEnabled = preferences.notificationsEnabled, ) } - validateFolder(preferences.receiveFolder) + if (preferences.receiveFolder != previousFolder) { + validateFolder(preferences.receiveFolder) + } } } refreshNotificationPermission() @@ -96,7 +105,12 @@ class SettingsViewModel( } fun setUsername(value: String) { - viewModelScope.launch { preferencesRepository.setUsername(value) } + _state.update { it.copy(username = value) } + usernamePersistJob?.cancel() + usernamePersistJob = viewModelScope.launch { + delay(UsernamePersistDebounceMs) + preferencesRepository.setUsername(value) + } } fun setThemeMode(mode: ThemeMode) { @@ -203,4 +217,8 @@ class SettingsViewModel( val status = fileSystemService.validateReceiveFolder(folder) _state.update { it.copy(folderAccessStatus = status, isValidatingFolder = false) } } + + private companion object { + const val UsernamePersistDebounceMs = 350L + } } diff --git a/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt b/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt index 34af7a7..0705a26 100644 --- a/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt +++ b/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt @@ -80,6 +80,31 @@ class ViewModelsTest { assertEquals(1, notifications.cancelAllCount) } + @Test + fun settingsUsernameKeepsSpacesWhileTypingAndPersistsAfterDebounce() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + val preferences = preferences() + val viewModel = SettingsViewModel( + environment(), + { DeviceInfo("Device", null, "OS", null, null) }, + FakeFileSystemService(folder), + preferences, + FakeNotificationService(), + UiMessageController(), + ) + advanceUntilIdle() + + viewModel.setUsername("Ada ") + // Immediate local state must keep the trailing space so multi-word names can be typed. + assertEquals("Ada ", viewModel.state.value.username) + assertEquals("Receiver", preferences.mutablePreferences.value.username) + + testScheduler.advanceTimeBy(400) + advanceUntilIdle() + assertEquals("Ada", preferences.mutablePreferences.value.username) + assertEquals("Ada", viewModel.state.value.username) + } + @Test fun settingsKeepsNotificationsDisabledWhenPermissionIsDenied() = runTest { Dispatchers.setMain(StandardTestDispatcher(testScheduler)) diff --git a/shared/src/commonTest/kotlin/com/vnidrop/app/support/Fakes.kt b/shared/src/commonTest/kotlin/com/vnidrop/app/support/Fakes.kt index 0171acb..edbbea1 100644 --- a/shared/src/commonTest/kotlin/com/vnidrop/app/support/Fakes.kt +++ b/shared/src/commonTest/kotlin/com/vnidrop/app/support/Fakes.kt @@ -169,7 +169,9 @@ class FakePreferencesRepository( ) : PreferencesRepository { val mutablePreferences = MutableStateFlow(initial) override val preferences = mutablePreferences - override suspend fun setUsername(username: String) { mutablePreferences.value = mutablePreferences.value.copy(username = username) } + override suspend fun setUsername(username: String) { + mutablePreferences.value = mutablePreferences.value.copy(username = username.trim()) + } override suspend fun setReceiveFolder(folder: ReceiveFolder) { mutablePreferences.value = mutablePreferences.value.copy(receiveFolder = folder) } override suspend fun resetReceiveFolder() = Unit override suspend fun setThemeMode(mode: ThemeMode) { mutablePreferences.value = mutablePreferences.value.copy(themeMode = mode) }