Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -302,6 +303,8 @@ class ThreadActivity : SimpleActivity() {
binding.messageHolder.threadTypeMessage.setSelection(smsDraft.length)
}
}

markThreadMessagesRead(threadId)
}

val bottomBarColor = getBottomBarColor()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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() {
Expand All @@ -221,4 +240,8 @@ abstract class BaseConversationsAdapter(
return Conversation.areContentsTheSame(oldItem, newItem)
}
}

companion object {
private const val MAX_UNREAD_BADGE_COUNT = 99
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@file:Suppress("MagicNumber")
package org.fossify.messages.databases

import android.content.Context
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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")
}
}
}
}
}
40 changes: 37 additions & 3 deletions app/src/main/kotlin/org/fossify/messages/extensions/Context.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -349,6 +377,7 @@ fun Context.getConversations(
val conversations = ArrayList<Conversation>()
val simpleContactHelper = SimpleContactsHelper(this)
val blockedNumbers = getBlockedNumbers()
val unreadMap = getUnreadCountsByThread()
try {
queryCursorUnsafe(
uri,
Expand Down Expand Up @@ -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,
Expand All @@ -406,7 +436,8 @@ fun Context.getConversations(
photoUri = photoUri,
isGroupConversation = isGroupConversation,
phoneNumber = phoneNumbers.first(),
isArchived = archived
isArchived = archived,
unreadCount = unreadCount,
)
conversations.add(conversation)
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
}
}
24 changes: 20 additions & 4 deletions app/src/main/res/layout/item_conversation.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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_toEndOf="parent"
app:layout_constraintTop_toTopOf="@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"
tools:text="42"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
2 changes: 1 addition & 1 deletion detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading