浏览代码

Implement search in specific chat

Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>
Álvaro Brey 2 年之前
父节点
当前提交
b5d8f6ee95

+ 5 - 0
app/src/main/AndroidManifest.xml

@@ -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" />

+ 1 - 1
app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt

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

+ 24 - 7
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -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
+            }
+            R.id.conversation_search -> {
+                startMessageSearch()
+                true
             }
-            else -> return super.onOptionsItemSelected(item)
+            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

+ 1 - 1
app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java

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

+ 6 - 0
app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt

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

+ 238 - 0
app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchActivity.kt

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

+ 27 - 3
app/src/main/java/com/nextcloud/talk/controllers/util/MessageSearchHelper.kt → app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchHelper.kt

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

+ 114 - 0
app/src/main/java/com/nextcloud/talk/messagesearch/MessageSearchViewModel.kt

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

+ 7 - 1
app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepository.kt

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

+ 18 - 2
app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt

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

+ 63 - 0
app/src/main/res/layout/activity_message_search.xml

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

+ 3 - 0
app/src/main/res/layout/empty_list.xml

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

+ 9 - 2
app/src/main/res/menu/menu_conversation.xml

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

+ 30 - 0
app/src/main/res/menu/menu_search.xml

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

+ 9 - 8
app/src/main/res/values/strings.xml

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

+ 1 - 1
app/src/test/java/com/nextcloud/talk/controllers/util/MessageSearchHelperTest.kt → app/src/test/java/com/nextcloud/talk/messagesearch/MessageSearchHelperTest.kt

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

+ 9 - 2
app/src/test/java/com/nextcloud/talk/test/fakes/FakeUnifiedSearchRepository.kt

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