/* * Nextcloud Talk application * * @author Álvaro Brey * Copyright (C) 2022 Álvaro Brey * Copyright (C) 2022 Nextcloud GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.nextcloud.talk.messagesearch import android.app.Activity import android.content.Intent import android.os.Bundle import android.text.TextUtils import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.Toast import androidx.appcompat.widget.SearchView import androidx.core.content.res.ResourcesCompat import androidx.lifecycle.ViewModelProvider import autodagger.AutoInjector import com.nextcloud.talk.R import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.adapters.items.LoadMoreResultsItem import com.nextcloud.talk.adapters.items.MessageResultItem import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.controllers.ConversationsListController import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.ActivityMessageSearchBinding import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.rx.SearchViewObservable.Companion.observeSearchView import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.viewholders.FlexibleViewHolder import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers import java.util.concurrent.TimeUnit import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class MessageSearchActivity : BaseActivity() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @Inject lateinit var userProvider: CurrentUserProviderNew private lateinit var binding: ActivityMessageSearchBinding private lateinit var searchView: SearchView private lateinit var user: User private lateinit var viewModel: MessageSearchViewModel private var searchViewDisposable: Disposable? = null private var adapter: FlexibleAdapter>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) binding = ActivityMessageSearchBinding.inflate(layoutInflater) setupActionBar() setupSystemColors() setContentView(binding.root) viewModel = ViewModelProvider(this, viewModelFactory)[MessageSearchViewModel::class.java] user = userProvider.currentUser.blockingGet() val roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!! viewModel.initialize(roomToken) setupStateObserver() binding.swipeRefreshLayout.setOnRefreshListener { viewModel.refresh(searchView.query?.toString()) } } private fun setupActionBar() { setSupportActionBar(binding.messageSearchToolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) val conversationName = intent.getStringExtra(BundleKeys.KEY_CONVERSATION_NAME) supportActionBar?.title = conversationName } private fun setupSystemColors() { DisplayUtils.applyColorToStatusBar( this, ResourcesCompat.getColor( resources, R.color.appbar, null ) ) DisplayUtils.applyColorToNavigationBar( this.window, ResourcesCompat.getColor(resources, R.color.bg_default, null) ) } private fun setupStateObserver() { viewModel.state.observe(this) { state -> when (state) { MessageSearchViewModel.InitialState -> showInitial() MessageSearchViewModel.EmptyState -> showEmpty() is MessageSearchViewModel.LoadedState -> showLoaded(state) MessageSearchViewModel.LoadingState -> showLoading() MessageSearchViewModel.ErrorState -> showError() is MessageSearchViewModel.FinishedState -> onFinish() } } } private fun showError() { displayLoading(false) Toast.makeText(this, "Error while searching", Toast.LENGTH_SHORT).show() } private fun showLoading() { displayLoading(true) } private fun displayLoading(loading: Boolean) { binding.swipeRefreshLayout.isRefreshing = loading } private fun showLoaded(state: MessageSearchViewModel.LoadedState) { displayLoading(false) binding.emptyContainer.emptyListView.visibility = View.GONE binding.messageSearchRecycler.visibility = View.VISIBLE setAdapterItems(state) } private fun setAdapterItems(state: MessageSearchViewModel.LoadedState) { val loadMoreItems = if (state.hasMore) { listOf(LoadMoreResultsItem) } else { emptyList() } val newItems = state.results.map { MessageResultItem(this, user, it, false, viewThemeUtils) } + loadMoreItems if (adapter != null) { adapter!!.updateDataSet(newItems) } else { createAdapter(newItems) } } private fun createAdapter(items: List>) { adapter = FlexibleAdapter(items) binding.messageSearchRecycler.adapter = adapter adapter!!.addListener(object : FlexibleAdapter.OnItemClickListener { override fun onItemClick(view: View?, position: Int): Boolean { val item = adapter!!.getItem(position) when (item?.itemViewType) { LoadMoreResultsItem.VIEW_TYPE -> { viewModel.loadMore() } MessageResultItem.VIEW_TYPE -> { val messageItem = item as MessageResultItem viewModel.selectMessage(messageItem.messageEntry) } } return false } }) } private fun onFinish() { val state = viewModel.state.value if (state is MessageSearchViewModel.FinishedState) { val resultIntent = Intent().apply { putExtra(RESULT_KEY_MESSAGE_ID, state.selectedMessageId) } setResult(Activity.RESULT_OK, resultIntent) finish() } } private fun showInitial() { displayLoading(false) binding.messageSearchRecycler.visibility = View.GONE binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_typing) binding.emptyContainer.emptyListView.visibility = View.VISIBLE } private fun showEmpty() { displayLoading(false) binding.messageSearchRecycler.visibility = View.GONE binding.emptyContainer.emptyListViewHeadline.text = getString(R.string.message_search_begin_empty) binding.emptyContainer.emptyListView.visibility = View.VISIBLE } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_search, menu) return true } override fun onPrepareOptionsMenu(menu: Menu): Boolean { val menuItem = menu.findItem(R.id.action_search) searchView = menuItem.actionView as SearchView setupSearchView() menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { override fun onMenuItemActionExpand(item: MenuItem): Boolean { searchView.requestFocus() return true } override fun onMenuItemActionCollapse(item: MenuItem): Boolean { onBackPressed() return false } }) menuItem.expandActionView() return true } private fun setupSearchView() { searchView.queryHint = getString(R.string.message_search_hint) searchViewDisposable = observeSearchView(searchView) .debounce { query -> when { TextUtils.isEmpty(query) -> Observable.empty() else -> Observable.timer( ConversationsListController.SEARCH_DEBOUNCE_INTERVAL_MS.toLong(), TimeUnit.MILLISECONDS ) } } .distinctUntilChanged() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { newText -> viewModel.onQueryTextChange(newText) } } override fun onBackPressed() { setResult(Activity.RESULT_CANCELED) finish() } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { android.R.id.home -> { onBackPressed() true } else -> super.onOptionsItemSelected(item) } } override fun onDestroy() { super.onDestroy() searchViewDisposable?.dispose() } companion object { const val RESULT_KEY_MESSAGE_ID = "MessageSearchActivity.result.message" } }