feat: add unread badge count for conversations (#560)
* feat: add unread badge count for conversations * fix: mark all messages read on thread open * fix: address some lint issues * fix: mark as read/unread in the local db as well * docs: update changelog Refs: https://github.com/FossifyOrg/Messages/issues/264, https://github.com/FossifyOrg/Messages/issues/177
This commit is contained in:
parent
627f19471e
commit
0a66c46f0d
8 changed files with 110 additions and 16 deletions
|
|
@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Unread badge count for conversations ([#177])
|
||||
|
||||
### Changed
|
||||
- Optimized loading messages in conversations
|
||||
- Updated conversation item design to be more compact ([#376])
|
||||
|
|
@ -13,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Fixed position reset when opening attachments in conversations ([#82])
|
||||
- Fixed automatic scroll to searched message in conversations ([#350])
|
||||
- Fixed non-standard text and avatar sizes in list items
|
||||
- Fixed "Mark as read" not working in some cases ([#264])
|
||||
|
||||
## [1.4.0] - 2025-10-12
|
||||
### Added
|
||||
|
|
@ -157,12 +161,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#135]: https://github.com/FossifyOrg/Messages/issues/135
|
||||
[#153]: https://github.com/FossifyOrg/Messages/issues/153
|
||||
[#165]: https://github.com/FossifyOrg/Messages/issues/165
|
||||
[#177]: https://github.com/FossifyOrg/Messages/issues/177
|
||||
[#180]: https://github.com/FossifyOrg/Messages/issues/180
|
||||
[#209]: https://github.com/FossifyOrg/Messages/issues/209
|
||||
[#217]: https://github.com/FossifyOrg/Messages/issues/217
|
||||
[#225]: https://github.com/FossifyOrg/Messages/issues/225
|
||||
[#243]: https://github.com/FossifyOrg/Messages/issues/243
|
||||
[#262]: https://github.com/FossifyOrg/Messages/issues/262
|
||||
[#264]: https://github.com/FossifyOrg/Messages/issues/264
|
||||
[#274]: https://github.com/FossifyOrg/Messages/issues/274
|
||||
[#279]: https://github.com/FossifyOrg/Messages/issues/279
|
||||
[#287]: https://github.com/FossifyOrg/Messages/issues/287
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ import org.fossify.messages.extensions.isGifMimeType
|
|||
import org.fossify.messages.extensions.isImageMimeType
|
||||
import org.fossify.messages.extensions.launchConversationDetails
|
||||
import org.fossify.messages.extensions.markMessageRead
|
||||
import org.fossify.messages.extensions.markThreadMessagesRead
|
||||
import org.fossify.messages.extensions.markThreadMessagesUnread
|
||||
import org.fossify.messages.extensions.messagesDB
|
||||
import org.fossify.messages.extensions.moveMessageToRecycleBin
|
||||
|
|
@ -302,6 +303,8 @@ class ThreadActivity : SimpleActivity() {
|
|||
binding.messageHolder.threadTypeMessage.setSelection(smsDraft.length)
|
||||
}
|
||||
}
|
||||
|
||||
markThreadMessagesRead(threadId)
|
||||
}
|
||||
|
||||
val bottomBarColor = getBottomBarColor()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.os.Parcelable
|
|||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
|
|
@ -14,6 +15,7 @@ import org.fossify.commons.adapters.MyRecyclerViewListAdapter
|
|||
import org.fossify.commons.extensions.applyColorFilter
|
||||
import org.fossify.commons.extensions.beVisibleIf
|
||||
import org.fossify.commons.extensions.formatDateOrTime
|
||||
import org.fossify.commons.extensions.getContrastColor
|
||||
import org.fossify.commons.extensions.getTextSize
|
||||
import org.fossify.commons.extensions.setupViewBackground
|
||||
import org.fossify.commons.helpers.SimpleContactsHelper
|
||||
|
|
@ -171,21 +173,23 @@ abstract class BaseConversationsAdapter(
|
|||
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize * 0.8f)
|
||||
}
|
||||
|
||||
val style = if (conversation.read) {
|
||||
conversationBodyShort.alpha = 0.7f
|
||||
if (conversation.isScheduled) Typeface.ITALIC else Typeface.NORMAL
|
||||
} else {
|
||||
val isUnread = !conversation.read
|
||||
val style = if (isUnread) {
|
||||
conversationBodyShort.alpha = 1f
|
||||
if (conversation.isScheduled) Typeface.BOLD_ITALIC else Typeface.BOLD
|
||||
|
||||
} else {
|
||||
conversationBodyShort.alpha = 0.7f
|
||||
if (conversation.isScheduled) Typeface.ITALIC else Typeface.NORMAL
|
||||
}
|
||||
conversationAddress.setTypeface(null, style)
|
||||
conversationBodyShort.setTypeface(null, style)
|
||||
conversationDate.setTypeface(null, style)
|
||||
|
||||
arrayListOf(conversationAddress, conversationBodyShort, conversationDate).forEach {
|
||||
it.setTextColor(textColor)
|
||||
}
|
||||
|
||||
setupBadgeCount(unreadCountBadge, isUnread, conversation.unreadCount)
|
||||
// at group conversations we use an icon as the placeholder, not any letter
|
||||
val placeholder = if (conversation.isGroupConversation) {
|
||||
SimpleContactsHelper(activity).getColoredGroupIcon(conversation.title)
|
||||
|
|
@ -202,6 +206,21 @@ abstract class BaseConversationsAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupBadgeCount(view: TextView, isUnread: Boolean, count: Int) {
|
||||
view.apply {
|
||||
beVisibleIf(isUnread)
|
||||
if (isUnread) {
|
||||
text = when {
|
||||
count > MAX_UNREAD_BADGE_COUNT -> "$MAX_UNREAD_BADGE_COUNT+"
|
||||
count == 0 -> ""
|
||||
else -> count.toString()
|
||||
}
|
||||
setTextColor(properPrimaryColor.getContrastColor())
|
||||
background?.applyColorFilter(properPrimaryColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChange(position: Int) = currentList.getOrNull(position)?.title ?: ""
|
||||
|
||||
private fun saveRecyclerViewState() {
|
||||
|
|
@ -221,4 +240,8 @@ abstract class BaseConversationsAdapter(
|
|||
return Conversation.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_UNREAD_BADGE_COUNT = 99
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
package org.fossify.messages.databases
|
||||
|
||||
import android.content.Context
|
||||
|
|
@ -29,7 +30,7 @@ import org.fossify.messages.models.RecycleBinMessage
|
|||
RecycleBinMessage::class,
|
||||
Draft::class
|
||||
],
|
||||
version = 9
|
||||
version = 10
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class MessagesDatabase : RoomDatabase() {
|
||||
|
|
@ -65,6 +66,7 @@ abstract class MessagesDatabase : RoomDatabase() {
|
|||
.addMigrations(MIGRATION_6_7)
|
||||
.addMigrations(MIGRATION_7_8)
|
||||
.addMigrations(MIGRATION_8_9)
|
||||
.addMigrations(MIGRATION_9_10)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -154,5 +156,13 @@ abstract class MessagesDatabase : RoomDatabase() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_9_10 = object : Migration(9, 10) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.apply {
|
||||
execSQL("ALTER TABLE conversations ADD COLUMN unread_count INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -318,6 +318,34 @@ fun Context.getMMSSender(msgId: Long): String {
|
|||
return ""
|
||||
}
|
||||
|
||||
fun Context.getUnreadCountsByThread(): Map<Long, Int> {
|
||||
val result = HashMap<Long, Int>(128)
|
||||
|
||||
fun bump(id: Long) {
|
||||
result[id] = (result[id] ?: 0) + 1
|
||||
}
|
||||
|
||||
// Unread SMS
|
||||
queryCursor(
|
||||
uri = Sms.CONTENT_URI,
|
||||
projection = arrayOf(Sms.THREAD_ID),
|
||||
selection = "${Sms.READ}=0 AND ${Sms.TYPE}=${Sms.MESSAGE_TYPE_INBOX}",
|
||||
selectionArgs = null,
|
||||
showErrors = false
|
||||
) { bump(it.getLongValue(Sms.THREAD_ID)) }
|
||||
|
||||
// Unread MMS
|
||||
queryCursor(
|
||||
uri = Mms.CONTENT_URI,
|
||||
projection = arrayOf(Mms.THREAD_ID),
|
||||
selection = "${Mms.READ}=0 AND ${Mms.MESSAGE_BOX}=${Mms.MESSAGE_BOX_INBOX}",
|
||||
selectionArgs = null,
|
||||
showErrors = false
|
||||
) { bump(it.getLongValue(Mms.THREAD_ID)) }
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun Context.getConversations(
|
||||
threadId: Long? = null,
|
||||
privateContacts: ArrayList<SimpleContact> = ArrayList(),
|
||||
|
|
@ -349,6 +377,7 @@ fun Context.getConversations(
|
|||
val conversations = ArrayList<Conversation>()
|
||||
val simpleContactHelper = SimpleContactsHelper(this)
|
||||
val blockedNumbers = getBlockedNumbers()
|
||||
val unreadMap = getUnreadCountsByThread()
|
||||
try {
|
||||
queryCursorUnsafe(
|
||||
uri,
|
||||
|
|
@ -397,6 +426,7 @@ fun Context.getConversations(
|
|||
val read = cursor.getIntValue(Threads.READ) == 1
|
||||
val archived =
|
||||
if (archiveAvailable) cursor.getIntValue(Threads.ARCHIVED) == 1 else false
|
||||
val unreadCount = if (!read) unreadMap[id] ?: 0 else 0
|
||||
val conversation = Conversation(
|
||||
threadId = id,
|
||||
snippet = snippet,
|
||||
|
|
@ -406,7 +436,8 @@ fun Context.getConversations(
|
|||
photoUri = photoUri,
|
||||
isGroupConversation = isGroupConversation,
|
||||
phoneNumber = phoneNumbers.first(),
|
||||
isArchived = archived
|
||||
isArchived = archived,
|
||||
unreadCount = unreadCount,
|
||||
)
|
||||
conversations.add(conversation)
|
||||
}
|
||||
|
|
@ -973,6 +1004,7 @@ fun Context.markThreadMessagesRead(threadId: Long) {
|
|||
contentResolver.update(uri, contentValues, selection, selectionArgs)
|
||||
}
|
||||
messagesDB.markThreadRead(threadId)
|
||||
conversationsDB.markRead(threadId)
|
||||
}
|
||||
|
||||
fun Context.markThreadMessagesUnread(threadId: Long) {
|
||||
|
|
@ -985,7 +1017,8 @@ fun Context.markThreadMessagesUnread(threadId: Long) {
|
|||
val selectionArgs = arrayOf(threadId.toString())
|
||||
contentResolver.update(uri, contentValues, selection, selectionArgs)
|
||||
}
|
||||
}
|
||||
conversationsDB.markUnread(threadId)
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
fun Context.getThreadId(address: String): Long {
|
||||
|
|
@ -1258,7 +1291,8 @@ fun Context.createTemporaryThread(
|
|||
phoneNumber = addresses.first(),
|
||||
isScheduled = true,
|
||||
usesCustomTitle = cachedConv?.usesCustomTitle == true,
|
||||
isArchived = false
|
||||
isArchived = false,
|
||||
unreadCount = 0,
|
||||
)
|
||||
try {
|
||||
conversationsDB.insertOrUpdate(conversation)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ data class Conversation(
|
|||
@ColumnInfo(name = "phone_number") var phoneNumber: String,
|
||||
@ColumnInfo(name = "is_scheduled") var isScheduled: Boolean = false,
|
||||
@ColumnInfo(name = "uses_custom_title") var usesCustomTitle: Boolean = false,
|
||||
@ColumnInfo(name = "archived") var isArchived: Boolean = false
|
||||
@ColumnInfo(name = "archived") var isArchived: Boolean = false,
|
||||
@ColumnInfo(name = "unread_count") var unreadCount: Int = 0,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
|
@ -32,7 +33,8 @@ data class Conversation(
|
|||
old.title == new.title &&
|
||||
old.photoUri == new.photoUri &&
|
||||
old.isGroupConversation == new.isGroupConversation &&
|
||||
old.phoneNumber == new.phoneNumber
|
||||
old.phoneNumber == new.phoneNumber &&
|
||||
old.unreadCount == new.unreadCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,12 +88,28 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/pin_indicator"
|
||||
android:layout_width="@dimen/pin_icon_size"
|
||||
android:layout_height="@dimen/pin_icon_size"
|
||||
android:layout_width="@dimen/small_icon_size"
|
||||
android:layout_height="@dimen/small_icon_size"
|
||||
android:alpha="0.7"
|
||||
android:padding="@dimen/tiny_margin"
|
||||
android:src="@drawable/ic_pin_filled_vector"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/conversation_body_short"
|
||||
app:layout_constraintEnd_toStartOf="@id/unread_count_badge"
|
||||
app:layout_constraintTop_toTopOf="@id/conversation_body_short"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unread_count_badge"
|
||||
android:layout_width="@dimen/small_icon_size"
|
||||
android:layout_height="@dimen/small_icon_size"
|
||||
android:background="@drawable/circle_background"
|
||||
android:gravity="center"
|
||||
android:textSize="@dimen/list_tertiary_text_size"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/conversation_body_short"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/conversation_body_short" />
|
||||
|
||||
app:layout_constraintTop_toTopOf="@id/conversation_body_short"
|
||||
tools:text="42"
|
||||
tools:visibility="visible" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ style:
|
|||
active: true
|
||||
ignoreAnnotated: ["Composable"]
|
||||
ignoreEnums: true
|
||||
ignoreNumbers: ["-1", "0", "1", "2", "42", "1000"]
|
||||
ignoreNumbers: ["-1", "0", "1", "2", "42", "128", "256", "1000"]
|
||||
MaxLineLength:
|
||||
active: true
|
||||
maxLineLength: 120
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue