@@ -16,7 +16,9 @@ import kotlinx.coroutines.flow.SharingStarted
16import kotlinx.coroutines.flow.StateFlow
16import kotlinx.coroutines.flow.StateFlow
17import kotlinx.coroutines.flow.asSharedFlow
17import kotlinx.coroutines.flow.asSharedFlow
18import kotlinx.coroutines.flow.asStateFlow
18import kotlinx.coroutines.flow.asStateFlow
19+import kotlinx.coroutines.flow.combine
19import kotlinx.coroutines.flow.stateIn
20import kotlinx.coroutines.flow.stateIn
21+import kotlinx.coroutines.flow.update
20import kotlinx.coroutines.launch
22import kotlinx.coroutines.launch
22/** A transient error surfaced as a snackbar, optionally paired with the profile that
24/** A transient error surfaced as a snackbar, optionally paired with the profile that
@@ -25,8 +27,15 @@ data class ConnectError(val message: String, val profile: ServerProfile?)
26class ServerListViewModel(private val container: AppContainer) : ViewModel() {
28class ServerListViewModel(private val container: AppContainer) : ViewModel() {
30+ /** Ids optimistically hidden while their deferred delete's undo window is open, so a
31+ * "deleted" row disappears immediately instead of lingering (tappable) for the whole
32+ * undo window. Mirrors [SessionListViewModel]'s optimistic-removal pattern. */
33+ private val _hiddenIds = MutableStateFlow<Set<String>>(emptySet())
28 val profiles: StateFlow<List<ServerProfile>> =
35 val profiles: StateFlow<List<ServerProfile>> =
29− container.profileStore.profiles.stateIn(
36+ combine(container.profileStore.profiles, _hiddenIds) { profiles, hidden ->
37+ profiles.filterNot { it.id in hidden }
30 scope = viewModelScope,
39 scope = viewModelScope,
31 started = SharingStarted.WhileSubscribed(NetworkConfig.stateFlowSubscriptionTimeoutMs),
40 started = SharingStarted.WhileSubscribed(NetworkConfig.stateFlowSubscriptionTimeoutMs),
32 initialValue = emptyList(),
41 initialValue = emptyList(),
@@ -96,6 +105,8 @@ class ServerListViewModel(private val container: AppContainer) : ViewModel() {
97 fun delete(profile: ServerProfile) {
106 fun delete(profile: ServerProfile) {
98 pendingDeletes[profile.id] = profile
107 pendingDeletes[profile.id] = profile
108+ // Optimistically hide the row so the UI feels instant; undo re-shows it.
109+ _hiddenIds.update { it + profile.id }
99 if (container.activeConnection.value?.profile?.id == profile.id) {
110 if (container.activeConnection.value?.profile?.id == profile.id) {
100 // disconnect() is suspend; run it on the VM scope so the optimistic removal
111 // disconnect() is suspend; run it on the VM scope so the optimistic removal
101 // and undo emission aren't blocked on the connection close completing.
112 // and undo emission aren't blocked on the connection close completing.
@@ -106,13 +117,23 @@ class ServerListViewModel(private val container: AppContainer) : ViewModel() {
106 delay(NetworkConfig.undoServerDeleteDelayMs)
117 delay(NetworkConfig.undoServerDeleteDelayMs)
107 if (pendingDeletes.remove(profile.id) != null) {
118 if (pendingDeletes.remove(profile.id) != null) {
108 runCatchingCancellable { container.profileStore.delete(profile.id) }
119 runCatchingCancellable { container.profileStore.delete(profile.id) }
121+ // Drop the optimistic hide only after the store has emitted the
122+ // removal, so the row doesn't flash back before the list updates.
123+ _hiddenIds.update { it - profile.id }
126+ // Delete failed: re-show the row so it isn't hidden forever.
127+ _hiddenIds.update { it - profile.id }
109− .onFailure { _errorEvents.tryEmit(ConnectError(container.friendlyError(it), null)) }
128+ _errorEvents.tryEmit(ConnectError(container.friendlyError(it), null))
114− /** Cancel a pending delete (Undo snackbar action). */
134+ /** Cancel a pending delete (Undo snackbar action) and re-show the hidden row. */
115 fun undoDelete(profileId: String) {
135 fun undoDelete(profileId: String) {
116 pendingDeletes.remove(profileId)
136 pendingDeletes.remove(profileId)
137+ _hiddenIds.update { it - profileId }