diff --git a/app/src/androidTest/java/com/geeksville/mesh/PacketDaoTest.kt b/app/src/androidTest/java/com/geeksville/mesh/PacketDaoTest.kt index bb2e4ac7..1395115b 100644 --- a/app/src/androidTest/java/com/geeksville/mesh/PacketDaoTest.kt +++ b/app/src/androidTest/java/com/geeksville/mesh/PacketDaoTest.kt @@ -125,6 +125,24 @@ class PacketDaoTest { } } + @Test + fun test_getUnreadCount() = runBlocking { + testContactKeys.forEach { contactKey -> + val unreadCount = packetDao.getUnreadCount(contactKey) + assertEquals(SAMPLE_SIZE, unreadCount) + } + } + + @Test + fun test_clearUnreadCount() = runBlocking { + val timestamp = System.currentTimeMillis() + testContactKeys.forEach { contactKey -> + packetDao.clearUnreadCount(contactKey, timestamp) + val unreadCount = packetDao.getUnreadCount(contactKey) + assertEquals(0, unreadCount) + } + } + @Test fun test_deleteContacts() = runBlocking { packetDao.deleteContacts(testContactKeys) diff --git a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt index 0e81f268..63d75bd7 100644 --- a/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/database/PacketRepository.kt @@ -24,6 +24,14 @@ class PacketRepository @Inject constructor(private val packetDaoLazy: dagger.Laz packetDao.getMessageCount(contact) } + suspend fun getUnreadCount(contact: String): Int = withContext(Dispatchers.IO) { + packetDao.getUnreadCount(contact) + } + + suspend fun clearUnreadCount(contact: String, timestamp: Long) = withContext(Dispatchers.IO) { + packetDao.clearUnreadCount(contact, timestamp) + } + suspend fun getQueuedPackets(): List? = withContext(Dispatchers.IO) { packetDao.getQueuedPackets() } diff --git a/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt b/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt index e9556727..0edcd534 100644 --- a/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt +++ b/app/src/main/java/com/geeksville/mesh/database/dao/PacketDao.kt @@ -45,6 +45,25 @@ interface PacketDao { ) suspend fun getMessageCount(contact: String): Int + @Query( + """ + SELECT COUNT(*) FROM packet + WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo)) + AND port_num = 1 AND contact_key = :contact AND read = 0 + """ + ) + suspend fun getUnreadCount(contact: String): Int + + @Query( + """ + UPDATE packet + SET read = 1 + WHERE (myNodeNum = 0 OR myNodeNum = (SELECT myNodeNum FROM MyNodeInfo)) + AND port_num = 1 AND contact_key = :contact AND read = 0 AND received_time <= :timestamp + """ + ) + suspend fun clearUnreadCount(contact: String, timestamp: Long) + @Insert fun insert(packet: Packet) diff --git a/app/src/main/java/com/geeksville/mesh/model/ContactsViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/ContactsViewModel.kt index 6131713a..7214d2d6 100644 --- a/app/src/main/java/com/geeksville/mesh/model/ContactsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/ContactsViewModel.kt @@ -106,7 +106,7 @@ class ContactsViewModel @Inject constructor( longName = longName, lastMessageTime = getShortDateTime(data.time), lastMessageText = if (fromLocal) data.text else "$shortName: ${data.text}", - unreadCount = 0, + unreadCount = packetRepository.getUnreadCount(contactKey), messageCount = packetRepository.getMessageCount(contactKey), isMuted = settings[contactKey]?.isMuted == true, ) 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 7b20ade6..59b1e769 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -290,6 +290,10 @@ class UIViewModel @Inject constructor( packetRepository.deleteWaypoint(id) } + fun clearUnreadCount(contact: String, timestamp: Long) = viewModelScope.launch(Dispatchers.IO) { + packetRepository.clearUnreadCount(contact, timestamp) + } + companion object { fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index bc859920..df968284 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -603,7 +603,7 @@ class MeshService : Service(), Logging { dataPacket.dataType, contactKey, System.currentTimeMillis(), - true, // TODO isLocal + fromLocal, dataPacket ) serviceScope.handledLaunch { diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt index 83878616..c00bb7ff 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt @@ -1,6 +1,7 @@ package com.geeksville.mesh.ui import android.graphics.Color +import android.graphics.Rect import android.graphics.drawable.GradientDrawable import android.os.Bundle import android.view.* @@ -98,6 +99,21 @@ class MessagesFragment : Fragment(), Logging { if (itemCount > 0) layoutManager.scrollToPosition(itemCount - 1) } + fun scrollToFirstUnreadMessage() { + val position = messages.indexOfFirst { !it.read } + if (position > 0) { + val rect = Rect() + binding.toolbar.getGlobalVisibleRect(rect) + val toolbarOffset = rect.bottom + val offset = binding.messageListView.height - toolbarOffset + + layoutManager.scrollToPositionWithOffset(position, offset) + messages[position].apply { model.clearUnreadCount(contact_key, received_time) } + } else { + scrollToBottom() + } + } + override fun getItemCount(): Int = messages.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { @@ -226,13 +242,14 @@ class MessagesFragment : Fragment(), Logging { /// Called when our node DB changes fun onMessagesChanged(messages: List) { val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() - val shouldScrollToBottom = - lastVisibleItemPosition <= 0 || lastVisibleItemPosition == itemCount - 1 + val shouldScrollToUnread = lastVisibleItemPosition <= 0 + val shouldScrollToBottom = lastVisibleItemPosition == itemCount - 1 this.messages = messages notifyDataSetChanged() // FIXME, this is super expensive and redraws all messages if (shouldScrollToBottom) scrollToBottom() + if (shouldScrollToUnread) scrollToFirstUnreadMessage() } } @@ -283,6 +300,25 @@ class MessagesFragment : Fragment(), Logging { layoutManager.stackFromEnd = true // We want the last rows to always be shown binding.messageListView.layoutManager = layoutManager + binding.messageListView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val firstUnreadItem = messagesAdapter.messages.firstOrNull { !it.read } + if (firstUnreadItem != null && dy > 0) { + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + if (lastVisibleItemPosition != RecyclerView.NO_POSITION) { + val lastVisibleItem = messagesAdapter.messages[lastVisibleItemPosition] + val timestamp = lastVisibleItem.received_time + + if (timestamp > firstUnreadItem.received_time) { + model.clearUnreadCount(contactKey, timestamp) + } + } + } + } + }) + model.getMessagesFrom(contactKey).asLiveData().observe(viewLifecycleOwner) { debug("New messages received: ${it.size}") messagesAdapter.onMessagesChanged(it)