mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Implement iOS support and unify Compose Multiplatform infrastructure (#4876)
This commit is contained in:
parent
f04924ded5
commit
d136b162a4
170 changed files with 2208 additions and 2432 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
@ -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,
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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() },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue