Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .skills/compose-ui/strings-index.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,10 @@
<string name="remove_ignored">Remove from ignored</string>
<string name="remove_layer">Remove Layer</string>
<string name="remove_node_text">This node will be removed from your list until your node receives data from it again.</string>
<plurals name="remove_nodes_text">
<item quantity="one">%1$d node will be removed from your list until your node receives data from it again.</item>
<item quantity="other">%1$d nodes will be removed from your list until your node receives data from them again.</item>
</plurals>
<string name="replace">Replace</string>
<string name="replace_channels_and_settings_description">This QR code contains a complete configuration. This will REPLACE your existing channels and radio settings. All existing channels will be removed.</string>
<string name="reply">Reply</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
*/
package org.meshtastic.feature.node.detail

import androidx.compose.material3.Text
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.pluralStringResource
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.safeCatching
Expand All @@ -43,6 +45,7 @@ import org.meshtastic.core.resources.mute_notifications
import org.meshtastic.core.resources.mute_remove
import org.meshtastic.core.resources.remove
import org.meshtastic.core.resources.remove_node_text
import org.meshtastic.core.resources.remove_nodes_text
import org.meshtastic.core.resources.unmute
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.core.ui.util.SnackbarManager
Expand All @@ -67,6 +70,17 @@ constructor(
)
}

/** One alert for the batch: [AlertManager] holds a single alert, so per-node requests replace each other. */
open fun requestRemoveNodes(scope: CoroutineScope, nodes: List<Node>) {
if (nodes.isEmpty()) return
if (nodes.size == 1) return requestRemoveNode(scope, nodes.single())
alertManager.showAlert(
titleRes = Res.string.remove,
composableMessage = { Text(pluralStringResource(Res.plurals.remove_nodes_text, nodes.size, nodes.size)) },
onConfirm = { launchRadioMutation(scope, "removeNodes") { nodes.forEach { removeNode(it.num) } } },
Comment thread
jamesarich marked this conversation as resolved.
)
}

open suspend fun removeNode(nodeNum: Int) {
Logger.i { "Removing node '$nodeNum'" }
val packetId = radioController.generatePacketId()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ fun NodeListScreen(
onTraceRoute = { viewModel.traceRoute(node) },
lastTracerouteTime = lastTracerouteTime,
onIgnore = { viewModel.ignoreNode(node) },
onRemove = { viewModel.removeNode(node) },
onRemove = { viewModel.removeNodes(listOf(node)) },
onDismiss = { expanded = false },
)
}
Expand Down Expand Up @@ -347,7 +347,7 @@ fun NodeListScreen(
UnheardNodesBanner(
unheardNodes = unheardNodes,
onKeep = { viewModel.keepUnheardNodes(unheardNodes) },
onRemoveAll = { unheardNodes.forEach(viewModel::removeNode) },
onRemoveAll = { viewModel.removeNodes(unheardNodes) },
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp),
)
val filterPrefs = viewModel.nodeFilterPreferences
Expand Down Expand Up @@ -462,7 +462,7 @@ private fun NodeListEmptyState(
* node may simply be out of range rather than on another channel — it comes back on its own when next heard.
*
* Keep is saved per node, so the banner stays away across launches and returns only for nodes not yet kept. Remove
* hides it for this composition, since each removal still waits on its own confirmation.
* hides it for this composition, since the removal still waits on its confirmation.
*/
@Composable
private fun UnheardNodesBanner(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ class NodeListViewModel(

fun muteNode(node: Node) = nodeManagementActions.requestMuteNode(viewModelScope, node)

fun removeNode(node: Node) = nodeManagementActions.requestRemoveNode(viewModelScope, node)
fun removeNodes(nodes: List<Node>) = nodeManagementActions.requestRemoveNodes(viewModelScope, nodes)

/** Returns the contact key for navigating to a direct message conversation with this node. */
fun getDirectMessageRoute(node: Node): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ class NodeManagementActionsTest {
assertTrue(afterRemoveCalled)
}

@Test
fun requestRemoveNodes_removes_every_node_after_one_confirmation() {
val realAlertManager = AlertManager()
val actionsWithRealAlert = actionsWith(radioController, realAlertManager)
val nodes = listOf(1, 2, 3).map { Node(num = it) }
nodeRepository.setNodes(nodes + Node(num = 4))

actionsWithRealAlert.requestRemoveNodes(testScope, nodes)
realAlertManager.currentAlert.value?.onConfirm?.invoke()
testScope.runCurrent()

assertEquals(setOf(4), nodeRepository.nodeDBbyNum.value.keys)
}

@Test
fun requestRemoveNode_success_callback_failure_is_not_reported_as_radio_rejection() {
val realAlertManager = AlertManager()
Expand Down