MessageSearchActivity.kt 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Álvaro Brey
  5. * Copyright (C) 2022 Álvaro Brey
  6. * Copyright (C) 2022 Nextcloud GmbH
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation, either version 3 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. */
  21. package com.nextcloud.talk.messagesearch
  22. import android.app.Activity
  23. import android.content.Intent
  24. import android.os.Bundle
  25. import android.text.TextUtils
  26. import android.view.Menu
  27. import android.view.MenuItem
  28. import android.view.View
  29. import android.widget.Toast
  30. import androidx.appcompat.widget.SearchView
  31. import androidx.core.content.res.ResourcesCompat
  32. import androidx.lifecycle.ViewModelProvider
  33. import autodagger.AutoInjector
  34. import com.nextcloud.talk.R
  35. import com.nextcloud.talk.activities.BaseActivity
  36. import com.nextcloud.talk.adapters.items.LoadMoreResultsItem
  37. import com.nextcloud.talk.adapters.items.MessageResultItem
  38. import com.nextcloud.talk.application.NextcloudTalkApplication
  39. import com.nextcloud.talk.controllers.ConversationsListController
  40. import com.nextcloud.talk.data.user.model.User
  41. import com.nextcloud.talk.databinding.ActivityMessageSearchBinding
  42. import com.nextcloud.talk.utils.DisplayUtils
  43. import com.nextcloud.talk.utils.bundle.BundleKeys
  44. import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
  45. import com.nextcloud.talk.utils.rx.SearchViewObservable.Companion.observeSearchView
  46. import eu.davidea.flexibleadapter.FlexibleAdapter
  47. import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
  48. import eu.davidea.viewholders.FlexibleViewHolder
  49. import io.reactivex.Observable
  50. import io.reactivex.android.schedulers.AndroidSchedulers
  51. import io.reactivex.disposables.Disposable
  52. import io.reactivex.schedulers.Schedulers
  53. import java.util.concurrent.TimeUnit
  54. import javax.inject.Inject
  55. @AutoInjector(NextcloudTalkApplication::class)
  56. class MessageSearchActivity : BaseActivity() {
  57. @Inject
  58. lateinit var viewModelFactory: ViewModelProvider.Factory
  59. @Inject
  60. lateinit var userProvider: CurrentUserProviderNew
  61. private lateinit var binding: ActivityMessageSearchBinding
  62. private lateinit var searchView: SearchView
  63. private lateinit var user: User
  64. private lateinit var viewModel: MessageSearchViewModel
  65. private var searchViewDisposable: Disposable? = null
  66. private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = null
  67. override fun onCreate(savedInstanceState: Bundle?) {
  68. super.onCreate(savedInstanceState)
  69. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  70. binding = ActivityMessageSearchBinding.inflate(layoutInflater)
  71. setupActionBar()
  72. setupSystemColors()
  73. setContentView(binding.root)
  74. viewModel = ViewModelProvider(this, viewModelFactory)[MessageSearchViewModel::class.java]
  75. user = userProvider.currentUser.blockingGet()
  76. val roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!!
  77. viewModel.initialize(roomToken)
  78. setupStateObserver()
  79. binding.swipeRefreshLayout.setOnRefreshListener {
  80. viewModel.refresh(searchView.query?.toString())
  81. }
  82. }
  83. private fun setupActionBar() {
  84. setSupportActionBar(binding.messageSearchToolbar)
  85. supportActionBar?.setDisplayHomeAsUpEnabled(true)
  86. val conversationName = intent.getStringExtra(BundleKeys.KEY_CONVERSATION_NAME)
  87. supportActionBar?.title = conversationName
  88. }
  89. private fun setupSystemColors() {
  90. DisplayUtils.applyColorToStatusBar(
  91. this,
  92. ResourcesCompat.getColor(
  93. resources,
  94. R.color.appbar,
  95. null
  96. )
  97. )
  98. DisplayUtils.applyColorToNavigationBar(
  99. this.window,
  100. ResourcesCompat.getColor(resources, R.color.bg_default, null)
  101. )
  102. }
  103. private fun setupStateObserver() {
  104. viewModel.state.observe(this) { state ->
  105. when (state) {
  106. MessageSearchViewModel.InitialState -> showInitial()
  107. MessageSearchViewModel.EmptyState -> showEmpty()
  108. is MessageSearchViewModel.LoadedState -> showLoaded(state)
  109. MessageSearchViewModel.LoadingState -> showLoading()
  110. MessageSearchViewModel.ErrorState -> showError()
  111. is MessageSearchViewModel.FinishedState -> onFinish()
  112. }
  113. }
  114. }
  115. private fun showError() {
  116. displayLoading(false)
  117. Toast.makeText(this, "Error while searching", Toast.LENGTH_SHORT).show()
  118. }
  119. private fun showLoading() {
  120. displayLoading(true)
  121. }
  122. private fun displayLoading(loading: Boolean) {
  123. binding.swipeRefreshLayout.isRefreshing = loading
  124. }
  125. private fun showLoaded(state: MessageSearchViewModel.LoadedState) {
  126. displayLoading(false)
  127. binding.emptyContainer.emptyListView.visibility = View.GONE
  128. binding.messageSearchRecycler.visibility = View.VISIBLE
  129. setAdapterItems(state)
  130. }
  131. private fun setAdapterItems(state: MessageSearchViewModel.LoadedState) {
  132. val loadMoreItems = if (state.hasMore) {
  133. listOf(LoadMoreResultsItem)
  134. } else {
  135. emptyList()
  136. }
  137. val newItems =
  138. state.results.map { MessageResultItem(this, user, it, false, viewThemeUtils) } + loadMoreItems
  139. if (adapter != null) {
  140. adapter!!.updateDataSet(newItems)
  141. } else {
  142. createAdapter(newItems)
  143. }
  144. }
  145. private fun createAdapter(items: List<AbstractFlexibleItem<out FlexibleViewHolder>>) {
  146. adapter = FlexibleAdapter(items)
  147. binding.messageSearchRecycler.adapter = adapter
  148. adapter!!.addListener(object : FlexibleAdapter.OnItemClickListener {
  149. override fun onItemClick(view: View?, position: Int): Boolean {
  150. val item = adapter!!.getItem(position)
  151. when (item?.itemViewType) {
  152. LoadMoreResultsItem.VIEW_TYPE -> {
  153. viewModel.loadMore()
  154. }
  155. MessageResultItem.VIEW_TYPE -> {
  156. val messageItem = item as MessageResultItem
  157. viewModel.selectMessage(messageItem.messageEntry)
  158. }
  159. }
  160. return false
  161. }
  162. })
  163. }
  164. private fun onFinish() {
  165. val state = viewModel.state.value
  166. if (state is MessageSearchViewModel.FinishedState) {
  167. val resultIntent = Intent().apply {
  168. putExtra(RESULT_KEY_MESSAGE_ID, state.selectedMessageId)
  169. }
  170. setResult(Activity.RESULT_OK, resultIntent)
  171. finish()
  172. }
  173. }
  174. private fun showInitial() {
  175. displayLoading(false)
  176. binding.messageSearchRecycler.visibility = View.GONE
  177. binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_typing)
  178. binding.emptyContainer.emptyListView.visibility = View.VISIBLE
  179. }
  180. private fun showEmpty() {
  181. displayLoading(false)
  182. binding.messageSearchRecycler.visibility = View.GONE
  183. binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_empty)
  184. binding.emptyContainer.emptyListView.visibility = View.VISIBLE
  185. }
  186. override fun onCreateOptionsMenu(menu: Menu?): Boolean {
  187. menuInflater.inflate(R.menu.menu_search, menu)
  188. return true
  189. }
  190. override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
  191. val menuItem = menu!!.findItem(R.id.action_search)
  192. searchView = menuItem.actionView as SearchView
  193. setupSearchView()
  194. menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
  195. override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
  196. searchView.requestFocus()
  197. return true
  198. }
  199. override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
  200. onBackPressed()
  201. return false
  202. }
  203. })
  204. menuItem.expandActionView()
  205. return true
  206. }
  207. private fun setupSearchView() {
  208. searchView.queryHint = getString(R.string.message_search_hint)
  209. searchViewDisposable = observeSearchView(searchView)
  210. .debounce { query ->
  211. when {
  212. TextUtils.isEmpty(query) -> Observable.empty()
  213. else -> Observable.timer(
  214. ConversationsListController.SEARCH_DEBOUNCE_INTERVAL_MS.toLong(),
  215. TimeUnit.MILLISECONDS
  216. )
  217. }
  218. }
  219. .distinctUntilChanged()
  220. .subscribeOn(Schedulers.io())
  221. .observeOn(AndroidSchedulers.mainThread())
  222. .subscribe { newText -> viewModel.onQueryTextChange(newText) }
  223. }
  224. override fun onBackPressed() {
  225. setResult(Activity.RESULT_CANCELED)
  226. finish()
  227. }
  228. override fun onOptionsItemSelected(item: MenuItem): Boolean {
  229. return when (item.itemId) {
  230. android.R.id.home -> {
  231. onBackPressed()
  232. true
  233. }
  234. else -> super.onOptionsItemSelected(item)
  235. }
  236. }
  237. override fun onDestroy() {
  238. super.onDestroy()
  239. searchViewDisposable?.dispose()
  240. }
  241. companion object {
  242. const val RESULT_KEY_MESSAGE_ID = "MessageSearchActivity.result.message"
  243. }
  244. }