feat: add ActionMenu option to mute contacts (#1003)

This commit is contained in:
Andre K 2024-04-28 16:18:16 -03:00 committed by GitHub
parent b409c17fe8
commit ecaf35d7f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 626 additions and 25 deletions

View file

@ -12,6 +12,7 @@ import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.dao.MeshLogDao
import com.geeksville.mesh.database.dao.NodeInfoDao
import com.geeksville.mesh.database.dao.QuickChatActionDao
import com.geeksville.mesh.database.entity.ContactSettings
import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.database.entity.QuickChatAction
@ -21,6 +22,7 @@ import com.geeksville.mesh.database.entity.QuickChatAction
MyNodeInfo::class,
NodeInfo::class,
Packet::class,
ContactSettings::class,
MeshLog::class,
QuickChatAction::class
],
@ -28,8 +30,9 @@ import com.geeksville.mesh.database.entity.QuickChatAction
AutoMigration (from = 3, to = 4),
AutoMigration (from = 4, to = 5),
AutoMigration (from = 5, to = 6),
AutoMigration (from = 6, to = 7),
],
version = 6,
version = 7,
exportSchema = true,
)
@TypeConverters(Converters::class)

View file

@ -3,6 +3,7 @@ package com.geeksville.mesh.database
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.database.dao.PacketDao
import com.geeksville.mesh.database.entity.ContactSettings
import com.geeksville.mesh.database.entity.Packet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@ -65,4 +66,14 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz
suspend fun update(packet: Packet) = withContext(Dispatchers.IO) {
packetDao.update(packet)
}
fun getContactSettings(): Flow<Map<String, ContactSettings>> = packetDao.getContactSettings()
suspend fun getContactSettings(contact: String) = withContext(Dispatchers.IO) {
packetDao.getContactSettings(contact) ?: ContactSettings(contact)
}
suspend fun setMuteUntil(contacts: List<String>, until: Long) = withContext(Dispatchers.IO) {
packetDao.setMuteUntil(contacts, until)
}
}

View file

@ -6,8 +6,10 @@ import androidx.room.MapColumn
import androidx.room.Update
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.database.entity.ContactSettings
import com.geeksville.mesh.database.entity.Packet
import kotlinx.coroutines.flow.Flow
@ -78,4 +80,22 @@ interface PacketDao {
val uuidList = getAllWaypoints().filter { it.data.waypoint?.id == id }.map { it.uuid }
deleteMessages(uuidList)
}
@Query("SELECT * FROM contact_settings")
fun getContactSettings(): Flow<Map<@MapColumn(columnName = "contact_key") String, ContactSettings>>
@Query("SELECT * FROM contact_settings WHERE contact_key = :contact")
suspend fun getContactSettings(contact:String): ContactSettings?
@Upsert
fun upsertContactSettings(contacts: List<ContactSettings>)
@Transaction
suspend fun setMuteUntil(contacts: List<String>, until: Long) {
val contactList = contacts.map { contact ->
getContactSettings(contact)?.copy(muteUntil = until)
?: ContactSettings(contact_key = contact, muteUntil = until)
}
upsertContactSettings(contactList)
}
}

View file

@ -12,5 +12,12 @@ data class Packet(
@ColumnInfo(name = "contact_key") val contact_key: String,
@ColumnInfo(name = "received_time") val received_time: Long,
@ColumnInfo(name = "data") val data: DataPacket
)
@Entity(tableName = "contact_settings")
data class ContactSettings(
@PrimaryKey val contact_key: String,
val muteUntil: Long = 0L,
) {
val isMuted get() = System.currentTimeMillis() <= muteUntil
}

View file

@ -222,6 +222,12 @@ class UIViewModel @Inject constructor(
contacts + (placeholder - contacts.keys)
}.asLiveData()
val contactSettings get() = packetRepository.getContactSettings()
fun setMuteUntil(contacts: List<String>, until: Long) = viewModelScope.launch(Dispatchers.IO) {
packetRepository.setMuteUntil(contacts, until)
}
@OptIn(ExperimentalCoroutinesApi::class)
val waypoints: LiveData<Map<Int, Packet>> = _packets.mapLatest { list ->
list.filter { it.port_num == Portnums.PortNum.WAYPOINT_APP_VALUE }

View file

@ -574,7 +574,7 @@ class MeshService : Service(), Logging {
Portnums.PortNum.WAYPOINT_APP_VALUE,
)
private fun rememberDataPacket(dataPacket: DataPacket) {
private fun rememberDataPacket(dataPacket: DataPacket, updateNotification: Boolean = true) {
if (dataPacket.dataType !in rememberDataType) return
val fromLocal = dataPacket.from == DataPacket.ID_LOCAL
val toBroadcast = dataPacket.to == DataPacket.ID_BROADCAST
@ -590,7 +590,13 @@ class MeshService : Service(), Logging {
System.currentTimeMillis(),
dataPacket
)
insertPacket(packetToSave)
serviceScope.handledLaunch {
packetRepository.get().apply {
insert(packetToSave)
val isMuted = getContactSettings(contactKey).isMuted
if (updateNotification && !isMuted) updateMessageNotification(dataPacket)
}
}
}
/// Update our model and resend as needed for a MeshPacket we just received from the radio
@ -625,15 +631,13 @@ class MeshService : Service(), Logging {
debug("Received CLEAR_TEXT from $fromId")
rememberDataPacket(dataPacket)
updateMessageNotification(dataPacket)
}
Portnums.PortNum.WAYPOINT_APP_VALUE -> {
val u = MeshProtos.Waypoint.parseFrom(data.payload)
// Validate locked Waypoints from the original sender
if (u.lockedTo != 0 && u.lockedTo != packet.from) return
rememberDataPacket(dataPacket)
if (u.expire > currentSecond()) updateMessageNotification(dataPacket)
rememberDataPacket(dataPacket, u.expire > currentSecond())
}
// Handle new style position info
@ -694,13 +698,11 @@ class MeshService : Service(), Logging {
if (!moduleConfig.rangeTest.enabled) return
val u = dataPacket.copy(dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)
rememberDataPacket(u)
updateMessageNotification(u)
}
Portnums.PortNum.DETECTION_SENSOR_APP_VALUE -> {
val u = dataPacket.copy(dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)
rememberDataPacket(u)
updateMessageNotification(u)
}
Portnums.PortNum.TRACEROUTE_APP_VALUE -> {
@ -849,7 +851,6 @@ class MeshService : Service(), Logging {
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
)
rememberDataPacket(u)
updateMessageNotification(u)
}
else -> {}
@ -1030,12 +1031,6 @@ class MeshService : Service(), Logging {
}
}
private fun insertPacket(packet: Packet) {
serviceScope.handledLaunch {
packetRepository.get().insert(packet)
}
}
private fun insertMeshLog(packetToSave: MeshLog) {
serviceScope.handledLaunch {
// Do not log, because might contain PII

View file

@ -7,6 +7,7 @@ import android.view.*
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.asLiveData
import androidx.recyclerview.widget.LinearLayoutManager
@ -14,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.ContactSettings
import com.geeksville.mesh.database.entity.Packet
import com.geeksville.mesh.databinding.AdapterContactLayoutBinding
import com.geeksville.mesh.databinding.FragmentContactsBinding
@ -21,7 +23,8 @@ import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.UIViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import java.util.*
import java.util.Date
import java.util.concurrent.TimeUnit
@AndroidEntryPoint
class ContactsFragment : ScreenFragment("Messages"), Logging {
@ -43,6 +46,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
val longName = itemView.longName
val lastMessageTime = itemView.lastMessageTime
val lastMessageText = itemView.lastMessageText
val mutedIcon = itemView.mutedIcon
}
private val contactsAdapter = object : RecyclerView.Adapter<ViewHolder>() {
@ -60,6 +64,9 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
var contacts = arrayOf<Packet>()
var selectedList = ArrayList<String>()
var contactSettings = mapOf<String, ContactSettings>()
val isAllMuted get() = selectedList.all { contactSettings[it]?.isMuted == true }
override fun getItemCount(): Int = contacts.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
@ -94,6 +101,8 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
holder.lastMessageTime.text = getShortDateTime(Date(contact.time))
} else holder.lastMessageTime.visibility = View.INVISIBLE
holder.mutedIcon.isVisible = contactSettings[packet.contact_key]?.isMuted == true
holder.itemView.setOnLongClickListener {
clickItem(holder, packet.contact_key)
if (actionMode == null) {
@ -148,6 +157,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
// show total items selected on action mode title
actionMode?.title = selectedList.size.toString()
}
actionMode?.invalidate()
notifyItemChanged(position)
}
@ -155,10 +165,6 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
this.contacts = contacts.values.toTypedArray()
notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes
}
fun onChannelsChanged() {
onContactsChanged(contacts.associateBy { it.contact_key })
}
}
override fun onPause() {
@ -180,10 +186,6 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
binding.contactsView.adapter = contactsAdapter
binding.contactsView.layoutManager = LinearLayoutManager(requireContext())
model.channels.asLiveData().observe(viewLifecycleOwner) {
contactsAdapter.onChannelsChanged()
}
model.nodeDB.nodes.asLiveData().observe(viewLifecycleOwner) {
contactsAdapter.notifyDataSetChanged()
}
@ -192,6 +194,11 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
debug("New contacts received: ${it.size}")
contactsAdapter.onContactsChanged(it)
}
model.contactSettings.asLiveData().observe(viewLifecycleOwner) {
contactsAdapter.contactSettings = it
contactsAdapter.notifyDataSetChanged()
}
}
override fun onDestroyView() {
@ -210,11 +217,49 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
menu.findItem(R.id.muteButton).setIcon(
if (contactsAdapter.isAllMuted) {
R.drawable.ic_twotone_volume_up_24
} else {
R.drawable.ic_twotone_volume_off_24
}
)
return false
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
when (item.itemId) {
R.id.muteButton -> if (contactsAdapter.isAllMuted) {
model.setMuteUntil(contactsAdapter.selectedList.toList(), 0L)
mode.finish()
} else {
var muteUntil: Long = Long.MAX_VALUE
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.mute_notifications)
.setSingleChoiceItems(
setOf(
R.string.mute_8_hours,
R.string.mute_1_week,
R.string.mute_always,
).map(::getString).toTypedArray(),
2
) { _, which ->
muteUntil = when (which) {
0 -> System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8)
1 -> System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7)
else -> Long.MAX_VALUE // always
}
}
.setPositiveButton(getString(R.string.okay)) { _, _ ->
debug("User clicked muteButton")
model.setMuteUntil(contactsAdapter.selectedList.toList(), muteUntil)
mode.finish()
}
.setNeutralButton(R.string.cancel) { _, _ ->
}
.show()
}
R.id.deleteButton -> {
val messagesTotal = model.packets.value.filter { it.port_num == 1 }
val selectedList = contactsAdapter.selectedList

View file

@ -333,6 +333,7 @@ class MessagesFragment : Fragment(), Logging {
private inner class ActionModeCallback : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.menu_messages, menu)
menu.findItem(R.id.muteButton).isVisible = false
mode.title = "1"
return true
}