MessageSearchActivity.kt 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. /*
  2. * Nextcloud Talk - Android Client
  3. *
  4. * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham <ezhil56x.contact@gmail.com>
  5. * SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
  6. * SPDX-FileCopyrightText: 2022 Nextcloud GmbH
  7. * SPDX-License-Identifier: GPL-3.0-or-later
  8. */
  9. package com.nextcloud.talk.messagesearch
  10. import android.app.Activity
  11. import android.content.Intent
  12. import android.os.Bundle
  13. import android.text.TextUtils
  14. import android.view.Menu
  15. import android.view.MenuItem
  16. import android.view.View
  17. import androidx.activity.OnBackPressedCallback
  18. import androidx.appcompat.widget.SearchView
  19. import androidx.lifecycle.ViewModelProvider
  20. import autodagger.AutoInjector
  21. import com.google.android.material.snackbar.Snackbar
  22. import com.nextcloud.talk.R
  23. import com.nextcloud.talk.activities.BaseActivity
  24. import com.nextcloud.talk.adapters.items.LoadMoreResultsItem
  25. import com.nextcloud.talk.adapters.items.MessageResultItem
  26. import com.nextcloud.talk.application.NextcloudTalkApplication
  27. import com.nextcloud.talk.conversationlist.ConversationsListActivity
  28. import com.nextcloud.talk.data.user.model.User
  29. import com.nextcloud.talk.databinding.ActivityMessageSearchBinding
  30. import com.nextcloud.talk.utils.bundle.BundleKeys
  31. import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
  32. import com.nextcloud.talk.utils.rx.SearchViewObservable.Companion.observeSearchView
  33. import eu.davidea.flexibleadapter.FlexibleAdapter
  34. import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
  35. import eu.davidea.viewholders.FlexibleViewHolder
  36. import io.reactivex.Observable
  37. import io.reactivex.android.schedulers.AndroidSchedulers
  38. import io.reactivex.disposables.Disposable
  39. import io.reactivex.schedulers.Schedulers
  40. import java.util.concurrent.TimeUnit
  41. import javax.inject.Inject
  42. @AutoInjector(NextcloudTalkApplication::class)
  43. class MessageSearchActivity : BaseActivity() {
  44. @Inject
  45. lateinit var viewModelFactory: ViewModelProvider.Factory
  46. @Inject
  47. lateinit var userProvider: CurrentUserProviderNew
  48. private lateinit var binding: ActivityMessageSearchBinding
  49. private lateinit var searchView: SearchView
  50. private lateinit var user: User
  51. private lateinit var viewModel: MessageSearchViewModel
  52. private var searchViewDisposable: Disposable? = null
  53. private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
  54. private val onBackPressedCallback = object : OnBackPressedCallback(true) {
  55. override fun handleOnBackPressed() {
  56. setResult(Activity.RESULT_CANCELED)
  57. finish()
  58. }
  59. }
  60. override fun onCreate(savedInstanceState: Bundle?) {
  61. super.onCreate(savedInstanceState)
  62. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  63. binding = ActivityMessageSearchBinding.inflate(layoutInflater)
  64. setupActionBar()
  65. setContentView(binding.root)
  66. setupSystemColors()
  67. viewModel = ViewModelProvider(this, viewModelFactory)[MessageSearchViewModel::class.java]
  68. user = userProvider.currentUser.blockingGet()
  69. val roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!!
  70. viewModel.initialize(roomToken)
  71. setupStateObserver()
  72. binding.swipeRefreshLayout.setOnRefreshListener {
  73. viewModel.refresh(searchView.query?.toString())
  74. }
  75. onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
  76. }
  77. private fun setupActionBar() {
  78. setSupportActionBar(binding.messageSearchToolbar)
  79. supportActionBar?.setDisplayHomeAsUpEnabled(true)
  80. val conversationName = intent.getStringExtra(BundleKeys.KEY_CONVERSATION_NAME)
  81. supportActionBar?.title = conversationName
  82. viewThemeUtils.material.themeToolbar(binding.messageSearchToolbar)
  83. }
  84. private fun setupStateObserver() {
  85. viewModel.state.observe(this) { state ->
  86. when (state) {
  87. MessageSearchViewModel.InitialState -> showInitial()
  88. MessageSearchViewModel.EmptyState -> showEmpty()
  89. is MessageSearchViewModel.LoadedState -> showLoaded(state)
  90. MessageSearchViewModel.LoadingState -> showLoading()
  91. MessageSearchViewModel.ErrorState -> showError()
  92. is MessageSearchViewModel.FinishedState -> onFinish()
  93. }
  94. }
  95. }
  96. private fun showError() {
  97. displayLoading(false)
  98. Snackbar.make(binding.root, "Error while searching", Snackbar.LENGTH_SHORT).show()
  99. }
  100. private fun showLoading() {
  101. displayLoading(true)
  102. }
  103. private fun displayLoading(loading: Boolean) {
  104. binding.swipeRefreshLayout.isRefreshing = loading
  105. }
  106. private fun showLoaded(state: MessageSearchViewModel.LoadedState) {
  107. displayLoading(false)
  108. binding.emptyContainer.emptyListView.visibility = View.GONE
  109. binding.messageSearchRecycler.visibility = View.VISIBLE
  110. setAdapterItems(state)
  111. }
  112. private fun setAdapterItems(state: MessageSearchViewModel.LoadedState) {
  113. val loadMoreItems = if (state.hasMore) {
  114. listOf(LoadMoreResultsItem)
  115. } else {
  116. emptyList()
  117. }
  118. val newItems =
  119. state.results.map { MessageResultItem(this, user, it, false, viewThemeUtils) } + loadMoreItems
  120. if (adapter != null) {
  121. adapter!!.updateDataSet(newItems)
  122. } else {
  123. createAdapter(newItems)
  124. }
  125. }
  126. private fun createAdapter(items: List<AbstractFlexibleItem<out FlexibleViewHolder>>) {
  127. adapter = FlexibleAdapter(items)
  128. binding.messageSearchRecycler.adapter = adapter
  129. adapter!!.addListener(object : FlexibleAdapter.OnItemClickListener {
  130. override fun onItemClick(view: View?, position: Int): Boolean {
  131. val item = adapter!!.getItem(position)
  132. when (item?.itemViewType) {
  133. LoadMoreResultsItem.VIEW_TYPE -> {
  134. viewModel.loadMore()
  135. }
  136. MessageResultItem.VIEW_TYPE -> {
  137. val messageItem = item as MessageResultItem
  138. viewModel.selectMessage(messageItem.messageEntry)
  139. }
  140. }
  141. return false
  142. }
  143. })
  144. }
  145. private fun onFinish() {
  146. val state = viewModel.state.value
  147. if (state is MessageSearchViewModel.FinishedState) {
  148. val resultIntent = Intent().apply {
  149. putExtra(RESULT_KEY_MESSAGE_ID, state.selectedMessageId)
  150. }
  151. setResult(Activity.RESULT_OK, resultIntent)
  152. finish()
  153. }
  154. }
  155. private fun showInitial() {
  156. displayLoading(false)
  157. binding.messageSearchRecycler.visibility = View.GONE
  158. binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_typing)
  159. binding.emptyContainer.emptyListView.visibility = View.VISIBLE
  160. }
  161. private fun showEmpty() {
  162. displayLoading(false)
  163. binding.messageSearchRecycler.visibility = View.GONE
  164. binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_empty)
  165. binding.emptyContainer.emptyListView.visibility = View.VISIBLE
  166. }
  167. override fun onCreateOptionsMenu(menu: Menu): Boolean {
  168. menuInflater.inflate(R.menu.menu_search, menu)
  169. return true
  170. }
  171. override fun onPrepareOptionsMenu(menu: Menu): Boolean {
  172. val menuItem = menu.findItem(R.id.action_search)
  173. searchView = menuItem.actionView as SearchView
  174. setupSearchView()
  175. menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
  176. override fun onMenuItemActionExpand(item: MenuItem): Boolean {
  177. searchView.requestFocus()
  178. return true
  179. }
  180. override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
  181. onBackPressedDispatcher.onBackPressed()
  182. return false
  183. }
  184. })
  185. menuItem.expandActionView()
  186. return true
  187. }
  188. private fun setupSearchView() {
  189. searchView.queryHint = getString(R.string.message_search_hint)
  190. searchViewDisposable = observeSearchView(searchView)
  191. .debounce { query ->
  192. when {
  193. TextUtils.isEmpty(query) -> Observable.empty()
  194. else -> Observable.timer(
  195. ConversationsListActivity.SEARCH_DEBOUNCE_INTERVAL_MS.toLong(),
  196. TimeUnit.MILLISECONDS
  197. )
  198. }
  199. }
  200. .distinctUntilChanged()
  201. .subscribeOn(Schedulers.io())
  202. .observeOn(AndroidSchedulers.mainThread())
  203. .subscribe { newText -> viewModel.onQueryTextChange(newText) }
  204. }
  205. override fun onOptionsItemSelected(item: MenuItem): Boolean {
  206. return when (item.itemId) {
  207. android.R.id.home -> {
  208. onBackPressedDispatcher.onBackPressed()
  209. true
  210. }
  211. else -> super.onOptionsItemSelected(item)
  212. }
  213. }
  214. override fun onDestroy() {
  215. super.onDestroy()
  216. searchViewDisposable?.dispose()
  217. }
  218. companion object {
  219. const val RESULT_KEY_MESSAGE_ID = "MessageSearchActivity.result.message"
  220. }
  221. }