mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-19 19:49:33 +01:00
Implement search in specific chat
Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>
This commit is contained in:
parent
d1d61e87a9
commit
b5d8f6ee95
@ -172,6 +172,11 @@
|
|||||||
android:name=".shareditems.activities.SharedItemsActivity"
|
android:name=".shareditems.activities.SharedItemsActivity"
|
||||||
android:theme="@style/AppTheme"/>
|
android:theme="@style/AppTheme"/>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".messagesearch.MessageSearchActivity"
|
||||||
|
android:theme="@style/AppTheme"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver android:name=".receivers.PackageReplacedReceiver">
|
<receiver android:name=".receivers.PackageReplacedReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
|
@ -42,7 +42,7 @@ data class MessageResultItem constructor(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val currentUser: UserEntity,
|
private val currentUser: UserEntity,
|
||||||
val messageEntry: SearchMessageEntry,
|
val messageEntry: SearchMessageEntry,
|
||||||
private val showHeader: Boolean
|
private val showHeader: Boolean = false
|
||||||
) :
|
) :
|
||||||
AbstractFlexibleItem<MessageResultItem.ViewHolder>(),
|
AbstractFlexibleItem<MessageResultItem.ViewHolder>(),
|
||||||
IFilterable<String>,
|
IFilterable<String>,
|
||||||
|
@ -130,6 +130,7 @@ import com.nextcloud.talk.events.UserMentionClickEvent
|
|||||||
import com.nextcloud.talk.events.WebSocketCommunicationEvent
|
import com.nextcloud.talk.events.WebSocketCommunicationEvent
|
||||||
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
|
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
|
||||||
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
|
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
|
||||||
|
import com.nextcloud.talk.messagesearch.MessageSearchActivity
|
||||||
import com.nextcloud.talk.models.database.CapabilitiesUtil
|
import com.nextcloud.talk.models.database.CapabilitiesUtil
|
||||||
import com.nextcloud.talk.models.database.UserEntity
|
import com.nextcloud.talk.models.database.UserEntity
|
||||||
import com.nextcloud.talk.models.json.chat.ChatMessage
|
import com.nextcloud.talk.models.json.chat.ChatMessage
|
||||||
@ -1346,6 +1347,7 @@ class ChatController(args: Bundle) :
|
|||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||||
if (resultCode != RESULT_OK) {
|
if (resultCode != RESULT_OK) {
|
||||||
|
// TODO for message search, CANCELED is fine
|
||||||
Log.e(TAG, "resultCode for received intent was != ok")
|
Log.e(TAG, "resultCode for received intent was != ok")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1452,6 +1454,8 @@ class ChatController(args: Bundle) :
|
|||||||
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
|
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (requestCode == REQUEST_CODE_MESSAGE_SEARCH) {
|
||||||
|
TODO()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2469,28 +2473,32 @@ class ChatController(args: Bundle) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
return when (item.itemId) {
|
||||||
android.R.id.home -> {
|
android.R.id.home -> {
|
||||||
(activity as MainActivity).resetConversationsList()
|
(activity as MainActivity).resetConversationsList()
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
R.id.conversation_video_call -> {
|
R.id.conversation_video_call -> {
|
||||||
startACall(false, false)
|
startACall(false, false)
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
R.id.conversation_voice_call -> {
|
R.id.conversation_voice_call -> {
|
||||||
startACall(true, false)
|
startACall(true, false)
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
R.id.conversation_info -> {
|
R.id.conversation_info -> {
|
||||||
showConversationInfoScreen()
|
showConversationInfoScreen()
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
R.id.shared_items -> {
|
R.id.shared_items -> {
|
||||||
showSharedItems()
|
showSharedItems()
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
else -> return super.onOptionsItemSelected(item)
|
R.id.conversation_search -> {
|
||||||
|
startMessageSearch()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2502,6 +2510,14 @@ class ChatController(args: Bundle) :
|
|||||||
activity!!.startActivity(intent)
|
activity!!.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startMessageSearch() {
|
||||||
|
val intent = Intent(activity, MessageSearchActivity::class.java)
|
||||||
|
intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
|
||||||
|
intent.putExtra(KEY_ROOM_TOKEN, roomToken)
|
||||||
|
intent.putExtra(KEY_USER_ENTITY, conversationUser as Parcelable)
|
||||||
|
activity!!.startActivityForResult(intent, REQUEST_CODE_MESSAGE_SEARCH)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
|
private fun handleSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
|
||||||
val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
|
val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
|
||||||
val chatMessageIterator = chatMessageMap.iterator()
|
val chatMessageIterator = chatMessageMap.iterator()
|
||||||
@ -3087,6 +3103,7 @@ class ChatController(args: Bundle) :
|
|||||||
private const val AGE_THREHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
|
private const val AGE_THREHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
|
||||||
private const val REQUEST_CODE_CHOOSE_FILE: Int = 555
|
private const val REQUEST_CODE_CHOOSE_FILE: Int = 555
|
||||||
private const val REQUEST_CODE_SELECT_CONTACT: Int = 666
|
private const val REQUEST_CODE_SELECT_CONTACT: Int = 666
|
||||||
|
private const val REQUEST_CODE_MESSAGE_SEARCH: Int = 777
|
||||||
private const val REQUEST_RECORD_AUDIO_PERMISSION = 222
|
private const val REQUEST_RECORD_AUDIO_PERMISSION = 222
|
||||||
private const val REQUEST_READ_CONTACT_PERMISSION = 234
|
private const val REQUEST_READ_CONTACT_PERMISSION = 234
|
||||||
private const val REQUEST_CAMERA_PERMISSION = 223
|
private const val REQUEST_CAMERA_PERMISSION = 223
|
||||||
|
@ -74,7 +74,7 @@ import com.nextcloud.talk.adapters.items.MessagesTextHeaderItem;
|
|||||||
import com.nextcloud.talk.api.NcApi;
|
import com.nextcloud.talk.api.NcApi;
|
||||||
import com.nextcloud.talk.application.NextcloudTalkApplication;
|
import com.nextcloud.talk.application.NextcloudTalkApplication;
|
||||||
import com.nextcloud.talk.controllers.base.BaseController;
|
import com.nextcloud.talk.controllers.base.BaseController;
|
||||||
import com.nextcloud.talk.controllers.util.MessageSearchHelper;
|
import com.nextcloud.talk.messagesearch.MessageSearchHelper;
|
||||||
import com.nextcloud.talk.events.ConversationsListFetchDataEvent;
|
import com.nextcloud.talk.events.ConversationsListFetchDataEvent;
|
||||||
import com.nextcloud.talk.events.EventStatus;
|
import com.nextcloud.talk.events.EventStatus;
|
||||||
import com.nextcloud.talk.interfaces.ConversationMenuInterface;
|
import com.nextcloud.talk.interfaces.ConversationMenuInterface;
|
||||||
|
@ -23,6 +23,7 @@ package com.nextcloud.talk.dagger.modules
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.nextcloud.talk.messagesearch.MessageSearchViewModel
|
||||||
import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
|
import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.MapKey
|
import dagger.MapKey
|
||||||
@ -53,4 +54,9 @@ abstract class ViewModelModule {
|
|||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(SharedItemsViewModel::class)
|
@ViewModelKey(SharedItemsViewModel::class)
|
||||||
abstract fun sharedItemsViewModel(viewModel: SharedItemsViewModel): ViewModel
|
abstract fun sharedItemsViewModel(viewModel: SharedItemsViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(MessageSearchViewModel::class)
|
||||||
|
abstract fun messageSearchViewModel(viewModel: MessageSearchViewModel): ViewModel
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,238 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.messagesearch
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
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.databinding.ActivityMessageSearchBinding
|
||||||
|
import com.nextcloud.talk.models.database.UserEntity
|
||||||
|
import com.nextcloud.talk.utils.DisplayUtils
|
||||||
|
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||||
|
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
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityMessageSearchBinding
|
||||||
|
private lateinit var searchView: SearchView
|
||||||
|
|
||||||
|
private lateinit var user: UserEntity
|
||||||
|
|
||||||
|
private lateinit var viewModel: MessageSearchViewModel
|
||||||
|
|
||||||
|
private var searchViewDisposable: Disposable? = null
|
||||||
|
private var adapter: FlexibleAdapter<AbstractFlexibleItem<*>>? = 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 = intent.getParcelableExtra(BundleKeys.KEY_USER_ENTITY)!!
|
||||||
|
val roomToken = intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN)!!
|
||||||
|
viewModel.initialize(user, roomToken)
|
||||||
|
setupStateObserver()
|
||||||
|
}
|
||||||
|
|
||||||
|
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.EmptyState -> showEmpty()
|
||||||
|
MessageSearchViewModel.InitialState -> showInitial()
|
||||||
|
is MessageSearchViewModel.LoadedState -> showLoaded(state)
|
||||||
|
MessageSearchViewModel.LoadingState -> showLoading()
|
||||||
|
MessageSearchViewModel.ErrorState -> showError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showError() {
|
||||||
|
Toast.makeText(this, "Error while searching", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoading() {
|
||||||
|
// TODO
|
||||||
|
Toast.makeText(this, "LOADING", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoaded(state: MessageSearchViewModel.LoadedState) {
|
||||||
|
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) } + loadMoreItems
|
||||||
|
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter!!.updateDataSet(newItems)
|
||||||
|
} else {
|
||||||
|
createAdapter(newItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAdapter(items: List<AbstractFlexibleItem<out FlexibleViewHolder>>) {
|
||||||
|
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)
|
||||||
|
if (item?.itemViewType == LoadMoreResultsItem.VIEW_TYPE) {
|
||||||
|
viewModel.loadMore()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showInitial() {
|
||||||
|
binding.messageSearchRecycler.visibility = View.GONE
|
||||||
|
binding.emptyContainer.emptyListViewHeadline.text = "Start typing to search..."
|
||||||
|
binding.emptyContainer.emptyListView.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEmpty() {
|
||||||
|
binding.messageSearchRecycler.visibility = View.GONE
|
||||||
|
binding.emptyContainer.emptyListViewHeadline.text = "No search results"
|
||||||
|
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.nc_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()
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.nextcloud.talk.controllers.util
|
package com.nextcloud.talk.messagesearch
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.nextcloud.talk.models.database.UserEntity
|
import com.nextcloud.talk.models.database.UserEntity
|
||||||
@ -28,9 +28,10 @@ import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
|
|||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
|
|
||||||
class MessageSearchHelper(
|
class MessageSearchHelper @JvmOverloads constructor(
|
||||||
private val user: UserEntity,
|
private val user: UserEntity,
|
||||||
private val unifiedSearchRepository: UnifiedSearchRepository,
|
private val unifiedSearchRepository: UnifiedSearchRepository,
|
||||||
|
private val fromRoom: String? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
data class MessageSearchResults(val messages: List<SearchMessageEntry>, val hasMore: Boolean)
|
data class MessageSearchResults(val messages: List<SearchMessageEntry>, val hasMore: Boolean)
|
||||||
@ -58,7 +59,7 @@ class MessageSearchHelper(
|
|||||||
|
|
||||||
private fun doSearch(search: String, cursor: Int = 0): Observable<MessageSearchResults> {
|
private fun doSearch(search: String, cursor: Int = 0): Observable<MessageSearchResults> {
|
||||||
disposeIfPossible()
|
disposeIfPossible()
|
||||||
return unifiedSearchRepository.searchMessages(user, search, cursor)
|
return searchCall(search, cursor)
|
||||||
.map { results ->
|
.map { results ->
|
||||||
previousSearch = search
|
previousSearch = search
|
||||||
previousCursor = results.cursor
|
previousCursor = results.cursor
|
||||||
@ -76,6 +77,29 @@ class MessageSearchHelper(
|
|||||||
.doOnComplete(this::disposeIfPossible)
|
.doOnComplete(this::disposeIfPossible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun searchCall(
|
||||||
|
search: String,
|
||||||
|
cursor: Int
|
||||||
|
): Observable<UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry>> {
|
||||||
|
return when {
|
||||||
|
fromRoom != null -> {
|
||||||
|
unifiedSearchRepository.searchInRoom(
|
||||||
|
userEntity = user,
|
||||||
|
roomToken = fromRoom,
|
||||||
|
searchTerm = search,
|
||||||
|
cursor = cursor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
unifiedSearchRepository.searchMessages(
|
||||||
|
userEntity = user,
|
||||||
|
searchTerm = search,
|
||||||
|
cursor = cursor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun resetCachedData() {
|
private fun resetCachedData() {
|
||||||
previousSearch = null
|
previousSearch = null
|
||||||
previousCursor = 0
|
previousCursor = 0
|
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.messagesearch
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.nextcloud.talk.models.database.UserEntity
|
||||||
|
import com.nextcloud.talk.models.domain.SearchMessageEntry
|
||||||
|
import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install PlantUML plugin to render this state diagram
|
||||||
|
* @startuml
|
||||||
|
* hide empty description
|
||||||
|
* [*] --> InitialState
|
||||||
|
* InitialState --> LoadingState
|
||||||
|
* LoadingState --> EmptyState
|
||||||
|
* LoadingState --> LoadedState
|
||||||
|
* LoadingState --> LoadingState
|
||||||
|
* LoadedState --> LoadingState
|
||||||
|
* EmptyState --> LoadingState
|
||||||
|
* LoadingState --> ErrorState
|
||||||
|
* ErrorState --> LoadingState
|
||||||
|
* @enduml
|
||||||
|
*/
|
||||||
|
class MessageSearchViewModel @Inject constructor(private val unifiedSearchRepository: UnifiedSearchRepository) :
|
||||||
|
ViewModel() {
|
||||||
|
|
||||||
|
sealed class ViewState
|
||||||
|
object InitialState : ViewState()
|
||||||
|
object LoadingState : ViewState()
|
||||||
|
object EmptyState : ViewState()
|
||||||
|
object ErrorState : ViewState()
|
||||||
|
class LoadedState(val results: List<SearchMessageEntry>, val hasMore: Boolean) : ViewState()
|
||||||
|
|
||||||
|
private lateinit var messageSearchHelper: MessageSearchHelper
|
||||||
|
|
||||||
|
private val _state: MutableLiveData<ViewState> = MutableLiveData(InitialState)
|
||||||
|
val state: LiveData<ViewState>
|
||||||
|
get() = _state
|
||||||
|
|
||||||
|
private var searchDisposable: Disposable? = null
|
||||||
|
|
||||||
|
fun initialize(user: UserEntity, roomToken: String) {
|
||||||
|
messageSearchHelper = MessageSearchHelper(user, unifiedSearchRepository, roomToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult") // handled by helper
|
||||||
|
fun onQueryTextChange(newText: String) {
|
||||||
|
if (newText.length >= MIN_CHARS_FOR_SEARCH) {
|
||||||
|
_state.value = LoadingState
|
||||||
|
messageSearchHelper.cancelSearch()
|
||||||
|
messageSearchHelper.startMessageSearch(newText)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(this::onReceiveResults, this::onError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult") // handled by helper
|
||||||
|
fun loadMore() {
|
||||||
|
_state.value = LoadingState
|
||||||
|
messageSearchHelper.cancelSearch()
|
||||||
|
messageSearchHelper.loadMore()
|
||||||
|
?.subscribeOn(Schedulers.io())
|
||||||
|
?.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
?.subscribe(this::onReceiveResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onReceiveResults(results: MessageSearchHelper.MessageSearchResults) {
|
||||||
|
if (results.messages.isEmpty()) {
|
||||||
|
_state.value = EmptyState
|
||||||
|
} else {
|
||||||
|
_state.value = LoadedState(results.messages, results.hasMore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onError(throwable: Throwable) {
|
||||||
|
Log.e(TAG, "onError:", throwable)
|
||||||
|
messageSearchHelper.cancelSearch()
|
||||||
|
_state.value = ErrorState
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = MessageSearchViewModel::class.simpleName
|
||||||
|
private const val MIN_CHARS_FOR_SEARCH = 2
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,13 @@ interface UnifiedSearchRepository {
|
|||||||
limit: Int = DEFAULT_PAGE_SIZE
|
limit: Int = DEFAULT_PAGE_SIZE
|
||||||
): Observable<UnifiedSearchResults<SearchMessageEntry>>
|
): Observable<UnifiedSearchResults<SearchMessageEntry>>
|
||||||
|
|
||||||
fun searchInRoom(text: String, roomId: String): Observable<List<SearchMessageEntry>>
|
fun searchInRoom(
|
||||||
|
userEntity: UserEntity,
|
||||||
|
roomToken: String,
|
||||||
|
searchTerm: String,
|
||||||
|
cursor: Int = 0,
|
||||||
|
limit: Int = DEFAULT_PAGE_SIZE
|
||||||
|
): Observable<UnifiedSearchResults<SearchMessageEntry>>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DEFAULT_PAGE_SIZE = 5
|
private const val DEFAULT_PAGE_SIZE = 5
|
||||||
|
@ -48,10 +48,26 @@ class UnifiedSearchRepositoryImpl(private val api: NcApi) : UnifiedSearchReposit
|
|||||||
return apiObservable.map { mapToMessageResults(it.ocs?.data!!, searchTerm, limit) }
|
return apiObservable.map { mapToMessageResults(it.ocs?.data!!, searchTerm, limit) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchInRoom(text: String, roomId: String): Observable<List<SearchMessageEntry>> {
|
override fun searchInRoom(
|
||||||
TODO()
|
userEntity: UserEntity,
|
||||||
|
roomToken: String,
|
||||||
|
searchTerm: String,
|
||||||
|
cursor: Int,
|
||||||
|
limit: Int
|
||||||
|
): Observable<UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry>> {
|
||||||
|
val apiObservable = api.performUnifiedSearch(
|
||||||
|
ApiUtils.getCredentials(userEntity.username, userEntity.token),
|
||||||
|
ApiUtils.getUrlForUnifiedSearch(userEntity.baseUrl, PROVIDER_TALK_MESSAGE_CURRENT),
|
||||||
|
searchTerm,
|
||||||
|
fromUrlForRoom(roomToken),
|
||||||
|
limit,
|
||||||
|
cursor
|
||||||
|
)
|
||||||
|
return apiObservable.map { mapToMessageResults(it.ocs?.data!!, searchTerm, limit) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fromUrlForRoom(roomToken: String) = "/call/$roomToken"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PROVIDER_TALK_MESSAGE = "talk-message"
|
private const val PROVIDER_TALK_MESSAGE = "talk-message"
|
||||||
private const val PROVIDER_TALK_MESSAGE_CURRENT = "talk-message-current"
|
private const val PROVIDER_TALK_MESSAGE_CURRENT = "talk-message-current"
|
||||||
|
63
app/src/main/res/layout/activity_message_search.xml
Normal file
63
app/src/main/res/layout/activity_message_search.xml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Nextcloud Talk application
|
||||||
|
~
|
||||||
|
~ @author Álvaro Brey
|
||||||
|
~ Copyright (C) 2022 Álvaro Brey <alvaro.brey@nextcloud.com>
|
||||||
|
~ 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 <http://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/bg_default"
|
||||||
|
tools:context=".messagesearch.MessageSearchActivity">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/message_search_appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/message_search_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="@color/appbar"
|
||||||
|
android:theme="?attr/actionBarPopupTheme"
|
||||||
|
app:layout_scrollFlags="scroll|enterAlways"
|
||||||
|
app:navigationIconTint="@color/fontAppbar"
|
||||||
|
app:popupTheme="@style/appActionBarPopupMenu"
|
||||||
|
app:titleTextColor="@color/fontAppbar"
|
||||||
|
tools:title="@string/nc_app_product_name">
|
||||||
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/emptyContainer"
|
||||||
|
layout="@layout/empty_list"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/message_search_recycler"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
tools:listitem="@layout/rv_item_search_message" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -20,6 +20,7 @@
|
|||||||
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/empty_list_view"
|
android:id="@+id/empty_list_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@ -60,5 +61,7 @@
|
|||||||
android:paddingTop="@dimen/standard_half_padding"
|
android:paddingTop="@dimen/standard_half_padding"
|
||||||
android:paddingBottom="@dimen/standard_half_padding"
|
android:paddingBottom="@dimen/standard_half_padding"
|
||||||
android:text=""
|
android:text=""
|
||||||
|
tools:visibility="visible"
|
||||||
|
tools:text="Empty list view text"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -35,15 +35,22 @@
|
|||||||
android:title="@string/nc_conversation_menu_video_call"
|
android:title="@string/nc_conversation_menu_video_call"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/conversation_search"
|
||||||
|
android:icon="@drawable/ic_search_white_24dp"
|
||||||
|
android:orderInCategory="2"
|
||||||
|
android:title="@string/nc_search"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/conversation_info"
|
android:id="@+id/conversation_info"
|
||||||
android:orderInCategory="1"
|
android:orderInCategory="3"
|
||||||
android:title="@string/nc_conversation_menu_conversation_info"
|
android:title="@string/nc_conversation_menu_conversation_info"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/shared_items"
|
android:id="@+id/shared_items"
|
||||||
android:orderInCategory="1"
|
android:orderInCategory="4"
|
||||||
android:title="@string/nc_shared_items"
|
android:title="@string/nc_shared_items"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
30
app/src/main/res/menu/menu_search.xml
Normal file
30
app/src/main/res/menu/menu_search.xml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ 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 <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_search"
|
||||||
|
android:icon="@drawable/ic_search_white_24dp"
|
||||||
|
android:title="@string/nc_search"
|
||||||
|
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||||
|
app:showAsAction="always|collapseActionView" />
|
||||||
|
</menu>
|
@ -273,14 +273,14 @@
|
|||||||
<string name="dnd">Do not disturb</string>
|
<string name="dnd">Do not disturb</string>
|
||||||
<string name="away">Away</string>
|
<string name="away">Away</string>
|
||||||
<string name="invisible">Invisible</string>
|
<string name="invisible">Invisible</string>
|
||||||
<string translatable="false" name="divider">—</string>
|
<string name="divider" translatable="false">—</string>
|
||||||
<string translatable="false" name="default_emoji">😃</string>
|
<string name="default_emoji" translatable="false">😃</string>
|
||||||
<string translatable="false" name="emoji_thumbsUp">👍</string>
|
<string name="emoji_thumbsUp" translatable="false">👍</string>
|
||||||
<string translatable="false" name="emoji_thumbsDown">👎</string>
|
<string name="emoji_thumbsDown" translatable="false">👎</string>
|
||||||
<string translatable="false" name="emoji_heart">❤️</string>
|
<string name="emoji_heart" translatable="false">❤️</string>
|
||||||
<string translatable="false" name="emoji_confused">😯</string>
|
<string name="emoji_confused" translatable="false">😯</string>
|
||||||
<string translatable="false" name="emoji_sad">😢</string>
|
<string name="emoji_sad" translatable="false">😢</string>
|
||||||
<string translatable="false" name="emoji_more">More emojis</string>
|
<string name="emoji_more" translatable="false">More emojis</string>
|
||||||
<string name="dontClear">Don\'t clear</string>
|
<string name="dontClear">Don\'t clear</string>
|
||||||
<string name="today">Today</string>
|
<string name="today">Today</string>
|
||||||
<string name="thirtyMinutes">30 minutes</string>
|
<string name="thirtyMinutes">30 minutes</string>
|
||||||
@ -525,5 +525,6 @@
|
|||||||
<string name="call_without_notification">Call without notification</string>
|
<string name="call_without_notification">Call without notification</string>
|
||||||
<string name="messages">Messages</string>
|
<string name="messages">Messages</string>
|
||||||
<string name="load_more_results">Load more results</string>
|
<string name="load_more_results">Load more results</string>
|
||||||
|
<string name="nc_search_hint">Search…</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.nextcloud.talk.controllers.util
|
package com.nextcloud.talk.messagesearch
|
||||||
|
|
||||||
import com.nextcloud.talk.models.database.UserEntity
|
import com.nextcloud.talk.models.database.UserEntity
|
||||||
import com.nextcloud.talk.models.domain.SearchMessageEntry
|
import com.nextcloud.talk.models.domain.SearchMessageEntry
|
@ -41,7 +41,14 @@ class FakeUnifiedSearchRepository : UnifiedSearchRepository {
|
|||||||
return Observable.just(response)
|
return Observable.just(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchInRoom(text: String, roomId: String): Observable<List<SearchMessageEntry>> {
|
override fun searchInRoom(
|
||||||
TODO("Not yet implemented")
|
userEntity: UserEntity,
|
||||||
|
roomToken: String,
|
||||||
|
searchTerm: String,
|
||||||
|
cursor: Int,
|
||||||
|
limit: Int
|
||||||
|
): Observable<UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry>> {
|
||||||
|
lastRequestedCursor = cursor
|
||||||
|
return Observable.just(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user