@@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.map
4343import kotlinx.coroutines.flow.mapNotNull
4444import kotlinx.coroutines.flow.onEach
4545import kotlinx.coroutines.flow.shareIn
46+ import org.jetbrains.compose.resources.StringResource
4647import org.jetbrains.compose.resources.getString
4748import org.meshtastic.core.analytics.platform.PlatformAnalytics
4849import org.meshtastic.core.data.repository.FirmwareReleaseRepository
@@ -54,15 +55,17 @@ import org.meshtastic.core.database.entity.asDeviceVersion
5455import org.meshtastic.core.datastore.UiPreferencesDataSource
5556import org.meshtastic.core.model.TracerouteMapAvailability
5657import org.meshtastic.core.model.evaluateTracerouteMapAvailability
57- import org.meshtastic.core.model.util.toChannelSet
58+ import org.meshtastic.core.model.util.dispatchMeshtasticUri
5859import org.meshtastic.core.service.IMeshService
5960import org.meshtastic.core.service.MeshServiceNotifications
6061import org.meshtastic.core.service.ServiceRepository
6162import org.meshtastic.core.service.TracerouteResponse
6263import org.meshtastic.core.strings.Res
6364import org.meshtastic.core.strings.client_notification
65+ import org.meshtastic.core.strings.compromised_keys
6466import 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
6669import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
6770import org.meshtastic.proto.ChannelSet
6871import 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