feat: Implement iOS support and unify Compose Multiplatform infrastructure (#4876)

This commit is contained in:
James Rich 2026-03-21 18:19:13 -05:00 committed by GitHub
parent f04924ded5
commit d136b162a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
170 changed files with 2208 additions and 2432 deletions

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.messaging.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.flow.Flow
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.core.ui.viewmodel.UIViewModel
import org.meshtastic.feature.messaging.MessageViewModel
import org.meshtastic.feature.messaging.ui.contact.AdaptiveContactsScreen
import org.meshtastic.feature.messaging.ui.contact.ContactsViewModel
@Composable
actual fun ContactsEntryContent(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent>,
initialContactKey: String?,
initialMessage: String,
) {
val uiViewModel: UIViewModel = koinViewModel()
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
val contactsViewModel = koinViewModel<ContactsViewModel>()
val messageViewModel = koinViewModel<MessageViewModel>()
initialContactKey?.let { messageViewModel.setContactKey(it) }
AdaptiveContactsScreen(
backStack = backStack,
contactsViewModel = contactsViewModel,
messageViewModel = messageViewModel,
scrollToTopEvents = scrollToTopEvents,
sharedContactRequested = sharedContactRequested,
requestChannelSet = requestChannelSet,
onHandleScannedUri = uiViewModel::handleScannedUri,
onClearSharedContactRequested = uiViewModel::clearSharedContactRequested,
onClearRequestChannelUrl = uiViewModel::clearRequestChannelUrl,
initialContactKey = initialContactKey,
initialMessage = initialMessage,
)
}

View file

@ -19,6 +19,7 @@ package org.meshtastic.feature.messaging.component
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.tooling.preview.NodePreviewParameterProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
@ -26,7 +27,6 @@ import org.junit.runner.RunWith
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.model.Message
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
@RunWith(AndroidJUnit4::class)
class MessageItemTest {

View file

@ -18,7 +18,6 @@
package org.meshtastic.feature.messaging
import android.content.ClipData
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
@ -78,6 +77,7 @@ import org.meshtastic.core.resources.unknown_channel
import org.meshtastic.core.ui.component.SharedContactDialog
import org.meshtastic.core.ui.component.smartScrollToIndex
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.util.createClipEntry
import org.meshtastic.feature.messaging.component.ActionModeTopBar
import org.meshtastic.feature.messaging.component.DeleteMessageDialog
import org.meshtastic.feature.messaging.component.MESSAGE_CHARACTER_LIMIT_BYTES
@ -86,7 +86,6 @@ import org.meshtastic.feature.messaging.component.MessageTopBar
import org.meshtastic.feature.messaging.component.QuickChatRow
import org.meshtastic.feature.messaging.component.ReplySnippet
import org.meshtastic.feature.messaging.component.ScrollToBottomFab
import java.nio.charset.StandardCharsets
private const val ROUNDED_CORNER_PERCENT = 100
private const val MAX_LINES = 3
@ -243,11 +242,7 @@ fun MessageScreen(
is MessageScreenEvent.NavigateToNodeDetails -> navigateToNodeDetails(event.nodeNum)
MessageScreenEvent.NavigateBack -> onNavigateBack()
is MessageScreenEvent.CopyToClipboard -> {
coroutineScope.launch {
clipboardManager.setClipEntry(
androidx.compose.ui.platform.ClipEntry(ClipData.newPlainText(event.text, event.text)),
)
}
coroutineScope.launch { clipboardManager.setClipEntry(createClipEntry(event.text, event.text)) }
selectedMessageIds.value = emptySet()
}
}
@ -450,7 +445,7 @@ private fun MessageInput(
val currentByteLength =
remember(currentText) {
// Recalculate only when text changes
currentText.toByteArray(StandardCharsets.UTF_8).size
currentText.encodeToByteArray().size
}
val isOverLimit = currentByteLength > maxByteSize

View file

@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -34,6 +33,7 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.model.ContactSettings
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Message
@ -190,7 +190,7 @@ class MessageViewModel(
}
fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) {
viewModelScope.launch(Dispatchers.IO) { packetRepository.setContactFilteringDisabled(contactKey, disabled) }
viewModelScope.launch(ioDispatcher) { packetRepository.setContactFilteringDisabled(contactKey, disabled) }
}
fun getNode(userId: String?) = nodeRepository.getNode(userId ?: DataPacket.ID_BROADCAST)
@ -218,10 +218,10 @@ class MessageViewModel(
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.Reaction(emoji, replyId, contactKey)) }
fun deleteMessages(uuidList: List<Long>) =
viewModelScope.launch(Dispatchers.IO) { packetRepository.deleteMessages(uuidList) }
viewModelScope.launch(ioDispatcher) { packetRepository.deleteMessages(uuidList) }
fun clearUnreadCount(contact: String, messageUuid: Long, lastReadTimestamp: Long) =
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(ioDispatcher) {
val existingTimestamp = contactSettings.value[contact]?.lastReadMessageTimestamp ?: Long.MIN_VALUE
if (lastReadTimestamp <= existingTimestamp) {
return@launch

View file

@ -202,7 +202,7 @@ internal fun EditQuickChatDialog(
label = stringResource(Res.string.message),
value = actionInput.message,
maxSize = 200,
getSize = { it.toByteArray().size + 1 },
getSize = { it.encodeToByteArray().size + 1 },
modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
) {
actionInput = actionInput.copy(message = it)

View file

@ -18,9 +18,9 @@ package org.meshtastic.feature.messaging
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.repository.QuickChatActionRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
@ -31,7 +31,7 @@ class QuickChatViewModel(private val quickChatActionRepository: QuickChatActionR
get() = quickChatActionRepository.getAllActions().stateInWhileSubscribed(initialValue = emptyList())
fun updateActionPositions(actions: List<QuickChatAction>) {
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(ioDispatcher) {
for (position in actions.indices) {
quickChatActionRepository.setItemPosition(actions[position].uuid, position)
}
@ -39,8 +39,8 @@ class QuickChatViewModel(private val quickChatActionRepository: QuickChatActionR
}
fun addQuickChatAction(action: QuickChatAction) =
viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.upsert(action) }
viewModelScope.launch(ioDispatcher) { quickChatActionRepository.upsert(action) }
fun deleteQuickChatAction(action: QuickChatAction) =
viewModelScope.launch(Dispatchers.IO) { quickChatActionRepository.delete(action) }
viewModelScope.launch(ioDispatcher) { quickChatActionRepository.delete(action) }
}

View file

@ -17,27 +17,23 @@
package org.meshtastic.feature.messaging.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.core.ui.viewmodel.UIViewModel
import org.meshtastic.feature.messaging.MessageViewModel
import org.meshtastic.feature.messaging.QuickChatScreen
import org.meshtastic.feature.messaging.QuickChatViewModel
import org.meshtastic.feature.messaging.ui.contact.AdaptiveContactsScreen
import org.meshtastic.feature.messaging.ui.contact.ContactsViewModel
import org.meshtastic.feature.messaging.ui.sharing.ShareScreen
@Suppress("LongMethod")
fun EntryProviderScope<NavKey>.contactsGraph(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent>,
scrollToTopEvents: Flow<ScrollToTopEvent> = MutableSharedFlow(),
) {
entry<ContactsRoutes.ContactsGraph> {
ContactsEntryContent(backStack = backStack, scrollToTopEvents = scrollToTopEvents)
@ -77,30 +73,9 @@ fun EntryProviderScope<NavKey>.contactsGraph(
}
@Composable
private fun ContactsEntryContent(
expect fun ContactsEntryContent(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent>,
initialContactKey: String? = null,
initialMessage: String = "",
) {
val uiViewModel: UIViewModel = koinViewModel()
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
val contactsViewModel = koinViewModel<ContactsViewModel>()
val messageViewModel = koinViewModel<MessageViewModel>()
initialContactKey?.let { messageViewModel.setContactKey(it) }
AdaptiveContactsScreen(
backStack = backStack,
contactsViewModel = contactsViewModel,
messageViewModel = messageViewModel,
scrollToTopEvents = scrollToTopEvents,
sharedContactRequested = sharedContactRequested,
requestChannelSet = requestChannelSet,
onHandleScannedUri = uiViewModel::handleScannedUri,
onClearSharedContactRequested = uiViewModel::clearSharedContactRequested,
onClearRequestChannelUrl = uiViewModel::clearRequestChannelUrl,
initialContactKey = initialContactKey,
initialMessage = initialMessage,
)
}
)

View file

@ -16,7 +16,6 @@
*/
package org.meshtastic.feature.messaging.ui.contact
import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
@ -29,7 +28,10 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.CancellationException
import androidx.navigationevent.NavigationEventInfo
import androidx.navigationevent.NavigationEventTransitionState
import androidx.navigationevent.compose.NavigationBackHandler
import androidx.navigationevent.compose.rememberNavigationEventState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
@ -44,6 +46,7 @@ import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.core.ui.icon.Conversations
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.feature.messaging.MessageScreen
import org.meshtastic.feature.messaging.MessageViewModel
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.SharedContact
@ -52,8 +55,8 @@ import org.meshtastic.proto.SharedContact
@Composable
fun AdaptiveContactsScreen(
backStack: NavBackStack<NavKey>,
contactsViewModel: org.meshtastic.feature.messaging.ui.contact.ContactsViewModel,
messageViewModel: org.meshtastic.feature.messaging.MessageViewModel,
contactsViewModel: ContactsViewModel,
messageViewModel: MessageViewModel,
scrollToTopEvents: Flow<ScrollToTopEvent>,
sharedContactRequested: SharedContact?,
requestChannelSet: ChannelSet?,
@ -62,6 +65,7 @@ fun AdaptiveContactsScreen(
onClearRequestChannelUrl: () -> Unit,
initialContactKey: String? = null,
initialMessage: String = "",
detailPaneCustom: @Composable ((contactKey: String) -> Unit)? = null,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<String>()
val scope = rememberCoroutineScope()
@ -95,14 +99,18 @@ fun AdaptiveContactsScreen(
}
}
PredictiveBackHandler(
enabled = navigator.currentDestination?.pane == ListDetailPaneScaffoldRole.Detail,
) { progress ->
try {
progress.collect { /* Predictive back progress could be used here to drive UI if scaffold supported it */ }
handleBack()
} catch (_: CancellationException) {
// Gesture cancelled
val navState = rememberNavigationEventState(NavigationEventInfo.None)
NavigationBackHandler(
state = navState,
isBackEnabled = navigator.currentDestination?.pane == ListDetailPaneScaffoldRole.Detail,
onBackCancelled = { /* Gesture cancelled */ },
onBackCompleted = { handleBack() },
)
LaunchedEffect(navState.transitionState) {
val transitionState = navState.transitionState
if (transitionState is NavigationEventTransitionState.InProgress) {
val progress = transitionState.latestEvent.progress
// Animate the back gesture progress could be used here to drive UI if scaffold supported it
}
}
@ -154,14 +162,18 @@ fun AdaptiveContactsScreen(
AnimatedPane {
navigator.currentDestination?.contentKey?.let { contactKey ->
key(contactKey) {
MessageScreen(
contactKey = contactKey,
message = if (contactKey == initialContactKey) initialMessage else "",
viewModel = messageViewModel,
navigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
navigateToQuickChatOptions = { backStack.add(ContactsRoutes.QuickChat) },
onNavigateBack = handleBack,
)
if (detailPaneCustom != null) {
detailPaneCustom(contactKey)
} else {
MessageScreen(
contactKey = contactKey,
message = if (contactKey == initialContactKey) initialMessage else "",
viewModel = messageViewModel,
navigateToNodeDetails = { backStack.add(NodesRoutes.NodeDetailGraph(it)) },
navigateToQuickChatOptions = { backStack.add(ContactsRoutes.QuickChat) },
onNavigateBack = handleBack,
)
}
}
}
?: EmptyDetailPlaceholder(

View file

@ -49,16 +49,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@ -66,7 +63,6 @@ import org.jetbrains.compose.resources.pluralStringResource
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.MeshtasticUri
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.toMeshtasticUri
import org.meshtastic.core.model.Contact
import org.meshtastic.core.model.ContactSettings
import org.meshtastic.core.model.util.TimeConstants
@ -108,12 +104,11 @@ import org.meshtastic.core.ui.icon.SelectAll
import org.meshtastic.core.ui.icon.VolumeMuteTwoTone
import org.meshtastic.core.ui.icon.VolumeUpTwoTone
import org.meshtastic.core.ui.qr.ScannedQrCodeDialog
import org.meshtastic.core.ui.util.showToast
import org.meshtastic.core.ui.util.rememberShowToastResource
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.SharedContact
import kotlin.time.Duration.Companion.days
@OptIn(ExperimentalPermissionsApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod", "LongParameterList")
@Composable
fun ContactsScreen(
@ -124,13 +119,13 @@ fun ContactsScreen(
onClearSharedContactRequested: () -> Unit,
onClearRequestChannelUrl: () -> Unit,
viewModel: ContactsViewModel,
onClickNodeChip: (Int) -> Unit = {},
onNavigateToMessages: (String) -> Unit = {},
onNavigateToNodeDetails: (Int) -> Unit = {},
scrollToTopEvents: Flow<ScrollToTopEvent>? = null,
activeContactKey: String? = null,
onClickNodeChip: (Int) -> Unit,
onNavigateToMessages: (String) -> Unit,
onNavigateToNodeDetails: (Int) -> Unit,
scrollToTopEvents: Flow<ScrollToTopEvent>?,
activeContactKey: String?,
) {
val context = LocalContext.current
val showToast = rememberShowToastResource()
val scope = rememberCoroutineScope()
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle()
@ -258,8 +253,8 @@ fun ContactsScreen(
MeshtasticImportFAB(
sharedContact = sharedContactRequested,
onImport = { uriString ->
onHandleScannedUri(uriString.toUri().toMeshtasticUri()) {
scope.launch { context.showToast(Res.string.channel_invalid) }
onHandleScannedUri(MeshtasticUri(uriString)) {
scope.launch { showToast(Res.string.channel_invalid) }
}
},
onShareChannels = onNavigateToShare,

View file

@ -21,13 +21,13 @@ import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.map
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.model.Contact
import org.meshtastic.core.model.ContactSettings
import org.meshtastic.core.model.DataPacket
@ -189,17 +189,17 @@ class ContactsViewModel(
fun getNode(userId: String?) = nodeRepository.getNode(userId ?: DataPacket.ID_BROADCAST)
fun deleteContacts(contacts: List<String>) =
viewModelScope.launch(Dispatchers.IO) { packetRepository.deleteContacts(contacts) }
viewModelScope.launch(ioDispatcher) { packetRepository.deleteContacts(contacts) }
fun markAllAsRead() = viewModelScope.launch(Dispatchers.IO) { packetRepository.clearAllUnreadCounts() }
fun markAllAsRead() = viewModelScope.launch(ioDispatcher) { packetRepository.clearAllUnreadCounts() }
fun setMuteUntil(contacts: List<String>, until: Long) =
viewModelScope.launch(Dispatchers.IO) { packetRepository.setMuteUntil(contacts, until) }
viewModelScope.launch(ioDispatcher) { packetRepository.setMuteUntil(contacts, until) }
fun getContactSettings() = packetRepository.getContactSettings()
fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) {
viewModelScope.launch(Dispatchers.IO) { packetRepository.setContactFilteringDisabled(contactKey, disabled) }
viewModelScope.launch(ioDispatcher) { packetRepository.setContactFilteringDisabled(contactKey, disabled) }
}
/**

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.messaging.navigation
import androidx.compose.runtime.Composable
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.flow.Flow
import org.meshtastic.core.ui.component.ScrollToTopEvent
@Composable
actual fun ContactsEntryContent(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent>,
initialContactKey: String?,
initialMessage: String,
) {
// TODO: Implement iOS contacts screen
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.messaging.navigation
import androidx.compose.runtime.Composable
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import kotlinx.coroutines.flow.Flow
import org.koin.compose.viewmodel.koinViewModel
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.feature.messaging.MessageScreen
import org.meshtastic.feature.messaging.MessageViewModel
import org.meshtastic.feature.messaging.ui.contact.AdaptiveContactsScreen
import org.meshtastic.feature.messaging.ui.contact.ContactsViewModel
@Composable
actual fun ContactsEntryContent(
backStack: NavBackStack<NavKey>,
scrollToTopEvents: Flow<ScrollToTopEvent>,
initialContactKey: String?,
initialMessage: String,
) {
val viewModel: ContactsViewModel = koinViewModel()
AdaptiveContactsScreen(
backStack = backStack,
contactsViewModel = viewModel,
messageViewModel = koinViewModel(), // Used for desktop detail pane
scrollToTopEvents = scrollToTopEvents,
sharedContactRequested = null,
requestChannelSet = null,
onHandleScannedUri = { _, _ -> },
onClearSharedContactRequested = {},
onClearRequestChannelUrl = {},
initialContactKey = initialContactKey,
initialMessage = initialMessage,
detailPaneCustom = { contactKey ->
val messageViewModel: MessageViewModel = koinViewModel(key = "messages-$contactKey")
MessageScreen(
contactKey = contactKey,
message = if (contactKey == initialContactKey) initialMessage else "",
viewModel = messageViewModel,
navigateToNodeDetails = {
backStack.add(org.meshtastic.core.navigation.NodesRoutes.NodeDetailGraph(it))
},
navigateToQuickChatOptions = { backStack.add(org.meshtastic.core.navigation.ContactsRoutes.QuickChat) },
onNavigateBack = { backStack.removeLastOrNull() },
)
},
)
}