mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-30 13:35:18 +00:00
Update empty search
This commit is contained in:
parent
afd240dcce
commit
1f1c51669c
@ -59,6 +59,7 @@ import org.greenrobot.eventbus.EventBus
|
|||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.messaging.jobs.JobQueue
|
import org.session.libsession.messaging.jobs.JobQueue
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
import org.session.libsession.snode.SnodeAPI
|
import org.session.libsession.snode.SnodeAPI
|
||||||
@ -66,6 +67,8 @@ import org.session.libsession.utilities.Address
|
|||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.ProfilePictureModifiedEvent
|
import org.session.libsession.utilities.ProfilePictureModifiedEvent
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.session.libsession.utilities.associateByNotNull
|
||||||
|
import org.session.libsession.utilities.groupByNotNull
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
import org.session.libsignal.utilities.ThreadUtils
|
import org.session.libsignal.utilities.ThreadUtils
|
||||||
@ -274,49 +277,69 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// monitor the global search VM query
|
// monitor the global search VM query
|
||||||
launch {
|
launch {
|
||||||
binding.globalSearchInputLayout.query
|
binding.globalSearchInputLayout.query
|
||||||
.onEach(globalSearchViewModel::postQuery)
|
.onEach(globalSearchViewModel::postQuery)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
// Get group results and display them
|
// Get group results and display them
|
||||||
launch {
|
launch {
|
||||||
globalSearchViewModel.result.collect { result ->
|
globalSearchViewModel.result.collect { result ->
|
||||||
val currentUserPublicKey = publicKey
|
if (result.query.isEmpty()) {
|
||||||
val contactAndGroupList = result.contacts.map { GlobalSearchAdapter.Model.Contact(it) } +
|
val hasNames = result.contacts.filter { it.nickname != null || it.name != null }
|
||||||
|
.groupByNotNull { (it.nickname?.firstOrNull() ?: it.name?.firstOrNull())?.uppercase() }
|
||||||
|
.toSortedMap(compareBy { it })
|
||||||
|
.flatMap { (key, contacts) -> listOf(GlobalSearchAdapter.Model.SubHeader(key)) + contacts.map(GlobalSearchAdapter.Model::Contact) }
|
||||||
|
|
||||||
|
val noNames = result.contacts.filter { it.nickname == null && it.name == null }
|
||||||
|
.sortedBy { it.sessionID }
|
||||||
|
.map { GlobalSearchAdapter.Model.Contact(it) }
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
?.let {
|
||||||
|
buildList {
|
||||||
|
add(GlobalSearchAdapter.Model.Header("Unknown"))
|
||||||
|
addAll(it)
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
buildList {
|
||||||
|
add(GlobalSearchAdapter.Model.Header("Contacts"))
|
||||||
|
add(GlobalSearchAdapter.Model.SavedMessages(publicKey))
|
||||||
|
addAll(hasNames)
|
||||||
|
addAll(noNames)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val currentUserPublicKey = publicKey
|
||||||
|
val contactAndGroupList = result.contacts.map { GlobalSearchAdapter.Model.Contact(it) } +
|
||||||
result.threads.map { GlobalSearchAdapter.Model.GroupConversation(it) }
|
result.threads.map { GlobalSearchAdapter.Model.GroupConversation(it) }
|
||||||
|
|
||||||
val contactResults = contactAndGroupList.toMutableList()
|
val contactResults = contactAndGroupList.toMutableList()
|
||||||
|
|
||||||
if (contactResults.isEmpty()) {
|
if (contactResults.isEmpty()) {
|
||||||
contactResults.add(GlobalSearchAdapter.Model.SavedMessages(currentUserPublicKey))
|
contactResults.add(GlobalSearchAdapter.Model.SavedMessages(currentUserPublicKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
val userIndex = contactResults.indexOfFirst { it is GlobalSearchAdapter.Model.Contact && it.contact.sessionID == currentUserPublicKey }
|
val userIndex = contactResults.indexOfFirst { it is GlobalSearchAdapter.Model.Contact && it.contact.sessionID == currentUserPublicKey }
|
||||||
if (userIndex >= 0) {
|
if (userIndex >= 0) {
|
||||||
contactResults[userIndex] = GlobalSearchAdapter.Model.SavedMessages(currentUserPublicKey)
|
contactResults[userIndex] = GlobalSearchAdapter.Model.SavedMessages(currentUserPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contactResults.isNotEmpty()) {
|
if (contactResults.isNotEmpty()) {
|
||||||
contactResults.add(0, GlobalSearchAdapter.Model.Header(R.string.global_search_contacts_groups))
|
contactResults.add(0, GlobalSearchAdapter.Model.Header(R.string.global_search_contacts_groups))
|
||||||
}
|
}
|
||||||
|
|
||||||
val unreadThreadMap = result.messages
|
val unreadThreadMap = result.messages
|
||||||
.map { it.threadId }.toSet()
|
.map { it.threadId }.toSet()
|
||||||
.associateWith { mmsSmsDatabase.getUnreadCount(it) }
|
.associateWith { mmsSmsDatabase.getUnreadCount(it) }
|
||||||
|
|
||||||
val messageResults: MutableList<GlobalSearchAdapter.Model> = result.messages
|
val messageResults: MutableList<GlobalSearchAdapter.Model> = result.messages
|
||||||
.map { messageResult ->
|
.map { GlobalSearchAdapter.Model.Message(it, unreadThreadMap[it.threadId] ?: 0) }
|
||||||
GlobalSearchAdapter.Model.Message(
|
.toMutableList()
|
||||||
messageResult,
|
|
||||||
unreadThreadMap[messageResult.threadId] ?: 0
|
|
||||||
)
|
|
||||||
}.toMutableList()
|
|
||||||
|
|
||||||
if (messageResults.isNotEmpty()) {
|
if (messageResults.isNotEmpty()) {
|
||||||
messageResults.add(0, GlobalSearchAdapter.Model.Header(R.string.global_search_messages))
|
messageResults.add(0, GlobalSearchAdapter.Model.Header(R.string.global_search_messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
val newData = contactResults + messageResults
|
contactResults + messageResults
|
||||||
globalSearchAdapter.setNewData(result.query, newData)
|
}.let { globalSearchAdapter.setNewData(result.query, it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -586,7 +609,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
else if (thread.recipient.isCommunityRecipient) {
|
else if (thread.recipient.isCommunityRecipient) {
|
||||||
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) ?: return@onCopyConversationId Unit
|
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient)
|
||||||
val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit
|
val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit
|
||||||
|
|
||||||
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
|
val clip = ClipData.newPlainText("Community URL", openGroup.joinURL)
|
||||||
@ -785,3 +808,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class NameIdContact(val name: String?, val id: String, val contact: Contact): Comparable<NameIdContact> {
|
||||||
|
override fun compareTo(other: NameIdContact): Int = comparator.compare(this, other)
|
||||||
|
companion object {
|
||||||
|
val comparator = compareBy<NameIdContact>({ it.name }, { it.id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,10 +9,11 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
|
import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
|
||||||
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
|
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
|
||||||
|
import network.loki.messenger.databinding.ViewGlobalSearchSubheaderBinding
|
||||||
import org.session.libsession.utilities.GroupRecord
|
import org.session.libsession.utilities.GroupRecord
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
|
||||||
import org.thoughtcrime.securesms.search.model.MessageResult
|
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
import java.security.InvalidParameterException
|
import java.security.InvalidParameterException
|
||||||
import org.session.libsession.messaging.contacts.Contact as ContactModel
|
import org.session.libsession.messaging.contacts.Contact as ContactModel
|
||||||
|
|
||||||
@ -20,7 +21,8 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val HEADER_VIEW_TYPE = 0
|
const val HEADER_VIEW_TYPE = 0
|
||||||
const val CONTENT_VIEW_TYPE = 1
|
const val SUB_HEADER_VIEW_TYPE = 1
|
||||||
|
const val CONTENT_VIEW_TYPE = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
private var data: List<Model> = listOf()
|
private var data: List<Model> = listOf()
|
||||||
@ -34,21 +36,26 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int =
|
override fun getItemViewType(position: Int): Int =
|
||||||
if (data[position] is Model.Header) HEADER_VIEW_TYPE else CONTENT_VIEW_TYPE
|
when(data[position]) {
|
||||||
|
is Model.Header -> HEADER_VIEW_TYPE
|
||||||
|
is Model.SubHeader -> SUB_HEADER_VIEW_TYPE
|
||||||
|
else -> CONTENT_VIEW_TYPE
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = data.size
|
override fun getItemCount(): Int = data.size
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
||||||
if (viewType == HEADER_VIEW_TYPE) {
|
when (viewType) {
|
||||||
HeaderView(
|
HEADER_VIEW_TYPE -> HeaderView(
|
||||||
LayoutInflater.from(parent.context)
|
LayoutInflater.from(parent.context).inflate(R.layout.view_global_search_header, parent, false)
|
||||||
.inflate(R.layout.view_global_search_header, parent, false)
|
)
|
||||||
|
SUB_HEADER_VIEW_TYPE -> SubHeaderView(
|
||||||
|
LayoutInflater.from(parent.context).inflate(R.layout.view_global_search_subheader, parent, false)
|
||||||
|
)
|
||||||
|
else -> ContentView(
|
||||||
|
LayoutInflater.from(parent.context).inflate(R.layout.view_global_search_result, parent, false),
|
||||||
|
modelCallback
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
ContentView(
|
|
||||||
LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.view_global_search_result, parent, false)
|
|
||||||
, modelCallback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
override fun onBindViewHolder(
|
||||||
@ -61,10 +68,10 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
holder.bindPayload(newUpdateQuery, data[position])
|
holder.bindPayload(newUpdateQuery, data[position])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (holder is HeaderView) {
|
when (holder) {
|
||||||
holder.bind(data[position] as Model.Header)
|
is HeaderView -> holder.bind(data[position] as Model.Header)
|
||||||
} else if (holder is ContentView) {
|
is SubHeaderView -> holder.bind(data[position] as Model.SubHeader)
|
||||||
holder.bind(query.orEmpty(), data[position])
|
is ContentView -> holder.bind(query.orEmpty(), data[position])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +84,16 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
val binding = ViewGlobalSearchHeaderBinding.bind(view)
|
val binding = ViewGlobalSearchHeaderBinding.bind(view)
|
||||||
|
|
||||||
fun bind(header: Model.Header) {
|
fun bind(header: Model.Header) {
|
||||||
binding.searchHeader.setText(header.title)
|
binding.searchHeader.setText(header.title.string(binding.root.context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SubHeaderView(view: View) : RecyclerView.ViewHolder(view) {
|
||||||
|
|
||||||
|
val binding = ViewGlobalSearchSubheaderBinding.bind(view)
|
||||||
|
|
||||||
|
fun bind(header: Model.SubHeader) {
|
||||||
|
binding.searchHeader.text = header.title.string(binding.root.context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +118,7 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
is Model.Contact -> bindModel(query, model)
|
is Model.Contact -> bindModel(query, model)
|
||||||
is Model.Message -> bindModel(query, model)
|
is Model.Message -> bindModel(query, model)
|
||||||
is Model.SavedMessages -> bindModel(model)
|
is Model.SavedMessages -> bindModel(model)
|
||||||
is Model.Header -> throw InvalidParameterException("Can't display Model.Header as ContentView")
|
else -> throw InvalidParameterException("Can't display as ContentView")
|
||||||
}
|
}
|
||||||
binding.root.setOnClickListener { modelCallback(model) }
|
binding.root.setOnClickListener { modelCallback(model) }
|
||||||
}
|
}
|
||||||
@ -116,11 +132,17 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
|
|||||||
)
|
)
|
||||||
|
|
||||||
sealed class Model {
|
sealed class Model {
|
||||||
data class Header(@StringRes val title: Int) : Model()
|
data class Header(val title: GetString): Model() {
|
||||||
|
constructor(@StringRes title: Int): this(GetString(title))
|
||||||
|
constructor(title: String): this(GetString(title))
|
||||||
|
}
|
||||||
|
data class SubHeader(val title: GetString): Model() {
|
||||||
|
constructor(@StringRes title: Int): this(GetString(title))
|
||||||
|
constructor(title: String): this(GetString(title))
|
||||||
|
}
|
||||||
data class SavedMessages(val currentUserPublicKey: String): Model()
|
data class SavedMessages(val currentUserPublicKey: String): Model()
|
||||||
data class Contact(val contact: ContactModel) : Model()
|
data class Contact(val contact: ContactModel): Model()
|
||||||
data class GroupConversation(val groupRecord: GroupRecord) : Model()
|
data class GroupConversation(val groupRecord: GroupRecord): Model()
|
||||||
data class Message(val messageResult: MessageResult, val unread: Int) : Model()
|
data class Message(val messageResult: MessageResult, val unread: Int): Model()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupCon
|
|||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Header
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Header
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message
|
||||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages
|
||||||
|
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SubHeader
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
import org.thoughtcrime.securesms.util.SearchUtil
|
import org.thoughtcrime.securesms.util.SearchUtil
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@ -78,6 +79,7 @@ fun ContentView.bindQuery(query: String, model: GlobalSearchAdapter.Model) {
|
|||||||
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
binding.searchResultSubtitle.text = getHighlight(query, membersString)
|
||||||
}
|
}
|
||||||
is Header, // do nothing for header
|
is Header, // do nothing for header
|
||||||
|
is SubHeader, // do nothing for subheader
|
||||||
is SavedMessages -> Unit // do nothing for saved messages (displays note to self)
|
is SavedMessages -> Unit // do nothing for saved messages (displays note to self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,33 +2,25 @@ package org.thoughtcrime.securesms.home.search
|
|||||||
|
|
||||||
import org.session.libsession.messaging.contacts.Contact
|
import org.session.libsession.messaging.contacts.Contact
|
||||||
import org.session.libsession.utilities.GroupRecord
|
import org.session.libsession.utilities.GroupRecord
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
|
||||||
import org.thoughtcrime.securesms.search.model.MessageResult
|
import org.thoughtcrime.securesms.search.model.MessageResult
|
||||||
import org.thoughtcrime.securesms.search.model.SearchResult
|
import org.thoughtcrime.securesms.search.model.SearchResult
|
||||||
|
|
||||||
data class GlobalSearchResult(
|
data class GlobalSearchResult(
|
||||||
val query: String,
|
val query: String,
|
||||||
val contacts: List<Contact>,
|
val contacts: List<Contact> = emptyList(),
|
||||||
val threads: List<GroupRecord>,
|
val threads: List<GroupRecord> = emptyList(),
|
||||||
val messages: List<MessageResult>
|
val messages: List<MessageResult> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val isEmpty: Boolean
|
val isEmpty: Boolean
|
||||||
get() = contacts.isEmpty() && threads.isEmpty() && messages.isEmpty()
|
get() = contacts.isEmpty() && threads.isEmpty() && messages.isEmpty()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
val EMPTY = GlobalSearchResult("")
|
||||||
val EMPTY = GlobalSearchResult("", emptyList(), emptyList(), emptyList())
|
|
||||||
const val SEARCH_LIMIT = 5
|
|
||||||
|
|
||||||
fun from(searchResult: SearchResult): GlobalSearchResult {
|
|
||||||
val query = searchResult.query
|
|
||||||
val contactList = searchResult.contacts.toList()
|
|
||||||
val threads = searchResult.conversations.toList()
|
|
||||||
val messages = searchResult.messages.toList()
|
|
||||||
searchResult.close()
|
|
||||||
return GlobalSearchResult(query, contactList, threads, messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun SearchResult.toGlobalSearchResult(): GlobalSearchResult = try {
|
||||||
|
GlobalSearchResult(query, contacts.toList(), conversations.toList(), messages.toList())
|
||||||
|
} finally {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
@ -13,14 +13,18 @@ import kotlinx.coroutines.flow.launchIn
|
|||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
|
import org.session.libsession.utilities.truncateIdForDisplay
|
||||||
import org.session.libsignal.utilities.SettableFuture
|
import org.session.libsignal.utilities.SettableFuture
|
||||||
|
import org.thoughtcrime.securesms.database.SessionContactDatabase
|
||||||
import org.thoughtcrime.securesms.search.SearchRepository
|
import org.thoughtcrime.securesms.search.SearchRepository
|
||||||
import org.thoughtcrime.securesms.search.model.SearchResult
|
import org.thoughtcrime.securesms.search.model.SearchResult
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class GlobalSearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel() {
|
class GlobalSearchViewModel @Inject constructor(
|
||||||
|
private val searchRepository: SearchRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
private val executor = viewModelScope + SupervisorJob()
|
private val executor = viewModelScope + SupervisorJob()
|
||||||
|
|
||||||
@ -36,32 +40,27 @@ class GlobalSearchViewModel @Inject constructor(private val searchRepository: Se
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
//
|
_queryText.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
_queryText
|
.mapLatest { query ->
|
||||||
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
// User input delay in case we get a new query within a few hundred ms this
|
||||||
.mapLatest { query ->
|
// coroutine will be cancelled and the expensive query will not be run.
|
||||||
// Early exit on empty search query
|
delay(300)
|
||||||
if (query.trim().isEmpty()) {
|
|
||||||
SearchResult.EMPTY
|
|
||||||
} else {
|
|
||||||
// User input delay in case we get a new query within a few hundred ms this
|
|
||||||
// coroutine will be cancelled and the expensive query will not be run.
|
|
||||||
delay(300)
|
|
||||||
|
|
||||||
val settableFuture = SettableFuture<SearchResult>()
|
if (query.trim().isEmpty()) {
|
||||||
searchRepository.query(query.toString(), settableFuture::set)
|
GlobalSearchResult(query.toString(), searchRepository.queryContacts("05").first.toList())
|
||||||
try {
|
} else {
|
||||||
// search repository doesn't play nicely with suspend functions (yet)
|
val settableFuture = SettableFuture<SearchResult>()
|
||||||
settableFuture.get(10_000, TimeUnit.MILLISECONDS)
|
searchRepository.query(query.toString(), settableFuture::set)
|
||||||
} catch (e: Exception) {
|
try {
|
||||||
SearchResult.EMPTY
|
// search repository doesn't play nicely with suspend functions (yet)
|
||||||
}
|
settableFuture.get(10_000, TimeUnit.MILLISECONDS).toGlobalSearchResult()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
GlobalSearchResult(query.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onEach { result ->
|
}.onEach { result ->
|
||||||
// update the latest _result value
|
// update the latest _result value
|
||||||
_result.value = GlobalSearchResult.from(result)
|
_result.value = result
|
||||||
}
|
}.launchIn(executor)
|
||||||
.launchIn(executor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -72,10 +72,6 @@ public class SearchRepository {
|
|||||||
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
|
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
|
||||||
// If the sanitized search is empty then abort without search
|
// If the sanitized search is empty then abort without search
|
||||||
String cleanQuery = sanitizeQuery(query).trim();
|
String cleanQuery = sanitizeQuery(query).trim();
|
||||||
if (cleanQuery.isEmpty()) {
|
|
||||||
callback.onResult(SearchResult.EMPTY);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
Stopwatch timer = new Stopwatch("FtsQuery");
|
Stopwatch timer = new Stopwatch("FtsQuery");
|
||||||
@ -110,7 +106,7 @@ public class SearchRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Pair<CursorList<Contact>, List<String>> queryContacts(String query) {
|
public Pair<CursorList<Contact>, List<String>> queryContacts(String query) {
|
||||||
Cursor contacts = contactDatabase.queryContactsByName(query);
|
Cursor contacts = contactDatabase.queryContactsByName(query);
|
||||||
List<Address> contactList = new ArrayList<>();
|
List<Address> contactList = new ArrayList<>();
|
||||||
List<String> contactStrings = new ArrayList<>();
|
List<String> contactStrings = new ArrayList<>();
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="?android:colorControlHighlight">
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<color android:color="?colorPrimary" />
|
||||||
|
</item>
|
||||||
|
</ripple>
|
@ -2,7 +2,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:background="@drawable/conversation_view_background"
|
android:background="@drawable/conversation_view_search__background"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
15
app/src/main/res/layout/view_global_search_subheader.xml
Normal file
15
app/src/main/res/layout/view_global_search_subheader.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?colorPrimary"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<TextView
|
||||||
|
tools:text="@string/global_search_messages"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:paddingVertical="@dimen/medium_spacing"
|
||||||
|
android:paddingHorizontal="@dimen/very_large_spacing"
|
||||||
|
android:id="@+id/search_header"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="match_parent"/>
|
||||||
|
</LinearLayout>
|
@ -36,21 +36,12 @@ class Contact(val sessionID: String) {
|
|||||||
/**
|
/**
|
||||||
* The name to display in the UI. For local use only.
|
* The name to display in the UI. For local use only.
|
||||||
*/
|
*/
|
||||||
fun displayName(context: ContactContext): String? {
|
fun displayName(context: ContactContext): String? = nickname ?: when (context) {
|
||||||
nickname?.let { return it }
|
ContactContext.REGULAR -> name
|
||||||
return when (context) {
|
// In open groups, where it's more likely that multiple users have the same name,
|
||||||
ContactContext.REGULAR -> name
|
// we display a bit of the Session ID after a user's display name for added context.
|
||||||
ContactContext.OPEN_GROUP -> {
|
ContactContext.OPEN_GROUP -> name?.let { "$it (${sessionID.take(4)}...${sessionID.takeLast(4)})" }
|
||||||
// In open groups, where it's more likely that multiple users have the same name,
|
|
||||||
// we display a bit of the Session ID after a user's display name for added context.
|
|
||||||
name?.let {
|
|
||||||
return "$name (${sessionID.take(4)}...${sessionID.takeLast(4)})"
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// endregion
|
|
||||||
|
|
||||||
enum class ContactContext {
|
enum class ContactContext {
|
||||||
REGULAR, OPEN_GROUP
|
REGULAR, OPEN_GROUP
|
||||||
@ -75,7 +66,6 @@ class Contact(val sessionID: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun contextForRecipient(recipient: Recipient): ContactContext {
|
fun contextForRecipient(recipient: Recipient): ContactContext {
|
||||||
return if (recipient.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
return if (recipient.isCommunityRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
|
||||||
}
|
}
|
||||||
|
@ -383,3 +383,9 @@ fun <T, K: Any, V: Any> Iterable<T>.associateByNotNull(
|
|||||||
this[key] = value
|
this[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <T, K> Iterable<T>.groupByNotNull(keySelector: (T) -> K?): Map<K, List<T>> {
|
||||||
|
val map = mutableMapOf<K, MutableList<T>>()
|
||||||
|
forEach { e -> keySelector(e)?.let { k -> map.getOrPut(k) { mutableListOf() }.add(e) } }
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user