refactor: move `ContactsFragment` to main activity ViewModel

pull/1240/head
andrekir 2024-09-09 19:57:45 -03:00
rodzic 7411455e17
commit 41fc43b215
3 zmienionych plików z 90 dodań i 115 usunięć

Wyświetl plik

@ -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<String>, until: Long) = viewModelScope.launch(Dispatchers.IO) {
packetRepository.setMuteUntil(contacts, until)
}
fun deleteContacts(contacts: List<String>) = viewModelScope.launch(Dispatchers.IO) {
packetRepository.deleteContacts(contacts)
}
}

Wyświetl plik

@ -21,6 +21,7 @@ import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.MeshLogRepository import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.QuickChatActionRepository import com.geeksville.mesh.database.QuickChatActionRepository
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.QuickChatAction import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.repository.datastore.RadioConfigRepository import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.radio.RadioInterfaceService import com.geeksville.mesh.repository.radio.RadioInterfaceService
@ -30,7 +31,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow 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.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
@ -45,8 +46,11 @@ import kotlinx.coroutines.withContext
import java.io.BufferedWriter import java.io.BufferedWriter
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.FileWriter import java.io.FileWriter
import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt 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( data class Message(
val uuid: Long, val uuid: Long,
val receivedTime: Long, val receivedTime: Long,
@ -123,6 +138,18 @@ data class Message(
val status: MessageStatus?, 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 @HiltViewModel
class UIViewModel @Inject constructor( class UIViewModel @Inject constructor(
private val app: Application, private val app: Application,
@ -195,7 +222,7 @@ class UIViewModel @Inject constructor(
) )
}.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = WhileSubscribed(), started = Eagerly,
initialValue = NodesUiState.Empty, initialValue = NodesUiState.Empty,
) )
@ -204,7 +231,7 @@ class UIViewModel @Inject constructor(
nodeDB.getNodes(state.sort, state.filter, state.includeUnknown) nodeDB.getNodes(state.sort, state.filter, state.includeUnknown)
}.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = WhileSubscribed(5_000), started = Eagerly,
initialValue = emptyList(), initialValue = emptyList(),
) )
@ -239,6 +266,55 @@ class UIViewModel @Inject 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 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( fun getMessagesFrom(contactKey: String) = combine(
nodeDB.users, nodeDB.users,
packetRepository.getMessagesFrom(contactKey), packetRepository.getMessagesFrom(contactKey),
@ -333,6 +409,14 @@ class UIViewModel @Inject constructor(
} }
} }
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)
}
fun deleteMessages(uuidList: List<Long>) = viewModelScope.launch(Dispatchers.IO) { fun deleteMessages(uuidList: List<Long>) = viewModelScope.launch(Dispatchers.IO) {
packetRepository.deleteMessages(uuidList) packetRepository.deleteMessages(uuidList)
} }

Wyświetl plik

@ -23,12 +23,12 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.model.Contact 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.geeksville.mesh.ui.theme.AppTheme
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -39,7 +39,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
private val actionModeCallback: ActionModeCallback = ActionModeCallback() private val actionModeCallback: ActionModeCallback = ActionModeCallback()
private var actionMode: ActionMode? = null 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 contacts get() = model.contactList.value
private val selectedList = emptyList<String>().toMutableStateList() private val selectedList = emptyList<String>().toMutableStateList()