diff --git a/CHANGELOG.md b/CHANGELOG.md
index 767ec6d6..2c4e9280 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed position reset when opening attachments in conversations ([#82])
+- Fixed automatic scroll to searched message in conversations ([#350])
## [1.4.0] - 2025-10-12
### Added
@@ -148,6 +149,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#13]: https://github.com/FossifyOrg/Messages/issues/13
[#70]: https://github.com/FossifyOrg/Messages/issues/70
[#75]: https://github.com/FossifyOrg/Messages/issues/75
+[#82]: https://github.com/FossifyOrg/Messages/issues/82
[#99]: https://github.com/FossifyOrg/Messages/issues/99
[#115]: https://github.com/FossifyOrg/Messages/issues/115
[#135]: https://github.com/FossifyOrg/Messages/issues/135
@@ -167,8 +169,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#309]: https://github.com/FossifyOrg/Messages/issues/309
[#334]: https://github.com/FossifyOrg/Messages/issues/334
[#349]: https://github.com/FossifyOrg/Messages/issues/349
+[#350]: https://github.com/FossifyOrg/Messages/issues/350
[#359]: https://github.com/FossifyOrg/Messages/issues/359
-[#82]: https://github.com/FossifyOrg/Messages/issues/82
[#456]: https://github.com/FossifyOrg/Messages/issues/456
[#461]: https://github.com/FossifyOrg/Messages/issues/461
diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 515a9b6d..c68cc930 100644
--- a/app/detekt-baseline.xml
+++ b/app/detekt-baseline.xml
@@ -12,7 +12,7 @@
CyclomaticComplexMethod:ThreadActivity.kt$ThreadActivity$@SuppressLint("MissingPermission") private fun getThreadItems(): ArrayList<ThreadItem>
CyclomaticComplexMethod:ThreadActivity.kt$ThreadActivity$private fun refreshMenuItems()
CyclomaticComplexMethod:ThreadActivity.kt$ThreadActivity$private fun setupButtons()
- CyclomaticComplexMethod:ThreadActivity.kt$ThreadActivity$private fun setupThread()
+ CyclomaticComplexMethod:ThreadActivity.kt$ThreadActivity$private fun setupThread(callback: () -> Unit)
EmptyCatchBlock:MessagesWriter.kt$MessagesWriter${ }
EmptyFunctionBlock:ArchivedConversationsAdapter.kt$ArchivedConversationsAdapter${}
EmptyFunctionBlock:BaseConversationsAdapter.kt$BaseConversationsAdapter${}
@@ -20,7 +20,6 @@
EmptyFunctionBlock:ManageBlockedKeywordsAdapter.kt$ManageBlockedKeywordsAdapter${}
EmptyFunctionBlock:RecycleBinConversationsAdapter.kt$RecycleBinConversationsAdapter${}
EmptyFunctionBlock:SearchResultsAdapter.kt$SearchResultsAdapter${}
- EmptyFunctionBlock:ThreadActivity.kt$ThreadActivity.<no name provided>${}
EmptyFunctionBlock:ThreadAdapter.kt$ThreadAdapter${}
ForbiddenComment:MainActivity.kt$MainActivity$// FIXME: Scheduled message date is being reset here. Conversations with
ForbiddenComment:ShortcutHelper.kt$ShortcutHelper$// TODO: verify that thread isn't in recycle bin
@@ -48,7 +47,6 @@
MagicNumber:ImageCompressor.kt$ImageCompressor$8
MagicNumber:ImageCompressor.kt$ImageCompressor$90f
MagicNumber:MainActivity.kt$MainActivity$30
- MagicNumber:Message.kt$Message.Companion$31
MagicNumber:MessagesDatabase.kt$MessagesDatabase.Companion.<no name provided>$3
MagicNumber:MessagesDatabase.kt$MessagesDatabase.Companion.<no name provided>$4
MagicNumber:MessagesDatabase.kt$MessagesDatabase.Companion.<no name provided>$5
@@ -76,14 +74,9 @@
MagicNumber:SmsStatusDeliveredReceiver.kt$SmsStatusDeliveredReceiver$3
MagicNumber:ThreadActivity.kt$ThreadActivity$0.4f
MagicNumber:ThreadActivity.kt$ThreadActivity$0.9f
- MagicNumber:ThreadActivity.kt$ThreadActivity$14
- MagicNumber:ThreadActivity.kt$ThreadActivity$15
MagicNumber:ThreadActivity.kt$ThreadActivity$150
- MagicNumber:ThreadActivity.kt$ThreadActivity$16
MagicNumber:ThreadActivity.kt$ThreadActivity$2
- MagicNumber:ThreadActivity.kt$ThreadActivity$20
MagicNumber:ThreadActivity.kt$ThreadActivity$30
- MagicNumber:ThreadActivity.kt$ThreadActivity$300
MagicNumber:ThreadActivity.kt$ThreadActivity$500L
MagicNumber:ThreadAdapter.kt$ThreadAdapter$0.8f
MagicNumber:ThreadAdapter.kt$ThreadAdapter$4
@@ -103,8 +96,6 @@
MaxLineLength:ConversationsDao.kt$ConversationsDao$@Query("SELECT (SELECT body FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NOT NULL AND messages.thread_id = conversations.thread_id ORDER BY messages.date DESC LIMIT 1) as new_snippet, * FROM conversations WHERE (SELECT COUNT(*) FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NOT NULL AND messages.thread_id = conversations.thread_id) > 0")
MaxLineLength:ConversationsDao.kt$ConversationsDao$@Query("SELECT (SELECT body FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NULL AND messages.thread_id = conversations.thread_id ORDER BY messages.date DESC LIMIT 1) as new_snippet, * FROM conversations WHERE archived = 0")
MaxLineLength:ConversationsDao.kt$ConversationsDao$@Query("SELECT (SELECT body FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NULL AND messages.thread_id = conversations.thread_id ORDER BY messages.date DESC LIMIT 1) as new_snippet, * FROM conversations WHERE archived = 1")
- MaxLineLength:DirectReplyReceiver.kt$DirectReplyReceiver$context.notificationHelper.showMessageNotification(messageId, address, body, threadId, bitmap, sender = null, alertOnlyOnce = true)
- MaxLineLength:DirectReplyReceiver.kt$DirectReplyReceiver$val message = context.getMessages(threadId, getImageResolutions = false, includeScheduledMessages = false, limit = 1).lastOrNull()
MaxLineLength:ExportBlockedKeywordsDialog.kt$ExportBlockedKeywordsDialog$exportBlockedKeywordsFilename.setText("${activity.getString(R.string.blocked_keywords)}_${activity.getCurrentFormattedDateTime()}")
MaxLineLength:Gson.kt$private val gsonBuilder = GsonBuilder().registerTypeAdapter(object : TypeToken<Map<String, Any>>() {}.type, MapDeserializerDoubleAsIntFix())
MaxLineLength:HeadlessSmsSendService.kt$HeadlessSmsSendService$val number = Uri.decode(intent.dataString!!.removePrefix("sms:").removePrefix("smsto:").removePrefix("mms").removePrefix("mmsto:").trim())
@@ -150,7 +141,6 @@
MaxLineLength:ThreadAdapter.kt$ThreadAdapter$mimetype.isImageMimeType() || mimetype.isVideoMimeType() -> setupImageView(holder, binding = this, message, attachment)
MaxLineLength:ThreadAdapter.kt$ThreadAdapter$mimetype.isVCardMimeType() -> setupVCardView(holder, threadMessageAttachmentsHolder, message, attachment)
MaxLineLength:ThreadAdapter.kt$ThreadAdapter$private
- MaxLineLength:ThreadAdapter.kt$ThreadAdapter$private fun getSelectedItems()
MaxLineLength:ThreadAdapter.kt$ThreadAdapter$threadSuccess.setImageResource(if (isDelivered) R.drawable.ic_check_double_vector else org.fossify.commons.R.drawable.ic_check_vector)
MaxLineLength:ThreadAdapter.kt$ThreadAdapter$val
MaxLineLength:ThreadAdapter.kt$ThreadAdapter.<no name provided>$override
@@ -169,23 +159,13 @@
NestedBlockDepth:MessagingUtils.kt$MessagingUtils$@Deprecated("TODO: Move/rewrite MMS code into the app.") fun sendMmsMessage( text: String, addresses: List<String>, attachment: Attachment?, settings: Settings, messageId: Long? = null )
NestedBlockDepth:MessagingUtils.kt$MessagingUtils$fun updateSmsMessageSendingStatus(messageUri: Uri?, type: Int)
NestedBlockDepth:SmsStatusDeliveredReceiver.kt$SmsStatusDeliveredReceiver$override fun updateAndroidDatabase(context: Context, intent: Intent, receiverResultCode: Int)
- NestedBlockDepth:ThreadActivity.kt$ThreadActivity$private fun fetchNextMessages()
- NestedBlockDepth:ThreadActivity.kt$ThreadActivity$private fun setupAttachmentSizes()
NestedBlockDepth:ThreadActivity.kt$ThreadActivity$private fun setupButtons()
NestedBlockDepth:ThreadAdapter.kt$ThreadAdapter$private fun setupSentMessageView(messageBinding: ItemMessageBinding, message: Message)
NestedBlockDepth:ThreadAdapter.kt$ThreadAdapter$private fun setupView(holder: ViewHolder, view: View, message: Message)
PrintStackTrace:Context.kt$e
PrintStackTrace:ScheduledMessageReceiver.kt$ScheduledMessageReceiver$e
PrintStackTrace:SmsManager.kt$e
- ReturnCount:Context.kt$fun Context.getFileSizeFromUri(uri: Uri): Long
- ReturnCount:Context.kt$fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto
ReturnCount:MapDeserializerDoubleAsIntFix.kt$MapDeserializerDoubleAsIntFix$fun read(element: JsonElement): Any?
- ReturnCount:MessagesReader.kt$MessagesReader$@SuppressLint("NewApi") private fun usePart(partId: Long, block: (InputStream) -> String): String
- ReturnCount:ShortcutHelper.kt$ShortcutHelper$fun shouldPresentShortcut(conv: Conversation): Boolean
- ReturnCount:SmsIntentParser.kt$SmsIntentParser$private fun extractBodyFromUri(uri: Uri?): String?
- ReturnCount:SmsIntentParser.kt$SmsIntentParser$private fun parseRecipientsFromUri(uri: Uri?): Array<String>?
- ReturnCount:ThreadActivity.kt$ThreadActivity$private fun addAttachment(uri: Uri)
- ReturnCount:VCardParser.kt$fun VCard?.parseNameFromVCard(): String?
SpreadOperator:Context.kt$(*scheduledMessages)
SpreadOperator:MainActivity.kt$MainActivity$(*currentMessages.toTypedArray())
SpreadOperator:ThreadActivity.kt$ThreadActivity$(*currentMessages.toTypedArray())
@@ -256,21 +236,11 @@
TooManyFunctions:ShortcutHelper.kt$ShortcutHelper
TooManyFunctions:ThreadActivity.kt$ThreadActivity : SimpleActivity
TooManyFunctions:ThreadAdapter.kt$ThreadAdapter : MyRecyclerViewListAdapter
- UnusedParameter:ArchivedConversationsActivity.kt$ArchivedConversationsActivity$event: Events.RefreshMessages
- UnusedParameter:MainActivity.kt$MainActivity$event: Events.RefreshMessages
- UnusedParameter:RecycleBinConversationsActivity.kt$RecycleBinConversationsActivity$event: Events.RefreshMessages
- UnusedParameter:ThreadActivity.kt$ThreadActivity$event: Events.RefreshMessages
UseCheckOrError:AttachmentUtils.kt$AttachmentUtils$throw IllegalStateException()
UseCheckOrError:MessagesImporter.kt$MessagesImporter$throw IllegalStateException()
UseRequire:SmsSender.kt$SmsSender$throw IllegalArgumentException("SmsSender: empty text message")
VariableNaming:MainActivity.kt$MainActivity$private val MAKE_DEFAULT_APP_REQUEST = 1
VariableNaming:MessagesWriter.kt$MessagesWriter$private val INVALID_ID = -1L
- VariableNaming:ThreadActivity.kt$ThreadActivity$private val MIN_DATE_TIME_DIFF_SECS = 300
- VariableNaming:ThreadActivity.kt$ThreadActivity$private val SCROLL_TO_BOTTOM_FAB_LIMIT = 20
- VariableNaming:ThreadActivity.kt$ThreadActivity$private val TYPE_DELETE = 16
- VariableNaming:ThreadActivity.kt$ThreadActivity$private val TYPE_EDIT = 14
- VariableNaming:ThreadActivity.kt$ThreadActivity$private val TYPE_SEND = 15
- WildcardImport:ArchivedConversationsActivity.kt$import org.fossify.commons.extensions.*
WildcardImport:AttachmentPreviews.kt$import org.fossify.commons.extensions.*
WildcardImport:AttachmentPreviews.kt$import org.fossify.messages.extensions.*
WildcardImport:AttachmentsAdapter.kt$import org.fossify.commons.extensions.*
@@ -280,13 +250,6 @@
WildcardImport:JsonElement.kt$import com.google.gson.*
WildcardImport:ManageBlockedKeywordsAdapter.kt$import android.view.*
WildcardImport:MessagesDao.kt$import androidx.room.*
- WildcardImport:SmsReceiver.kt$import org.fossify.messages.extensions.*
- WildcardImport:SmsStatusSentReceiver.kt$import org.fossify.messages.extensions.*
- WildcardImport:ThreadAdapter.kt$import org.fossify.commons.extensions.*
- WildcardImport:ThreadAdapter.kt$import org.fossify.messages.databinding.*
- WildcardImport:ThreadAdapter.kt$import org.fossify.messages.extensions.*
- WildcardImport:ThreadAdapter.kt$import org.fossify.messages.helpers.*
- WildcardImport:ThreadAdapter.kt$import org.fossify.messages.models.ThreadItem.*
WildcardImport:VCard.kt$import ezvcard.property.*
WildcardImport:VCardViewerAdapter.kt$import org.fossify.commons.extensions.*
diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml
index 5c5de559..2faad604 100644
--- a/app/lint-baseline.xml
+++ b/app/lint-baseline.xml
@@ -29,7 +29,7 @@
errorLine1="app-build-targetSDK = "34""
errorLine2=" ~~~~">
@@ -51,7 +51,7 @@
errorLine1="distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
@@ -62,7 +62,7 @@
errorLine1="gradlePlugins-agp = "8.11.1""
errorLine2=" ~~~~~~~~">
@@ -73,7 +73,7 @@
errorLine1="androidx-lifecycleprocess = "2.8.7""
errorLine2=" ~~~~~~~">
@@ -84,7 +84,7 @@
errorLine1="app-build-compileSDKVersion = "34""
errorLine2=" ~~~~">
@@ -212,7 +212,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2149,21 +2182,10 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-
-
-
-
Unit) {
if (conversation == null && isLaunchedFromShortcut) {
if (isTaskRoot) {
Intent(this, MainActivity::class.java).apply {
@@ -494,6 +494,7 @@ class ThreadActivity : SimpleActivity() {
try {
if (participants.isNotEmpty() && messages.hashCode() == cachedMessagesCode && !hasParticipantWithoutName) {
setupAdapter()
+ runOnUiThread { callback() }
return@ensureBackgroundThread
}
} catch (ignored: Exception) {
@@ -553,6 +554,7 @@ class ThreadActivity : SimpleActivity() {
runOnUiThread {
setupThreadTitle()
setupSIMSelector()
+ callback()
}
}
}
@@ -717,7 +719,44 @@ class ThreadActivity : SimpleActivity() {
}
}
+ private fun jumpToMessage(messageId: Long) {
+ if (messages.any { it.id == messageId }) {
+ val index = threadItems.indexOfFirst { (it as? Message)?.id == messageId }
+ if (index != -1) binding.threadMessagesList.smoothScrollToPosition(index)
+ return
+ }
+
+ ensureBackgroundThread {
+ if (loadingOlderMessages) return@ensureBackgroundThread
+ loadingOlderMessages = true
+ isJumpingToMessage = true
+
+ var cutoff = messages.firstOrNull()?.date ?: Int.MAX_VALUE
+ var found = false
+ var loops = 0
+
+ // not the best solution, but this will do for now.
+ while (!found && !allMessagesFetched) {
+ if (fetchOlderMessages(cutoff).isEmpty() || loops >= 1000) break
+ cutoff = messages.first().date
+ found = messages.any { it.id == messageId }
+ loops++
+ }
+
+ threadItems = getThreadItems()
+ runOnUiThread {
+ loadingOlderMessages = false
+ val index = threadItems.indexOfFirst { (it as? Message)?.id == messageId }
+ getOrCreateThreadAdapter().updateMessages(
+ newMessages = threadItems, scrollPosition = index, smoothScroll = true
+ )
+ isJumpingToMessage = false
+ }
+ }
+ }
+
private fun tryLoadMoreMessages() {
+ if (isJumpingToMessage) return
val layoutManager = binding.threadMessagesList.layoutManager as LinearLayoutManager
if (layoutManager.findFirstVisibleItemPosition() <= PREFETCH_THRESHOLD) {
loadMoreMessages()
@@ -726,21 +765,11 @@ class ThreadActivity : SimpleActivity() {
private fun loadMoreMessages() {
if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) return
-
- val firstItem = messages.first()
- val dateOfFirstItem = firstItem.date
-
- oldestMessageDate = dateOfFirstItem
loadingOlderMessages = true
-
+ val cutoff = messages.first().date
ensureBackgroundThread {
- val olderMessages = getMessages(threadId, oldestMessageDate)
- .filterNotInByKey(messages) { it.getStableId() }
-
- messages.addAll(0, olderMessages)
- allMessagesFetched = olderMessages.isEmpty()
+ fetchOlderMessages(cutoff)
threadItems = getThreadItems()
-
runOnUiThread {
loadingOlderMessages = false
getOrCreateThreadAdapter().updateMessages(threadItems)
@@ -748,23 +777,32 @@ class ThreadActivity : SimpleActivity() {
}
}
+ private fun fetchOlderMessages(cutoff: Int): List {
+ val older = getMessages(threadId, cutoff)
+ .filterNotInByKey(messages) { it.getStableId() }
+
+ if (older.isEmpty()) {
+ allMessagesFetched = true
+ return older
+ }
+
+ messages.addAll(0, older)
+ return older
+ }
+
private fun loadConversation() {
handlePermission(PERMISSION_READ_PHONE_STATE) { granted ->
if (granted) {
setupButtons()
setupConversation()
setupCachedMessages {
- val searchedMessageId = intent.getLongExtra(SEARCHED_MESSAGE_ID, -1L)
- intent.removeExtra(SEARCHED_MESSAGE_ID)
- if (searchedMessageId != -1L) {
- val index =
- threadItems.indexOfFirst { (it as? Message)?.id == searchedMessageId }
- if (index != -1) {
- binding.threadMessagesList.smoothScrollToPosition(index)
+ setupThread {
+ val searchedMessageId = intent.getLongExtra(SEARCHED_MESSAGE_ID, -1L)
+ intent.removeExtra(SEARCHED_MESSAGE_ID)
+ if (searchedMessageId != -1L) {
+ jumpToMessage(searchedMessageId)
}
}
-
- setupThread()
setupScrollListener()
}
} else {
@@ -1712,14 +1750,13 @@ class ThreadActivity : SimpleActivity() {
}
@Subscribe(threadMode = ThreadMode.ASYNC)
- fun refreshMessages(event: Events.RefreshMessages) {
+ fun refreshMessages(@Suppress("unused") event: Events.RefreshMessages) {
if (isRecycleBin) {
return
}
refreshedSinceSent = true
allMessagesFetched = false
- oldestMessageDate = -1
if (isActivityVisible) {
notificationManager.cancel(threadId.hashCode())
diff --git a/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt b/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt
index 86d13fbe..a3a8518b 100644
--- a/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt
+++ b/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt
@@ -328,11 +328,19 @@ class ThreadAdapter(
private fun isThreadDateTime(position: Int) = currentList.getOrNull(position) is ThreadDateTime
- fun updateMessages(newMessages: ArrayList, scrollPosition: Int = -1) {
+ fun updateMessages(
+ newMessages: ArrayList,
+ scrollPosition: Int = -1,
+ smoothScroll: Boolean = false
+ ) {
val latestMessages = newMessages.toMutableList()
submitList(latestMessages) {
if (scrollPosition != -1) {
- recyclerView.scrollToPosition(scrollPosition)
+ if (smoothScroll) {
+ recyclerView.smoothScrollToPosition(scrollPosition)
+ } else {
+ recyclerView.scrollToPosition(scrollPosition)
+ }
}
}
}