diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 30fefabc4c..89953bcc04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt index 7dc18130d7..3e81a502e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt @@ -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 + } + }) } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt new file mode 100644 index 0000000000..93d24ff019 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchBottomBar.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt new file mode 100644 index 0000000000..f1d7c8a610 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/search/SearchViewModel.kt @@ -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 + 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 + 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 + 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 + 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 -> + runOnMain { + if (searchOpen && query == activeQuery) { + result.setValue(SearchResult(messages, 0)) + } else { + messages.close() + } + } + } + } + } + + class SearchResult(private val results: CursorList, val position: Int) : Closeable { + + fun getResults(): List { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml index 66d71f5cce..079c932e99 100644 --- a/app/src/main/res/layout/activity_conversation_v2.xml +++ b/app/src/main/res/layout/activity_conversation_v2.xml @@ -34,6 +34,13 @@ android:layout_height="wrap_content" android:layout_alignParentBottom="true" /> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file