From 41fc43b2150f4857c1d08511a5378b80752e4bd0 Mon Sep 17 00:00:00 2001 From: andrekir Date: Mon, 9 Sep 2024 19:57:45 -0300 Subject: [PATCH] refactor: move `ContactsFragment` to main activity ViewModel --- .../mesh/model/ContactsViewModel.kt | 109 ------------------ .../java/com/geeksville/mesh/model/UIState.kt | 90 ++++++++++++++- .../geeksville/mesh/ui/ContactsFragment.kt | 6 +- 3 files changed, 90 insertions(+), 115 deletions(-) delete mode 100644 app/src/main/java/com/geeksville/mesh/model/ContactsViewModel.kt diff --git a/app/src/main/java/com/geeksville/mesh/model/ContactsViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/ContactsViewModel.kt deleted file mode 100644 index 6023ae990..000000000 --- a/app/src/main/java/com/geeksville/mesh/model/ContactsViewModel.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.geeksville.mesh.model - -import android.app.Application -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.geeksville.mesh.DataPacket -import com.geeksville.mesh.R -import com.geeksville.mesh.android.Logging -import com.geeksville.mesh.database.PacketRepository -import com.geeksville.mesh.database.entity.Packet -import com.geeksville.mesh.repository.datastore.ChannelSetRepository -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import java.text.DateFormat -import java.util.Date -import javax.inject.Inject - -data class Contact( - val contactKey: String, - val shortName: String, - val longName: String, - val lastMessageTime: String?, - val lastMessageText: String?, - val unreadCount: Int, - val messageCount: Int, - val isMuted: Boolean, -) - -// return time if within 24 hours, otherwise date -internal fun getShortDateTime(time: Long): String? { - val date = if (time != 0L) Date(time) else return null - val isWithin24Hours = System.currentTimeMillis() - date.time <= 24 * 60 * 60 * 1000L - - return if (isWithin24Hours) { - DateFormat.getTimeInstance(DateFormat.SHORT).format(date) - } else { - DateFormat.getDateInstance(DateFormat.SHORT).format(date) - } -} - -@HiltViewModel -class ContactsViewModel @Inject constructor( - private val app: Application, - private val nodeDB: NodeDB, - channelSetRepository: ChannelSetRepository, - private val packetRepository: PacketRepository, -) : ViewModel(), Logging { - - val contactList = combine( - nodeDB.myNodeInfo, - packetRepository.getContacts(), - channelSetRepository.channelSetFlow, - 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 node = nodeDB.nodes.value[if (fromLocal) data.to else data.from] - - val shortName = node?.user?.shortName ?: app.getString(R.string.unknown_node_short_name) - val longName = if (toBroadcast) { - channelSet.getChannel(data.channel)?.name ?: app.getString(R.string.channel_name) - } else { - node?.user?.longName ?: app.getString(R.string.unknown_username) - } - - Contact( - contactKey = contactKey, - shortName = if (toBroadcast) "${data.channel}" else shortName, - longName = longName, - lastMessageTime = getShortDateTime(data.time), - lastMessageText = if (fromLocal) data.text else "$shortName: ${data.text}", - unreadCount = packetRepository.getUnreadCount(contactKey), - messageCount = packetRepository.getMessageCount(contactKey), - isMuted = settings[contactKey]?.isMuted == true, - ) - } - }.stateIn( - scope = viewModelScope, - started = WhileSubscribed(5_000), - initialValue = emptyList(), - ) - - fun setMuteUntil(contacts: List, until: Long) = viewModelScope.launch(Dispatchers.IO) { - packetRepository.setMuteUntil(contacts, until) - } - - fun deleteContacts(contacts: List) = viewModelScope.launch(Dispatchers.IO) { - packetRepository.deleteContacts(contacts) - } -} diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 2761f4852..8da3e34a1 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -21,6 +21,7 @@ import com.geeksville.mesh.android.Logging import com.geeksville.mesh.database.MeshLogRepository import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.QuickChatActionRepository +import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.database.entity.QuickChatAction import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.radio.RadioInterfaceService @@ -30,7 +31,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed +import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull @@ -45,8 +46,11 @@ import kotlinx.coroutines.withContext import java.io.BufferedWriter import java.io.FileNotFoundException import java.io.FileWriter +import java.text.DateFormat import java.text.SimpleDateFormat +import java.util.Date import java.util.Locale +import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.math.roundToInt @@ -113,6 +117,17 @@ data class NodesUiState( } } +data class Contact( + val contactKey: String, + val shortName: String, + val longName: String, + val lastMessageTime: String?, + val lastMessageText: String?, + val unreadCount: Int, + val messageCount: Int, + val isMuted: Boolean, +) + data class Message( val uuid: Long, val receivedTime: Long, @@ -123,6 +138,18 @@ data class Message( val status: MessageStatus?, ) +// return time if within 24 hours, otherwise date +internal fun getShortDateTime(time: Long): String? { + val date = if (time != 0L) Date(time) else return null + val isWithin24Hours = System.currentTimeMillis() - date.time <= TimeUnit.DAYS.toMillis(1) + + return if (isWithin24Hours) { + DateFormat.getTimeInstance(DateFormat.SHORT).format(date) + } else { + DateFormat.getDateInstance(DateFormat.SHORT).format(date) + } +} + @HiltViewModel class UIViewModel @Inject constructor( private val app: Application, @@ -195,7 +222,7 @@ class UIViewModel @Inject constructor( ) }.stateIn( scope = viewModelScope, - started = WhileSubscribed(), + started = Eagerly, initialValue = NodesUiState.Empty, ) @@ -204,7 +231,7 @@ class UIViewModel @Inject constructor( nodeDB.getNodes(state.sort, state.filter, state.includeUnknown) }.stateIn( scope = viewModelScope, - started = WhileSubscribed(5_000), + started = Eagerly, initialValue = emptyList(), ) @@ -239,6 +266,55 @@ class UIViewModel @Inject constructor( 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 node = nodeDB.nodes.value[if (fromLocal) data.to else data.from] + + val shortName = node?.user?.shortName ?: app.getString(R.string.unknown_node_short_name) + val longName = if (toBroadcast) { + channelSet.getChannel(data.channel)?.name ?: app.getString(R.string.channel_name) + } else { + node?.user?.longName ?: app.getString(R.string.unknown_username) + } + + Contact( + contactKey = contactKey, + shortName = if (toBroadcast) "${data.channel}" else shortName, + longName = longName, + lastMessageTime = getShortDateTime(data.time), + lastMessageText = if (fromLocal) data.text else "$shortName: ${data.text}", + unreadCount = packetRepository.getUnreadCount(contactKey), + messageCount = packetRepository.getMessageCount(contactKey), + isMuted = settings[contactKey]?.isMuted == true, + ) + } + }.stateIn( + scope = viewModelScope, + started = Eagerly, + initialValue = emptyList(), + ) + fun getMessagesFrom(contactKey: String) = combine( nodeDB.users, packetRepository.getMessagesFrom(contactKey), @@ -333,6 +409,14 @@ class UIViewModel @Inject constructor( } } + fun setMuteUntil(contacts: List, until: Long) = viewModelScope.launch(Dispatchers.IO) { + packetRepository.setMuteUntil(contacts, until) + } + + fun deleteContacts(contacts: List) = viewModelScope.launch(Dispatchers.IO) { + packetRepository.deleteContacts(contacts) + } + fun deleteMessages(uuidList: List) = viewModelScope.launch(Dispatchers.IO) { packetRepository.deleteMessages(uuidList) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt index 33d26f1ba..005cddea3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ContactsFragment.kt @@ -23,12 +23,12 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.unit.dp -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.android.Logging import com.geeksville.mesh.R import com.geeksville.mesh.model.Contact -import com.geeksville.mesh.model.ContactsViewModel +import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.ui.theme.AppTheme import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint @@ -39,7 +39,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging { private val actionModeCallback: ActionModeCallback = ActionModeCallback() private var actionMode: ActionMode? = null - private val model: ContactsViewModel by viewModels() + private val model: UIViewModel by activityViewModels() private val contacts get() = model.contactList.value private val selectedList = emptyList().toMutableStateList()