kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
refactor: move `ContactsFragment` to main activity ViewModel
rodzic
7411455e17
commit
41fc43b215
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Ładowanie…
Reference in New Issue