add search bottom bar ui

This commit is contained in:
Ryan Zhao 2021-06-29 11:49:10 +10:00
parent 2b26876c4c
commit 61ff68b532
6 changed files with 317 additions and 4 deletions

View File

@ -39,7 +39,6 @@ import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.thoughtcrime.securesms.conversation.v2.dialogs.*
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate
@ -291,7 +290,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, thread, this) { onOptionsItemSelected(it) }
ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, thread, threadID, this) { onOptionsItemSelected(it) }
super.onPrepareOptionsMenu(menu)
return true
}
@ -750,4 +749,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
draftDB.insertDrafts(threadID, drafts)
}
// endregion
// region Search
fun onSearchQueryUpdated(query: String?) {
adapter.onSearchQueryUpdated(query)
}
// endregion
}

View File

@ -6,17 +6,26 @@ import android.graphics.PorterDuffColorFilter
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.lifecycle.ViewModelProvider
import kotlinx.android.synthetic.main.activity_conversation_v2.*
import kotlinx.android.synthetic.main.session_logo_action_bar_content.*
import network.loki.messenger.R
import org.session.libsession.utilities.ExpirationUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.components.ConversationSearchBottomBar
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
import org.thoughtcrime.securesms.loki.utilities.getColorWithID
object ConversationMenuHelper {
fun onPrepareOptionsMenu(menu: Menu, inflater: MenuInflater, thread: Recipient, context: Context, onOptionsItemSelected: (MenuItem) -> Unit) {
fun onPrepareOptionsMenu(menu: Menu, inflater: MenuInflater, thread: Recipient, threadId: Long, context: Context, onOptionsItemSelected: (MenuItem) -> Unit) {
// Prepare
menu.clear()
val isOpenGroup = thread.isOpenGroupRecipient
@ -61,6 +70,51 @@ object ConversationMenuHelper {
} else {
inflater.inflate(R.menu.menu_conversation_unmuted, menu)
}
// TODO: Implement search
// Search
val searchViewItem = menu.findItem(R.id.menu_search)
val searchView = searchViewItem.actionView as SearchView
val searchViewModel:SearchViewModel = ViewModelProvider(context as ConversationActivityV2).get(SearchViewModel::class.java)
val queryListener = object : OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
searchViewModel.onQueryUpdated(query, threadId)
context.searchBottomBar.showLoading()
context.onSearchQueryUpdated(query)
return true
}
override fun onQueryTextChange(query: String): Boolean {
searchViewModel.onQueryUpdated(query, threadId)
context.searchBottomBar.showLoading()
context.onSearchQueryUpdated(query)
return true
}
}
searchViewItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
searchView.setOnQueryTextListener(queryListener)
searchViewModel.onSearchOpened()
context.searchBottomBar.visibility = View.VISIBLE
context.searchBottomBar.setData(0, 0)
context.inputBar.visibility = View.GONE
context.supportActionBar?.setDisplayHomeAsUpEnabled(false)
for (i in 0 until menu.size()) {
if (menu.getItem(i) != searchViewItem) {
menu.getItem(i).isVisible = false
}
}
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
searchView.setOnQueryTextListener(null)
searchViewModel.onSearchClosed()
context.searchBottomBar.visibility = View.GONE
context.inputBar.visibility = View.VISIBLE
context.supportActionBar?.setDisplayHomeAsUpEnabled(true)
context.onSearchQueryUpdated(null)
context.invalidateOptionsMenu()
return true
}
})
}
}

View File

@ -0,0 +1,64 @@
package org.thoughtcrime.securesms.conversation.v2.search
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.constraintlayout.widget.ConstraintLayout
import kotlinx.android.synthetic.main.view_search_bottom_bar.view.*
import network.loki.messenger.R
class SearchBottomBar : LinearLayout {
private var eventListener: EventListener? = null
// region Lifecycle
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_search_bottom_bar, this)
}
fun setData(position: Int, count: Int) {
searchProgressWheel.visibility = GONE
searchUp.setOnClickListener { v: View? ->
if (eventListener != null) {
eventListener!!.onSearchMoveUpPressed()
}
}
searchDown.setOnClickListener { v: View? ->
if (eventListener != null) {
eventListener!!.onSearchMoveDownPressed()
}
}
if (count > 0) {
searchPosition.text = resources.getString(R.string.ConversationActivity_search_position, position + 1, count)
} else {
searchPosition.setText(R.string.ConversationActivity_no_results)
}
setViewEnabled(searchUp, position < count - 1)
setViewEnabled(searchDown, position > 0)
}
fun showLoading() {
searchProgressWheel.visibility = VISIBLE
}
private fun setViewEnabled(view: View, enabled: Boolean) {
view.isEnabled = enabled
view.alpha = if (enabled) 1f else 0.25f
}
fun setEventListener(eventListener: EventListener?) {
this.eventListener = eventListener
}
interface EventListener {
fun onSearchMoveUpPressed()
fun onSearchMoveDownPressed()
}
}

View File

@ -0,0 +1,114 @@
package org.thoughtcrime.securesms.conversation.v2.search
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import org.session.libsession.utilities.Debouncer
import org.session.libsession.utilities.Util.runOnMain
import org.session.libsession.utilities.concurrent.SignalExecutors
import org.thoughtcrime.securesms.contacts.ContactAccessor
import org.thoughtcrime.securesms.database.CursorList
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.search.SearchRepository
import org.thoughtcrime.securesms.search.model.MessageResult
import org.thoughtcrime.securesms.util.CloseableLiveData
import java.io.Closeable
class SearchViewModel(application: Application) : AndroidViewModel(application) {
private val searchRepository: SearchRepository
private val result: CloseableLiveData<SearchResult>
private val debouncer: Debouncer
private var firstSearch = false
private var searchOpen = false
private var activeQuery: String? = null
private var activeThreadId: Long = 0
val searchResults: LiveData<SearchResult>
get() = result
fun onQueryUpdated(query: String, threadId: Long) {
if (firstSearch && query.length < 2) {
result.postValue(SearchResult(CursorList.emptyList(), 0))
return
}
if (query == activeQuery) {
return
}
updateQuery(query, threadId)
}
fun onMissingResult() {
if (activeQuery != null) {
updateQuery(activeQuery!!, activeThreadId)
}
}
fun onMoveUp() {
debouncer.clear()
val messages = result.value!!.getResults() as CursorList<MessageResult?>
val position = Math.min(result.value!!.position + 1, messages.size - 1)
result.setValue(SearchResult(messages, position), false)
}
fun onMoveDown() {
debouncer.clear()
val messages = result.value!!.getResults() as CursorList<MessageResult?>
val position = Math.max(result.value!!.position - 1, 0)
result.setValue(SearchResult(messages, position), false)
}
fun onSearchOpened() {
searchOpen = true
firstSearch = true
}
fun onSearchClosed() {
searchOpen = false
debouncer.clear()
result.close()
}
override fun onCleared() {
super.onCleared()
result.close()
}
private fun updateQuery(query: String, threadId: Long) {
activeQuery = query
activeThreadId = threadId
debouncer.publish {
firstSearch = false
searchRepository.query(query, threadId) { messages: CursorList<MessageResult?> ->
runOnMain {
if (searchOpen && query == activeQuery) {
result.setValue(SearchResult(messages, 0))
} else {
messages.close()
}
}
}
}
}
class SearchResult(private val results: CursorList<MessageResult?>, val position: Int) : Closeable {
fun getResults(): List<MessageResult?> {
return results
}
override fun close() {
results.close()
}
}
init {
val context = application.applicationContext
result = CloseableLiveData()
debouncer = Debouncer(500)
searchRepository = SearchRepository(context,
DatabaseFactory.getSearchDatabase(context),
DatabaseFactory.getThreadDatabase(context),
ContactAccessor.getInstance(),
SignalExecutors.SERIAL)
}
}

View File

@ -34,6 +34,13 @@
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />
<org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
android:id="@+id/searchBottomBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:visibility="gone"/>
<FrameLayout
android:id="@+id/additionalContentContainer"
android:layout_width="match_parent"

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/searchBottomBarConstraintLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/input_bar_height"
android:background="@color/compose_view_background"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/separator" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical">
<ImageView
android:id="@+id/searchUp"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:padding="4dp"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_keyboard_arrow_up_24"
android:tint="@color/accent"
tools:ignore="UseAppTint" />
<ImageView
android:id="@+id/searchDown"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="4dp"
android:layout_gravity="center_vertical"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_keyboard_arrow_down_24"
android:tint="@color/accent"
tools:ignore="UseAppTint" />
</LinearLayout>
<TextView
android:id="@+id/searchPosition"
style="@style/Signal.Text.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="37 of 73" />
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.DoubleBounce"
android:id="@+id/searchProgressWheel"
android:layout_width="match_parent"
android:layout_height="0dp"
android:padding="8dp"
android:background="@color/compose_view_background"
android:visibility="gone"/>
</RelativeLayout>
</LinearLayout>