Skip to content

Commit ea6d1ff

Browse files
authored
feat: consolidate dialogs (meshtastic#4506)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
1 parent 7bcc518 commit ea6d1ff

59 files changed

Lines changed: 2034 additions & 1651 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ This file serves as a comprehensive guide for AI agents and developers working o
4141

4242
Text(text = stringResource(Res.string.your_string_key))
4343
```
44+
- **Dialogs:**
45+
- Use the centralized `MeshtasticDialog` for all alerts and confirmation boxes.
46+
- **Specialized Overloads:** Use `MeshtasticResourceDialog` (for resource-only content) or `MeshtasticTextDialog` (for mixed resource/text content) to reduce boilerplate.
47+
- **Location:** Defined in `core/ui/src/main/kotlin/org/meshtastic/core/ui/component/AlertDialogs.kt`.
4448
- **Previews:** Create `@Preview` functions for your Composables to ensure they render correctly.
4549

4650
### B. Architecture & State

app/src/main/java/com/geeksville/mesh/MainActivity.kt

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,10 @@ import com.geeksville.mesh.ui.MainScreen
4747
import dagger.hilt.android.AndroidEntryPoint
4848
import kotlinx.coroutines.launch
4949
import org.meshtastic.core.datastore.UiPreferencesDataSource
50-
import org.meshtastic.core.model.util.handleMeshtasticUri
50+
import org.meshtastic.core.model.util.dispatchMeshtasticUri
5151
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
5252
import org.meshtastic.core.strings.Res
5353
import org.meshtastic.core.strings.channel_invalid
54-
import org.meshtastic.core.strings.contact_invalid
5554
import org.meshtastic.core.ui.theme.AppTheme
5655
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
5756
import org.meshtastic.core.ui.util.showToast
@@ -157,20 +156,10 @@ class MainActivity : AppCompatActivity() {
157156

158157
private fun handleMeshtasticUri(uri: Uri) {
159158
Logger.d { "Handling Meshtastic URI: $uri" }
160-
handleMeshtasticUri(
161-
uri = uri,
162-
onChannel = {
163-
model.requestChannelUrl(
164-
url = it,
165-
onFailure = { lifecycleScope.launch { showToast(Res.string.channel_invalid) } },
166-
)
167-
},
168-
onContact = {
169-
model.setSharedContactRequested(
170-
url = it,
171-
onFailure = { lifecycleScope.launch { showToast(Res.string.contact_invalid) } },
172-
)
173-
},
159+
uri.dispatchMeshtasticUri(
160+
onChannel = { model.setRequestChannelSet(it) },
161+
onContact = { model.setSharedContactRequested(it) },
162+
onInvalid = { lifecycleScope.launch { showToast(Res.string.channel_invalid) } },
174163
)
175164
}
176165

app/src/main/java/com/geeksville/mesh/model/UIState.kt

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.map
4343
import kotlinx.coroutines.flow.mapNotNull
4444
import kotlinx.coroutines.flow.onEach
4545
import kotlinx.coroutines.flow.shareIn
46+
import org.jetbrains.compose.resources.StringResource
4647
import org.jetbrains.compose.resources.getString
4748
import org.meshtastic.core.analytics.platform.PlatformAnalytics
4849
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
@@ -54,15 +55,17 @@ import org.meshtastic.core.database.entity.asDeviceVersion
5455
import org.meshtastic.core.datastore.UiPreferencesDataSource
5556
import org.meshtastic.core.model.TracerouteMapAvailability
5657
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
57-
import org.meshtastic.core.model.util.toChannelSet
58+
import org.meshtastic.core.model.util.dispatchMeshtasticUri
5859
import org.meshtastic.core.service.IMeshService
5960
import org.meshtastic.core.service.MeshServiceNotifications
6061
import org.meshtastic.core.service.ServiceRepository
6162
import org.meshtastic.core.service.TracerouteResponse
6263
import org.meshtastic.core.strings.Res
6364
import org.meshtastic.core.strings.client_notification
65+
import org.meshtastic.core.strings.compromised_keys
6466
import org.meshtastic.core.ui.component.ScrollToTopEvent
65-
import org.meshtastic.core.ui.component.toSharedContact
67+
import org.meshtastic.core.ui.util.AlertManager
68+
import org.meshtastic.core.ui.util.ComposableContent
6669
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
6770
import org.meshtastic.proto.ChannelSet
6871
import org.meshtastic.proto.ClientNotification
@@ -114,6 +117,7 @@ constructor(
114117
private val meshServiceNotifications: MeshServiceNotifications,
115118
private val analytics: PlatformAnalytics,
116119
packetRepository: PacketRepository,
120+
private val alertManager: AlertManager,
117121
) : ViewModel() {
118122

119123
val theme: StateFlow<Int> = uiPreferencesDataSource.theme
@@ -142,17 +146,7 @@ constructor(
142146
_scrollToTopEventFlow.tryEmit(event)
143147
}
144148

145-
data class AlertData(
146-
val title: String,
147-
val message: String? = null,
148-
val html: String? = null,
149-
val onConfirm: (() -> Unit)? = null,
150-
val onDismiss: (() -> Unit)? = null,
151-
val choices: Map<String, () -> Unit> = emptyMap(),
152-
)
153-
154-
private val _currentAlert: MutableStateFlow<AlertData?> = MutableStateFlow(null)
155-
val currentAlert = _currentAlert.asStateFlow()
149+
val currentAlert = alertManager.currentAlert
156150

157151
fun tracerouteMapAvailability(forwardRoute: List<Int>, returnRoute: List<Int>): TracerouteMapAvailability =
158152
evaluateTracerouteMapAvailability(
@@ -163,29 +157,39 @@ constructor(
163157
)
164158

165159
fun showAlert(
166-
title: String,
160+
title: String? = null,
161+
titleRes: StringResource? = null,
167162
message: String? = null,
163+
messageRes: StringResource? = null,
164+
composableMessage: ComposableContent? = null,
168165
html: String? = null,
169166
onConfirm: (() -> Unit)? = {},
170-
dismissable: Boolean = true,
167+
onDismiss: (() -> Unit)? = null,
168+
confirmText: String? = null,
169+
confirmTextRes: StringResource? = null,
170+
dismissText: String? = null,
171+
dismissTextRes: StringResource? = null,
171172
choices: Map<String, () -> Unit> = emptyMap(),
172173
) {
173-
_currentAlert.value =
174-
AlertData(
175-
title = title,
176-
message = message,
177-
html = html,
178-
onConfirm = {
179-
onConfirm?.invoke()
180-
dismissAlert()
181-
},
182-
onDismiss = { if (dismissable) dismissAlert() },
183-
choices = choices,
184-
)
174+
alertManager.showAlert(
175+
title = title,
176+
titleRes = titleRes,
177+
message = message,
178+
messageRes = messageRes,
179+
composableMessage = composableMessage,
180+
html = html,
181+
onConfirm = onConfirm,
182+
onDismiss = onDismiss,
183+
confirmText = confirmText,
184+
confirmTextRes = confirmTextRes,
185+
dismissText = dismissText,
186+
dismissTextRes = dismissTextRes,
187+
choices = choices,
188+
)
185189
}
186190

187-
private fun dismissAlert() {
188-
_currentAlert.value = null
191+
fun dismissAlert() {
192+
alertManager.dismissAlert()
189193
}
190194

191195
val meshService: IMeshService?
@@ -203,10 +207,25 @@ constructor(
203207
.filterNotNull()
204208
.onEach {
205209
showAlert(
206-
title = getString(Res.string.client_notification),
210+
titleRes = Res.string.client_notification,
207211
message = it,
208212
onConfirm = { serviceRepository.clearErrorMessage() },
209-
dismissable = false,
213+
)
214+
}
215+
.launchIn(viewModelScope)
216+
217+
serviceRepository.clientNotification
218+
.filterNotNull()
219+
.onEach { notification ->
220+
val isCompromised = notification.low_entropy_key != null || notification.duplicated_public_key != null
221+
showAlert(
222+
titleRes = Res.string.client_notification,
223+
message = if (isCompromised) getString(Res.string.compromised_keys) else notification.message,
224+
onConfirm = {
225+
// Action for compromised keys should be handled via a callback or event
226+
clearClientNotification(notification)
227+
},
228+
onDismiss = { clearClientNotification(notification) },
210229
)
211230
}
212231
.launchIn(viewModelScope)
@@ -218,12 +237,8 @@ constructor(
218237
val sharedContactRequested: StateFlow<SharedContact?>
219238
get() = _sharedContactRequested.asStateFlow()
220239

221-
fun setSharedContactRequested(url: Uri, onFailure: () -> Unit) {
222-
runCatching { _sharedContactRequested.value = url.toSharedContact() }
223-
.onFailure { ex ->
224-
Logger.e(ex) { "Shared contact error" }
225-
onFailure()
226-
}
240+
fun setSharedContactRequested(contact: SharedContact?) {
241+
_sharedContactRequested.value = contact
227242
}
228243

229244
/** Called immediately after activity observes requestChannelUrl */
@@ -239,20 +254,17 @@ constructor(
239254
val requestChannelSet: StateFlow<ChannelSet?>
240255
get() = _requestChannelSet
241256

242-
fun requestChannelUrl(url: Uri, onFailure: () -> Unit) =
243-
runCatching { _requestChannelSet.value = url.toChannelSet() }
244-
.onFailure { ex ->
245-
Logger.e(ex) { "Channel url error" }
246-
onFailure()
247-
}
257+
fun setRequestChannelSet(channelSet: ChannelSet?) {
258+
_requestChannelSet.value = channelSet
259+
}
248260

249261
/** Unified handler for scanned Meshtastic URIs (contacts or channels). */
250262
fun handleScannedUri(uri: Uri, onInvalid: () -> Unit) {
251-
if (uri.path?.contains("/v/") == true) {
252-
setSharedContactRequested(uri, onInvalid)
253-
} else {
254-
requestChannelUrl(uri, onInvalid)
255-
}
263+
uri.dispatchMeshtasticUri(
264+
onContact = { setSharedContactRequested(it) },
265+
onChannel = { setRequestChannelSet(it) },
266+
onInvalid = onInvalid,
267+
)
256268
}
257269

258270
val latestStableFirmwareRelease = firmwareReleaseRepository.stableRelease.mapNotNull { it?.asDeviceVersion() }
@@ -267,8 +279,8 @@ constructor(
267279
Logger.d { "ViewModel cleared" }
268280
}
269281

270-
val tracerouteResponse: LiveData<TracerouteResponse?>
271-
get() = serviceRepository.tracerouteResponse.asLiveData()
282+
val tracerouteResponse: Flow<TracerouteResponse?>
283+
get() = serviceRepository.tracerouteResponse
272284

273285
fun clearTracerouteResponse() {
274286
serviceRepository.clearTracerouteResponse()

0 commit comments

Comments
 (0)