Skip to content

Commit 0a404c1

Browse files
Shubham-344danascape
authored andcommitted
feat: Revamp searchScreen to show message previews in results
PR: #143 Signed-off-by: Saalim Quadri <danascape@gmail.com>
1 parent 274d0e6 commit 0a404c1

12 files changed

Lines changed: 349 additions & 55 deletions

File tree

data/src/main/java/com/moez/QKSMS/repository/ConversationRepositoryImpl.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import org.prauga.messages.model.Conversation
4141
import org.prauga.messages.model.Message
4242
import org.prauga.messages.model.Recipient
4343
import org.prauga.messages.model.SearchResult
44+
import org.prauga.messages.model.SearchItem
4445
import org.prauga.messages.util.PhoneNumberUtils
4546
import org.prauga.messages.util.tryOrNull
4647
import java.util.concurrent.TimeUnit
@@ -207,6 +208,77 @@ class ConversationRepositoryImpl @Inject constructor(
207208
return conversationMatches + messagesByConversation
208209
}
209210

211+
override fun searchConversationsGrouped(query: CharSequence): List<SearchItem> {
212+
val realm = Realm.getDefaultInstance()
213+
214+
val normalizedQuery = query.removeAccents()
215+
val conversations = realm.copyFromRealm(
216+
realm
217+
.where(Conversation::class.java)
218+
.notEqualTo("id", 0L)
219+
.isNotNull("lastMessage")
220+
.equalTo("blocked", false)
221+
.isNotEmpty("recipients")
222+
.sort("pinned", Sort.DESCENDING, "lastMessage.date", Sort.DESCENDING)
223+
.findAll()
224+
)
225+
226+
val conversationsById = conversations.associateBy { it.id }
227+
228+
// Get all messages matching the query, grouped by conversation
229+
val messagesByConversation = realm.copyFromRealm(
230+
realm
231+
.where(Message::class.java)
232+
.beginGroup()
233+
.contains("body", normalizedQuery, Case.INSENSITIVE)
234+
.or()
235+
.contains("parts.text", normalizedQuery, Case.INSENSITIVE)
236+
.endGroup()
237+
.sort("date", Sort.DESCENDING)
238+
.findAll()
239+
)
240+
.groupBy { message -> message.threadId }
241+
.mapNotNull { (threadId, messages) ->
242+
conversationsById[threadId]?.let { conversation ->
243+
Pair(conversation, messages)
244+
}
245+
}
246+
.sortedByDescending { (_, messages) -> messages.size }
247+
248+
realm.close()
249+
250+
// Build the flattened list with headers and messages
251+
val result = mutableListOf<SearchItem>()
252+
253+
messagesByConversation.forEach { (conversation, messages) ->
254+
// Add conversation header
255+
result.add(SearchItem.Header(
256+
conversationId = conversation.id,
257+
title = conversation.getTitle()
258+
))
259+
260+
// Add matching messages
261+
messages.forEach { message ->
262+
val body = when {
263+
message.body.isNotEmpty() -> message.body
264+
message.parts.isNotEmpty() -> message.parts.firstOrNull()?.text ?: ""
265+
else -> ""
266+
}
267+
268+
if (body.isNotEmpty()) {
269+
result.add(SearchItem.Message(
270+
messageId = message.id,
271+
conversationId = conversation.id,
272+
body = body,
273+
timestamp = message.date
274+
))
275+
}
276+
}
277+
}
278+
279+
return result
280+
}
281+
210282
override fun getBlockedConversations(): RealmResults<Conversation> =
211283
Realm.getDefaultInstance()
212284
.where(Conversation::class.java)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (C) 2025 Saalim Quadri <danascape@gmail.com>
3+
*/
4+
5+
package org.prauga.messages.model
6+
7+
sealed class SearchItem {
8+
data class Header(
9+
val conversationId: Long,
10+
val title: String
11+
) : SearchItem()
12+
13+
data class Message(
14+
val messageId: Long,
15+
val conversationId: Long,
16+
val body: String,
17+
val timestamp: Long
18+
) : SearchItem()
19+
}

domain/src/main/java/com/moez/QKSMS/repository/ConversationRepository.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ interface ConversationRepository {
4444

4545
fun searchConversations(query: CharSequence): List<SearchResult>
4646

47+
fun searchConversationsGrouped(query: CharSequence): List<org.prauga.messages.model.SearchItem>
48+
4749
fun getBlockedConversations(): RealmResults<Conversation>
4850

4951
fun getBlockedConversationsAsync(): RealmResults<Conversation>

presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,13 @@ class Navigator @Inject constructor(
150150
startActivity(intent)
151151
}
152152

153+
fun showConversation(threadId: Long, messageId: Long) {
154+
val intent = Intent(context, ComposeActivity::class.java)
155+
.putExtra("threadId", threadId)
156+
.putExtra("messageId", messageId)
157+
startActivity(intent)
158+
}
159+
153160
fun showConversationInfo(threadId: Long) {
154161
val intent = Intent(context, ConversationInfoActivity::class.java)
155162
intent.putExtra("threadId", threadId)

presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ class ComposeActivityModule {
4141
fun provideThreadId(activity: ComposeActivity): Long =
4242
activity.intent.extras?.getLong("threadId") ?: 0L
4343

44+
@Provides
45+
@Named("messageId")
46+
fun provideMessageId(activity: ComposeActivity): Long =
47+
activity.intent.extras?.getLong("messageId") ?: 0L
48+
4449
@Provides
4550
@Named("addresses")
4651
fun provideAddresses(activity: ComposeActivity): List<String> =

presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,14 @@ import timber.log.Timber
9292
import java.text.SimpleDateFormat
9393
import java.util.Locale
9494
import java.util.UUID
95+
import java.util.concurrent.TimeUnit
9596
import javax.inject.Inject
9697
import javax.inject.Named
9798

9899
class ComposeViewModel @Inject constructor(
99100
@Named("query") private val query: String,
100101
@Named("threadId") private val threadId: Long,
102+
@Named("messageId") private val messageId: Long,
101103
@Named("addresses") private val addresses: List<String>,
102104
@Named("text") private val sharedText: String,
103105
@Named("attachments") val sharedAttachments: List<Attachment>,
@@ -312,6 +314,19 @@ class ComposeViewModel @Inject constructor(
312314
return
313315
}
314316

317+
// Scroll to specific message if messageId was provided
318+
if (messageId != 0L) {
319+
disposables += messages
320+
.filter { it.isNotEmpty() }
321+
.take(1)
322+
.delay(300, TimeUnit.MILLISECONDS) // Small delay to ensure UI is ready
323+
.observeOn(AndroidSchedulers.mainThread())
324+
.autoDispose(view.scope())
325+
.subscribe {
326+
view.scrollToMessage(messageId)
327+
}
328+
}
329+
315330
val sharing = (sharedText.isNotEmpty() || sharedAttachments.isNotEmpty())
316331
if (shouldShowContacts) {
317332
shouldShowContacts = false

presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ class MainActivity : QkThemedActivity<MainActivityBinding>(MainActivityBinding::
210210
itemTouchCallback.adapter = conversationsAdapter
211211
conversationsAdapter.autoScrollToStart(binding.recyclerView)
212212

213+
// Setup search adapter click listener
214+
searchAdapter.onMessageClickListener = { conversationId, messageId ->
215+
navigator.showConversation(conversationId, messageId)
216+
}
217+
213218
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
214219
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
215220
super.onScrolled(recyclerView, dx, dy)
@@ -403,6 +408,7 @@ class MainActivity : QkThemedActivity<MainActivityBinding>(MainActivityBinding::
403408
if (binding.recyclerView.adapter !== searchAdapter) binding.recyclerView.adapter =
404409
searchAdapter
405410
searchAdapter.data = state.page.data ?: listOf()
411+
searchAdapter.setQuery(binding.toolbarSearch.text.toString())
406412
itemTouchHelper.attachToRecyclerView(null)
407413
binding.empty.setText(R.string.inbox_search_empty_text)
408414
}

presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package org.prauga.messages.feature.main
2222
import io.realm.RealmResults
2323
import org.prauga.messages.model.Conversation
2424
import org.prauga.messages.model.SearchResult
25+
import org.prauga.messages.model.SearchItem
2526
import org.prauga.messages.repository.SyncRepository
2627

2728
data class MainState(
@@ -51,7 +52,7 @@ data class Inbox(
5152

5253
data class Searching(
5354
val loading: Boolean = false,
54-
val data: List<SearchResult>? = null
55+
val data: List<SearchItem>? = null
5556
) : MainPage()
5657

5758
data class Archived(

presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import org.prauga.messages.manager.ChangelogManager
5252
import org.prauga.messages.manager.PermissionManager
5353
import org.prauga.messages.manager.RatingManager
5454
import org.prauga.messages.model.EmojiSyncNeeded
55+
import org.prauga.messages.model.SearchItem
5556
import org.prauga.messages.model.SearchResult
5657
import org.prauga.messages.model.SyncLog
5758
import org.prauga.messages.repository.ConversationRepository
@@ -307,13 +308,13 @@ class MainViewModel @Inject constructor(
307308
}
308309
}
309310
}
310-
Observable.empty<List<SearchResult>>()
311+
Observable.empty<List<SearchItem>>()
311312
} else {
312313
newState {
313314
val page = (page as? Searching) ?: Searching()
314315
copy(page = page.copy(loading = true))
315316
}
316-
Observable.fromCallable { conversationRepo.searchConversations(query) }
317+
Observable.fromCallable { conversationRepo.searchConversationsGrouped(query) }
317318
.subscribeOn(Schedulers.io())
318319
}
319320
}

0 commit comments

Comments
 (0)