Implement search in specific chat

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>
This commit is contained in:
Álvaro Brey 2022-05-25 16:29:17 +02:00
parent d1d61e87a9
commit b5d8f6ee95
No known key found for this signature in database
GPG Key ID: 2585783189A62105
17 changed files with 565 additions and 28 deletions

View File

@ -172,6 +172,11 @@
android:name=".shareditems.activities.SharedItemsActivity"
android:theme="@style/AppTheme"/>
<activity
android:name=".messagesearch.MessageSearchActivity"
android:theme="@style/AppTheme"
android:exported="false" />
<receiver android:name=".receivers.PackageReplacedReceiver">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />

View File

@ -42,7 +42,7 @@ data class MessageResultItem constructor(
private val context: Context,
private val currentUser: UserEntity,
val messageEntry: SearchMessageEntry,
private val showHeader: Boolean
private val showHeader: Boolean = false
) :
AbstractFlexibleItem<MessageResultItem.ViewHolder>(),
IFilterable<String>,

View File

@ -130,6 +130,7 @@ import com.nextcloud.talk.events.UserMentionClickEvent
import com.nextcloud.talk.events.WebSocketCommunicationEvent
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
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.UserEntity
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?) {
if (resultCode != RESULT_OK) {
// TODO for message search, CANCELED is fine
Log.e(TAG, "resultCode for received intent was != ok")
return
}
@ -1452,6 +1454,8 @@ class ChatController(args: Bundle) :
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 {
when (item.itemId) {
return when (item.itemId) {
android.R.id.home -> {
(activity as MainActivity).resetConversationsList()
return true
true
}
R.id.conversation_video_call -> {
startACall(false, false)
return true
true
}
R.id.conversation_voice_call -> {
startACall(true, false)
return true
true
}
R.id.conversation_info -> {
showConversationInfoScreen()
return true
true
}
R.id.shared_items -> {
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)
}
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> {
val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
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 REQUEST_CODE_CHOOSE_FILE: Int = 555
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_READ_CONTACT_PERMISSION = 234
private const val REQUEST_CAMERA_PERMISSION = 223

View File

@ -74,7 +74,7 @@ import com.nextcloud.talk.adapters.items.MessagesTextHeaderItem;
import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication;
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.EventStatus;
import com.nextcloud.talk.interfaces.ConversationMenuInterface;

View File

@ -23,6 +23,7 @@ package com.nextcloud.talk.dagger.modules
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.messagesearch.MessageSearchViewModel
import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel
import dagger.Binds
import dagger.MapKey
@ -53,4 +54,9 @@ abstract class ViewModelModule {
@IntoMap
@ViewModelKey(SharedItemsViewModel::class)
abstract fun sharedItemsViewModel(viewModel: SharedItemsViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(MessageSearchViewModel::class)
abstract fun messageSearchViewModel(viewModel: MessageSearchViewModel): ViewModel
}

View File

@ -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()
}
}

View File

@ -19,7 +19,7 @@
* 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 com.nextcloud.talk.models.database.UserEntity
@ -28,9 +28,10 @@ import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
import io.reactivex.Observable
import io.reactivex.disposables.Disposable
class MessageSearchHelper(
class MessageSearchHelper @JvmOverloads constructor(
private val user: UserEntity,
private val unifiedSearchRepository: UnifiedSearchRepository,
private val fromRoom: String? = null
) {
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> {
disposeIfPossible()
return unifiedSearchRepository.searchMessages(user, search, cursor)
return searchCall(search, cursor)
.map { results ->
previousSearch = search
previousCursor = results.cursor
@ -76,6 +77,29 @@ class MessageSearchHelper(
.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() {
previousSearch = null
previousCursor = 0

View File

@ -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
}
}

View File

@ -18,7 +18,13 @@ interface UnifiedSearchRepository {
limit: Int = DEFAULT_PAGE_SIZE
): 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 {
private const val DEFAULT_PAGE_SIZE = 5

View File

@ -48,10 +48,26 @@ class UnifiedSearchRepositoryImpl(private val api: NcApi) : UnifiedSearchReposit
return apiObservable.map { mapToMessageResults(it.ocs?.data!!, searchTerm, limit) }
}
override fun searchInRoom(text: String, roomId: String): Observable<List<SearchMessageEntry>> {
TODO()
override fun searchInRoom(
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 {
private const val PROVIDER_TALK_MESSAGE = "talk-message"
private const val PROVIDER_TALK_MESSAGE_CURRENT = "talk-message-current"

View 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>

View File

@ -20,6 +20,7 @@
License along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/empty_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -60,5 +61,7 @@
android:paddingTop="@dimen/standard_half_padding"
android:paddingBottom="@dimen/standard_half_padding"
android:text=""
tools:visibility="visible"
tools:text="Empty list view text"
android:visibility="gone" />
</LinearLayout>

View File

@ -35,15 +35,22 @@
android:title="@string/nc_conversation_menu_video_call"
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
android:id="@+id/conversation_info"
android:orderInCategory="1"
android:orderInCategory="3"
android:title="@string/nc_conversation_menu_conversation_info"
app:showAsAction="never" />
<item
android:id="@+id/shared_items"
android:orderInCategory="1"
android:orderInCategory="4"
android:title="@string/nc_shared_items"
app:showAsAction="never" />
</menu>

View 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>

View File

@ -273,14 +273,14 @@
<string name="dnd">Do not disturb</string>
<string name="away">Away</string>
<string name="invisible">Invisible</string>
<string translatable="false" name="divider"></string>
<string translatable="false" name="default_emoji">😃</string>
<string translatable="false" name="emoji_thumbsUp">👍</string>
<string translatable="false" name="emoji_thumbsDown">👎</string>
<string translatable="false" name="emoji_heart">❤️</string>
<string translatable="false" name="emoji_confused">😯</string>
<string translatable="false" name="emoji_sad">😢</string>
<string translatable="false" name="emoji_more">More emojis</string>
<string name="divider" translatable="false"></string>
<string name="default_emoji" translatable="false">😃</string>
<string name="emoji_thumbsUp" translatable="false">👍</string>
<string name="emoji_thumbsDown" translatable="false">👎</string>
<string name="emoji_heart" translatable="false">❤️</string>
<string name="emoji_confused" translatable="false">😯</string>
<string name="emoji_sad" translatable="false">😢</string>
<string name="emoji_more" translatable="false">More emojis</string>
<string name="dontClear">Don\'t clear</string>
<string name="today">Today</string>
<string name="thirtyMinutes">30 minutes</string>
@ -525,5 +525,6 @@
<string name="call_without_notification">Call without notification</string>
<string name="messages">Messages</string>
<string name="load_more_results">Load more results</string>
<string name="nc_search_hint">Search…</string>
</resources>

View File

@ -19,7 +19,7 @@
* 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.domain.SearchMessageEntry

View File

@ -41,7 +41,14 @@ class FakeUnifiedSearchRepository : UnifiedSearchRepository {
return Observable.just(response)
}
override fun searchInRoom(text: String, roomId: String): Observable<List<SearchMessageEntry>> {
TODO("Not yet implemented")
override fun searchInRoom(
userEntity: UserEntity,
roomToken: String,
searchTerm: String,
cursor: Int,
limit: Int
): Observable<UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry>> {
lastRequestedCursor = cursor
return Observable.just(response)
}
}