kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
Decouple contacts nav graph from `UiViewModel` (#3215)
rodzic
3e83e61a1a
commit
6d5e56b34f
|
|
@ -54,7 +54,6 @@ import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -67,19 +66,14 @@ import org.meshtastic.core.data.repository.DeviceHardwareRepository
|
||||||
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
|
||||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||||
import org.meshtastic.core.data.repository.NodeRepository
|
import org.meshtastic.core.data.repository.NodeRepository
|
||||||
import org.meshtastic.core.data.repository.PacketRepository
|
|
||||||
import org.meshtastic.core.data.repository.QuickChatActionRepository
|
import org.meshtastic.core.data.repository.QuickChatActionRepository
|
||||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||||
import org.meshtastic.core.database.entity.Packet
|
|
||||||
import org.meshtastic.core.database.entity.QuickChatAction
|
import org.meshtastic.core.database.entity.QuickChatAction
|
||||||
import org.meshtastic.core.database.entity.asDeviceVersion
|
import org.meshtastic.core.database.entity.asDeviceVersion
|
||||||
import org.meshtastic.core.database.model.Node
|
import org.meshtastic.core.database.model.Node
|
||||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||||
import org.meshtastic.core.model.DataPacket
|
|
||||||
import org.meshtastic.core.model.DeviceHardware
|
import org.meshtastic.core.model.DeviceHardware
|
||||||
import org.meshtastic.core.model.util.getChannel
|
|
||||||
import org.meshtastic.core.model.util.getShortDate
|
|
||||||
import org.meshtastic.core.model.util.toChannelSet
|
import org.meshtastic.core.model.util.toChannelSet
|
||||||
import org.meshtastic.core.strings.R
|
import org.meshtastic.core.strings.R
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -165,9 +159,8 @@ constructor(
|
||||||
private val radioConfigRepository: RadioConfigRepository,
|
private val radioConfigRepository: RadioConfigRepository,
|
||||||
private val serviceRepository: ServiceRepository,
|
private val serviceRepository: ServiceRepository,
|
||||||
radioInterfaceService: RadioInterfaceService,
|
radioInterfaceService: RadioInterfaceService,
|
||||||
private val meshLogRepository: MeshLogRepository,
|
meshLogRepository: MeshLogRepository,
|
||||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||||
private val packetRepository: PacketRepository,
|
|
||||||
private val quickChatActionRepository: QuickChatActionRepository,
|
private val quickChatActionRepository: QuickChatActionRepository,
|
||||||
firmwareReleaseRepository: FirmwareReleaseRepository,
|
firmwareReleaseRepository: FirmwareReleaseRepository,
|
||||||
private val uiPreferencesDataSource: UiPreferencesDataSource,
|
private val uiPreferencesDataSource: UiPreferencesDataSource,
|
||||||
|
|
@ -279,10 +272,6 @@ constructor(
|
||||||
val ourNodeInfo: StateFlow<Node?>
|
val ourNodeInfo: StateFlow<Node?>
|
||||||
get() = nodeDB.ourNodeInfo
|
get() = nodeDB.ourNodeInfo
|
||||||
|
|
||||||
fun getNode(userId: String?) = nodeDB.getNode(userId ?: DataPacket.ID_BROADCAST)
|
|
||||||
|
|
||||||
fun getUser(userId: String?) = nodeDB.getUser(userId ?: DataPacket.ID_BROADCAST)
|
|
||||||
|
|
||||||
val snackBarHostState = SnackbarHostState()
|
val snackBarHostState = SnackbarHostState()
|
||||||
|
|
||||||
fun showSnackBar(text: Int) = showSnackBar(app.getString(text))
|
fun showSnackBar(text: Int) = showSnackBar(app.getString(text))
|
||||||
|
|
@ -327,67 +316,6 @@ constructor(
|
||||||
debug("ViewModel created")
|
debug("ViewModel created")
|
||||||
}
|
}
|
||||||
|
|
||||||
val contactList =
|
|
||||||
combine(nodeDB.myNodeInfo, packetRepository.getContacts(), channels, packetRepository.getContactSettings()) {
|
|
||||||
myNodeInfo,
|
|
||||||
contacts,
|
|
||||||
channelSet,
|
|
||||||
settings,
|
|
||||||
->
|
|
||||||
val myNodeNum = myNodeInfo?.myNodeNum ?: return@combine emptyList()
|
|
||||||
// Add empty channel placeholders (always show Broadcast contacts, even when empty)
|
|
||||||
val placeholder =
|
|
||||||
(0 until channelSet.settingsCount).associate { ch ->
|
|
||||||
val contactKey = "$ch${DataPacket.ID_BROADCAST}"
|
|
||||||
val data = DataPacket(bytes = null, dataType = 1, time = 0L, channel = ch)
|
|
||||||
contactKey to Packet(0L, myNodeNum, 1, contactKey, 0L, true, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
(contacts + (placeholder - contacts.keys)).values.map { packet ->
|
|
||||||
val data = packet.data
|
|
||||||
val contactKey = packet.contact_key
|
|
||||||
|
|
||||||
// Determine if this is my message (originated on this device)
|
|
||||||
val fromLocal = data.from == DataPacket.ID_LOCAL
|
|
||||||
val toBroadcast = data.to == DataPacket.ID_BROADCAST
|
|
||||||
|
|
||||||
// grab usernames from NodeInfo
|
|
||||||
val user = getUser(if (fromLocal) data.to else data.from)
|
|
||||||
val node = getNode(if (fromLocal) data.to else data.from)
|
|
||||||
|
|
||||||
val shortName = user.shortName
|
|
||||||
val longName =
|
|
||||||
if (toBroadcast) {
|
|
||||||
channelSet.getChannel(data.channel)?.name ?: app.getString(R.string.channel_name)
|
|
||||||
} else {
|
|
||||||
user.longName
|
|
||||||
}
|
|
||||||
|
|
||||||
Contact(
|
|
||||||
contactKey = contactKey,
|
|
||||||
shortName = if (toBroadcast) "${data.channel}" else shortName,
|
|
||||||
longName = longName,
|
|
||||||
lastMessageTime = getShortDate(data.time),
|
|
||||||
lastMessageText = if (fromLocal) data.text else "$shortName: ${data.text}",
|
|
||||||
unreadCount = packetRepository.getUnreadCount(contactKey),
|
|
||||||
messageCount = packetRepository.getMessageCount(contactKey),
|
|
||||||
isMuted = settings[contactKey]?.isMuted == true,
|
|
||||||
isUnmessageable = user.isUnmessagable,
|
|
||||||
nodeColors =
|
|
||||||
if (!toBroadcast) {
|
|
||||||
node.colors
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.stateIn(
|
|
||||||
scope = viewModelScope,
|
|
||||||
started = SharingStarted.WhileSubscribed(5_000),
|
|
||||||
initialValue = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val _sharedContactRequested: MutableStateFlow<AdminProtos.SharedContact?> = MutableStateFlow(null)
|
private val _sharedContactRequested: MutableStateFlow<AdminProtos.SharedContact?> = MutableStateFlow(null)
|
||||||
val sharedContactRequested: StateFlow<AdminProtos.SharedContact?>
|
val sharedContactRequested: StateFlow<AdminProtos.SharedContact?>
|
||||||
get() = _sharedContactRequested.asStateFlow()
|
get() = _sharedContactRequested.asStateFlow()
|
||||||
|
|
@ -396,12 +324,6 @@ constructor(
|
||||||
_sharedContactRequested.value = sharedContact
|
_sharedContactRequested.value = sharedContact
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setMuteUntil(contacts: List<String>, until: Long) =
|
|
||||||
viewModelScope.launch(Dispatchers.IO) { packetRepository.setMuteUntil(contacts, until) }
|
|
||||||
|
|
||||||
fun deleteContacts(contacts: List<String>) =
|
|
||||||
viewModelScope.launch(Dispatchers.IO) { packetRepository.deleteContacts(contacts) }
|
|
||||||
|
|
||||||
// Connection state to our radio device
|
// Connection state to our radio device
|
||||||
val connectionState
|
val connectionState
|
||||||
get() = serviceRepository.connectionState
|
get() = serviceRepository.connectionState
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.navDeepLink
|
import androidx.navigation.navDeepLink
|
||||||
import androidx.navigation.navigation
|
import androidx.navigation.navigation
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
|
||||||
import com.geeksville.mesh.ui.contact.ContactsScreen
|
import com.geeksville.mesh.ui.contact.ContactsScreen
|
||||||
import com.geeksville.mesh.ui.message.MessageScreen
|
import com.geeksville.mesh.ui.message.MessageScreen
|
||||||
import com.geeksville.mesh.ui.message.QuickChatScreen
|
import com.geeksville.mesh.ui.message.QuickChatScreen
|
||||||
|
|
@ -34,13 +33,12 @@ import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||||
import org.meshtastic.core.navigation.NodesRoutes
|
import org.meshtastic.core.navigation.NodesRoutes
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
fun NavGraphBuilder.contactsGraph(navController: NavHostController, uiViewModel: UIViewModel) {
|
fun NavGraphBuilder.contactsGraph(navController: NavHostController) {
|
||||||
navigation<ContactsRoutes.ContactsGraph>(startDestination = ContactsRoutes.Contacts) {
|
navigation<ContactsRoutes.ContactsGraph>(startDestination = ContactsRoutes.Contacts) {
|
||||||
composable<ContactsRoutes.Contacts>(
|
composable<ContactsRoutes.Contacts>(
|
||||||
deepLinks = listOf(navDeepLink<ContactsRoutes.Contacts>(basePath = "$DEEP_LINK_BASE_URI/contacts")),
|
deepLinks = listOf(navDeepLink<ContactsRoutes.Contacts>(basePath = "$DEEP_LINK_BASE_URI/contacts")),
|
||||||
) {
|
) {
|
||||||
ContactsScreen(
|
ContactsScreen(
|
||||||
uiViewModel,
|
|
||||||
onClickNodeChip = {
|
onClickNodeChip = {
|
||||||
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
|
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
|
|
@ -81,7 +79,7 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, uiViewModel:
|
||||||
),
|
),
|
||||||
) { backStackEntry ->
|
) { backStackEntry ->
|
||||||
val message = backStackEntry.toRoute<ContactsRoutes.Share>().message
|
val message = backStackEntry.toRoute<ContactsRoutes.Share>().message
|
||||||
ShareScreen(uiViewModel) {
|
ShareScreen {
|
||||||
navController.navigate(ContactsRoutes.Messages(it, message)) {
|
navController.navigate(ContactsRoutes.Messages(it, message)) {
|
||||||
popUpTo<ContactsRoutes.Share> { inclusive = true }
|
popUpTo<ContactsRoutes.Share> { inclusive = true }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -410,7 +410,7 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
||||||
startDestination = NodesRoutes.NodesGraph,
|
startDestination = NodesRoutes.NodesGraph,
|
||||||
modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding().imePadding(),
|
modifier = Modifier.fillMaxSize().recalculateWindowInsets().safeDrawingPadding().imePadding(),
|
||||||
) {
|
) {
|
||||||
contactsGraph(navController, uiViewModel = uIViewModel)
|
contactsGraph(navController)
|
||||||
nodesGraph(navController, uiViewModel = uIViewModel)
|
nodesGraph(navController, uiViewModel = uIViewModel)
|
||||||
mapGraph(navController)
|
mapGraph(navController)
|
||||||
channelsGraph(navController, uiViewModel = uIViewModel)
|
channelsGraph(navController, uiViewModel = uIViewModel)
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.geeksville.mesh.AppOnlyProtos
|
import com.geeksville.mesh.AppOnlyProtos
|
||||||
import com.geeksville.mesh.model.Contact
|
import com.geeksville.mesh.model.Contact
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
|
||||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||||
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
import com.geeksville.mesh.ui.node.components.NodeMenuAction
|
||||||
import org.meshtastic.core.strings.R
|
import org.meshtastic.core.strings.R
|
||||||
|
|
@ -73,14 +72,14 @@ import java.util.concurrent.TimeUnit
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
fun ContactsScreen(
|
fun ContactsScreen(
|
||||||
uiViewModel: UIViewModel = hiltViewModel(),
|
viewModel: ContactsViewModel = hiltViewModel(),
|
||||||
onClickNodeChip: (Int) -> Unit = {},
|
onClickNodeChip: (Int) -> Unit = {},
|
||||||
onNavigateToMessages: (String) -> Unit = {},
|
onNavigateToMessages: (String) -> Unit = {},
|
||||||
onNavigateToNodeDetails: (Int) -> Unit = {},
|
onNavigateToNodeDetails: (Int) -> Unit = {},
|
||||||
onNavigateToShare: () -> Unit,
|
onNavigateToShare: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle()
|
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||||
val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
val ourNode by viewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||||
var showMuteDialog by remember { mutableStateOf(false) }
|
var showMuteDialog by remember { mutableStateOf(false) }
|
||||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
@ -89,7 +88,7 @@ fun ContactsScreen(
|
||||||
val isSelectionModeActive by remember { derivedStateOf { selectedContactKeys.isNotEmpty() } }
|
val isSelectionModeActive by remember { derivedStateOf { selectedContactKeys.isNotEmpty() } }
|
||||||
|
|
||||||
// State for contacts list
|
// State for contacts list
|
||||||
val contacts by uiViewModel.contactList.collectAsStateWithLifecycle()
|
val contacts by viewModel.contactList.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Derived state for selected contacts and count
|
// Derived state for selected contacts and count
|
||||||
val selectedContacts =
|
val selectedContacts =
|
||||||
|
|
@ -116,7 +115,7 @@ fun ContactsScreen(
|
||||||
if (contact.contactKey.contains("!")) {
|
if (contact.contactKey.contains("!")) {
|
||||||
// if it's a node, look up the nodeNum including the !
|
// if it's a node, look up the nodeNum including the !
|
||||||
val nodeKey = contact.contactKey.substring(1)
|
val nodeKey = contact.contactKey.substring(1)
|
||||||
val node = uiViewModel.getNode(nodeKey)
|
val node = viewModel.getNode(nodeKey)
|
||||||
|
|
||||||
if (node != null) {
|
if (node != null) {
|
||||||
// navigate to node details.
|
// navigate to node details.
|
||||||
|
|
@ -145,8 +144,8 @@ fun ContactsScreen(
|
||||||
MainAppBar(
|
MainAppBar(
|
||||||
title = stringResource(R.string.conversations),
|
title = stringResource(R.string.conversations),
|
||||||
ourNode = ourNode,
|
ourNode = ourNode,
|
||||||
isConnected = isConnected,
|
isConnected = connectionState.isConnected(),
|
||||||
showNodeChip = ourNode != null && isConnected,
|
showNodeChip = ourNode != null && connectionState.isConnected(),
|
||||||
canNavigateUp = false,
|
canNavigateUp = false,
|
||||||
onNavigateUp = {},
|
onNavigateUp = {},
|
||||||
actions = {},
|
actions = {},
|
||||||
|
|
@ -160,7 +159,11 @@ fun ContactsScreen(
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
modifier = Modifier.animateFloatingActionButton(visible = isConnected, alignment = Alignment.BottomEnd),
|
modifier =
|
||||||
|
Modifier.animateFloatingActionButton(
|
||||||
|
visible = connectionState.isConnected(),
|
||||||
|
alignment = Alignment.BottomEnd,
|
||||||
|
),
|
||||||
onClick = onNavigateToShare,
|
onClick = onNavigateToShare,
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.QrCode2, contentDescription = null)
|
Icon(Icons.Rounded.QrCode2, contentDescription = null)
|
||||||
|
|
@ -183,7 +186,7 @@ fun ContactsScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val channels by uiViewModel.channels.collectAsStateWithLifecycle()
|
val channels by viewModel.channels.collectAsStateWithLifecycle()
|
||||||
ContactListView(
|
ContactListView(
|
||||||
contacts = contacts,
|
contacts = contacts,
|
||||||
selectedList = selectedContactKeys,
|
selectedList = selectedContactKeys,
|
||||||
|
|
@ -200,7 +203,7 @@ fun ContactsScreen(
|
||||||
onDismiss = { showDeleteDialog = false },
|
onDismiss = { showDeleteDialog = false },
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
showDeleteDialog = false
|
showDeleteDialog = false
|
||||||
uiViewModel.deleteContacts(selectedContactKeys.toList())
|
viewModel.deleteContacts(selectedContactKeys.toList())
|
||||||
selectedContactKeys.clear()
|
selectedContactKeys.clear()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -210,7 +213,7 @@ fun ContactsScreen(
|
||||||
onDismiss = { showMuteDialog = false },
|
onDismiss = { showMuteDialog = false },
|
||||||
onConfirm = { muteUntil ->
|
onConfirm = { muteUntil ->
|
||||||
showMuteDialog = false
|
showMuteDialog = false
|
||||||
uiViewModel.setMuteUntil(selectedContactKeys.toList(), muteUntil)
|
viewModel.setMuteUntil(selectedContactKeys.toList(), muteUntil)
|
||||||
selectedContactKeys.clear()
|
selectedContactKeys.clear()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 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 com.geeksville.mesh.ui.contact
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.geeksville.mesh.channelSet
|
||||||
|
import com.geeksville.mesh.model.Contact
|
||||||
|
import com.geeksville.mesh.service.ServiceRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.meshtastic.core.data.repository.NodeRepository
|
||||||
|
import org.meshtastic.core.data.repository.PacketRepository
|
||||||
|
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||||
|
import org.meshtastic.core.database.entity.Packet
|
||||||
|
import org.meshtastic.core.model.DataPacket
|
||||||
|
import org.meshtastic.core.model.util.getChannel
|
||||||
|
import org.meshtastic.core.model.util.getShortDate
|
||||||
|
import org.meshtastic.core.strings.R
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.collections.map
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ContactsViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val nodeRepository: NodeRepository,
|
||||||
|
private val packetRepository: PacketRepository,
|
||||||
|
radioConfigRepository: RadioConfigRepository,
|
||||||
|
serviceRepository: ServiceRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
val ourNodeInfo = nodeRepository.ourNodeInfo
|
||||||
|
|
||||||
|
val connectionState = serviceRepository.connectionState
|
||||||
|
|
||||||
|
val channels =
|
||||||
|
radioConfigRepository.channelSetFlow.stateIn(
|
||||||
|
viewModelScope,
|
||||||
|
SharingStarted.WhileSubscribed(5_000),
|
||||||
|
channelSet {},
|
||||||
|
)
|
||||||
|
|
||||||
|
val contactList =
|
||||||
|
combine(
|
||||||
|
nodeRepository.myNodeInfo,
|
||||||
|
packetRepository.getContacts(),
|
||||||
|
channels,
|
||||||
|
packetRepository.getContactSettings(),
|
||||||
|
) { myNodeInfo, contacts, channelSet, settings ->
|
||||||
|
val myNodeNum = myNodeInfo?.myNodeNum ?: return@combine emptyList()
|
||||||
|
// Add empty channel placeholders (always show Broadcast contacts, even when empty)
|
||||||
|
val placeholder =
|
||||||
|
(0 until channelSet.settingsCount).associate { ch ->
|
||||||
|
val contactKey = "$ch${DataPacket.ID_BROADCAST}"
|
||||||
|
val data = DataPacket(bytes = null, dataType = 1, time = 0L, channel = ch)
|
||||||
|
contactKey to Packet(0L, myNodeNum, 1, contactKey, 0L, true, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
(contacts + (placeholder - contacts.keys)).values.map { packet ->
|
||||||
|
val data = packet.data
|
||||||
|
val contactKey = packet.contact_key
|
||||||
|
|
||||||
|
// Determine if this is my message (originated on this device)
|
||||||
|
val fromLocal = data.from == DataPacket.ID_LOCAL
|
||||||
|
val toBroadcast = data.to == DataPacket.ID_BROADCAST
|
||||||
|
|
||||||
|
// grab usernames from NodeInfo
|
||||||
|
val user = getUser(if (fromLocal) data.to else data.from)
|
||||||
|
val node = getNode(if (fromLocal) data.to else data.from)
|
||||||
|
|
||||||
|
val shortName = user.shortName
|
||||||
|
val longName =
|
||||||
|
if (toBroadcast) {
|
||||||
|
channelSet.getChannel(data.channel)?.name ?: context.getString(R.string.channel_name)
|
||||||
|
} else {
|
||||||
|
user.longName
|
||||||
|
}
|
||||||
|
|
||||||
|
Contact(
|
||||||
|
contactKey = contactKey,
|
||||||
|
shortName = if (toBroadcast) "${data.channel}" else shortName,
|
||||||
|
longName = longName,
|
||||||
|
lastMessageTime = getShortDate(data.time),
|
||||||
|
lastMessageText = if (fromLocal) data.text else "$shortName: ${data.text}",
|
||||||
|
unreadCount = packetRepository.getUnreadCount(contactKey),
|
||||||
|
messageCount = packetRepository.getMessageCount(contactKey),
|
||||||
|
isMuted = settings[contactKey]?.isMuted == true,
|
||||||
|
isUnmessageable = user.isUnmessagable,
|
||||||
|
nodeColors =
|
||||||
|
if (!toBroadcast) {
|
||||||
|
node.colors
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5_000),
|
||||||
|
initialValue = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getNode(userId: String?) = nodeRepository.getNode(userId ?: DataPacket.ID_BROADCAST)
|
||||||
|
|
||||||
|
fun deleteContacts(contacts: List<String>) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) { packetRepository.deleteContacts(contacts) }
|
||||||
|
|
||||||
|
fun setMuteUntil(contacts: List<String>, until: Long) =
|
||||||
|
viewModelScope.launch(Dispatchers.IO) { packetRepository.setMuteUntil(contacts, until) }
|
||||||
|
|
||||||
|
private fun getUser(userId: String?) = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST)
|
||||||
|
}
|
||||||
|
|
@ -40,13 +40,13 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.geeksville.mesh.model.Contact
|
import com.geeksville.mesh.model.Contact
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
|
||||||
import com.geeksville.mesh.ui.contact.ContactItem
|
import com.geeksville.mesh.ui.contact.ContactItem
|
||||||
|
import com.geeksville.mesh.ui.contact.ContactsViewModel
|
||||||
import org.meshtastic.core.strings.R
|
import org.meshtastic.core.strings.R
|
||||||
import org.meshtastic.core.ui.theme.AppTheme
|
import org.meshtastic.core.ui.theme.AppTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShareScreen(viewModel: UIViewModel = hiltViewModel(), onConfirm: (String) -> Unit) {
|
fun ShareScreen(viewModel: ContactsViewModel = hiltViewModel(), onConfirm: (String) -> Unit) {
|
||||||
val contactList by viewModel.contactList.collectAsStateWithLifecycle()
|
val contactList by viewModel.contactList.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
ShareScreen(contacts = contactList, onConfirm = onConfirm)
|
ShareScreen(contacts = contactList, onConfirm = onConfirm)
|
||||||
|
|
|
||||||
Ładowanie…
Reference in New Issue