diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt
new file mode 100644
index 000000000..5ae33ee15
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/items/LoadMoreResultsItem.kt
@@ -0,0 +1,79 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.adapters.items
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.talk.R
+import com.nextcloud.talk.databinding.RvItemLoadMoreBinding
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.davidea.flexibleadapter.items.IFilterable
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.davidea.viewholders.FlexibleViewHolder
+
+object LoadMoreResultsItem :
+ AbstractFlexibleItem(),
+ IFilterable {
+
+ // layout is used as view type for uniqueness
+ const val VIEW_TYPE: Int = R.layout.rv_item_load_more
+
+ class ViewHolder(view: View, adapter: FlexibleAdapter<*>) :
+ FlexibleViewHolder(view, adapter) {
+ var binding: RvItemLoadMoreBinding
+
+ init {
+ binding = RvItemLoadMoreBinding.bind(view)
+ }
+ }
+
+ override fun getLayoutRes(): Int = R.layout.rv_item_load_more
+
+ override fun createViewHolder(
+ view: View,
+ adapter: FlexibleAdapter>
+ ): ViewHolder = ViewHolder(view, adapter)
+
+ override fun bindViewHolder(
+ adapter: FlexibleAdapter>,
+ holder: ViewHolder,
+ position: Int,
+ payloads: MutableList?
+ ) {
+ // nothing, it's immutable
+ }
+
+ override fun filter(constraint: String?): Boolean = true
+
+ override fun getItemViewType(): Int {
+ return VIEW_TYPE
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other is LoadMoreResultsItem
+ }
+
+ override fun hashCode(): Int {
+ return 0
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt
new file mode 100644
index 000000000..725ed5e1c
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/items/MessageResultItem.kt
@@ -0,0 +1,115 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.adapters.items
+
+import android.content.Context
+import android.text.SpannableString
+import android.view.View
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import com.nextcloud.talk.R
+import com.nextcloud.talk.databinding.RvItemSearchMessageBinding
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.models.domain.SearchMessageEntry
+import com.nextcloud.talk.utils.DisplayUtils
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.davidea.flexibleadapter.items.IFilterable
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.davidea.flexibleadapter.items.ISectionable
+import eu.davidea.viewholders.FlexibleViewHolder
+
+data class MessageResultItem constructor(
+ private val context: Context,
+ private val currentUser: UserEntity,
+ val messageEntry: SearchMessageEntry,
+ private val showHeader: Boolean
+) :
+ AbstractFlexibleItem(),
+ IFilterable,
+ ISectionable {
+
+ class ViewHolder(view: View, adapter: FlexibleAdapter<*>) :
+ FlexibleViewHolder(view, adapter) {
+ var binding: RvItemSearchMessageBinding
+
+ init {
+ binding = RvItemSearchMessageBinding.bind(view)
+ }
+ }
+
+ override fun getLayoutRes(): Int = R.layout.rv_item_search_message
+
+ override fun createViewHolder(
+ view: View,
+ adapter: FlexibleAdapter>
+ ): ViewHolder = ViewHolder(view, adapter)
+
+ override fun bindViewHolder(
+ adapter: FlexibleAdapter>,
+ holder: ViewHolder,
+ position: Int,
+ payloads: MutableList?
+ ) {
+ holder.binding.conversationTitle.text = messageEntry.title
+ bindMessageExcerpt(holder)
+ loadImage(holder)
+ }
+
+ private fun bindMessageExcerpt(holder: ViewHolder) {
+ val messageSpannable = SpannableString(messageEntry.messageExcerpt)
+ val highlightColor = ContextCompat.getColor(context, R.color.colorPrimary)
+ val highlightedSpan = DisplayUtils.searchAndColor(messageSpannable, messageEntry.searchTerm, highlightColor)
+ holder.binding.messageExcerpt.text = highlightedSpan
+ }
+
+ private fun loadImage(holder: ViewHolder) {
+ DisplayUtils.loadAvatarPlaceholder(holder.binding.thumbnail)
+ if (messageEntry.thumbnailURL != null) {
+ val imageRequest = DisplayUtils.getImageRequestForUrl(
+ messageEntry.thumbnailURL,
+ currentUser
+ )
+ DisplayUtils.loadImage(holder.binding.thumbnail, imageRequest)
+ }
+ }
+
+ override fun filter(constraint: String?): Boolean = true
+
+ override fun getItemViewType(): Int {
+ return VIEW_TYPE
+ }
+
+ companion object {
+ // layout is used as view type for uniqueness
+ const val VIEW_TYPE: Int = R.layout.rv_item_search_message
+ }
+
+ override fun getHeader(): GenericTextHeaderItem = MessagesTextHeaderItem(context)
+ .apply {
+ isHidden = showHeader // FlexibleAdapter needs this hack for some reason
+ }
+
+ override fun setHeader(header: GenericTextHeaderItem?) {
+ // nothing, header is always the same
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MessagesTextHeaderItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/MessagesTextHeaderItem.kt
new file mode 100644
index 000000000..24ddeabc5
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/adapters/items/MessagesTextHeaderItem.kt
@@ -0,0 +1,36 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.adapters.items
+
+import android.content.Context
+import com.nextcloud.talk.R
+
+class MessagesTextHeaderItem(context: Context) : GenericTextHeaderItem(context.getString(R.string.messages)) {
+ companion object {
+ /**
+ * "Random" value, just has to be different than other view types
+ */
+ const val VIEW_TYPE = 1120391230
+ }
+
+ override fun getItemViewType(): Int = VIEW_TYPE
+}
diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java
index e160ebfda..35742aeb5 100644
--- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java
+++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java
@@ -45,6 +45,7 @@ import com.nextcloud.talk.models.json.signaling.SignalingOverall;
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
import com.nextcloud.talk.models.json.status.StatusOverall;
import com.nextcloud.talk.models.json.statuses.StatusesOverall;
+import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchOverall;
import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall;
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall;
@@ -519,4 +520,13 @@ public interface NcApi {
Observable getReactions(@Header("Authorization") String authorization,
@Url String url,
@Query("reaction") String reaction);
+
+ // TODO use path params instead of passing URL
+ @GET
+ Observable performUnifiedSearch(@Header("Authorization") String authorization,
+ @Url String url,
+ @Query("term") String term,
+ @Query("from") String fromUrl,
+ @Query("limit") Integer limit,
+ @Query("cursor") Integer cursor);
}
diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java
index dfb72b333..a84ec2a25 100644
--- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java
+++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java
@@ -68,9 +68,13 @@ import com.nextcloud.talk.R;
import com.nextcloud.talk.activities.MainActivity;
import com.nextcloud.talk.adapters.items.ConversationItem;
import com.nextcloud.talk.adapters.items.GenericTextHeaderItem;
+import com.nextcloud.talk.adapters.items.LoadMoreResultsItem;
+import com.nextcloud.talk.adapters.items.MessageResultItem;
+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.events.ConversationsListFetchDataEvent;
import com.nextcloud.talk.events.EventStatus;
import com.nextcloud.talk.interfaces.ConversationMenuInterface;
@@ -80,15 +84,18 @@ import com.nextcloud.talk.jobs.DeleteConversationWorker;
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker;
import com.nextcloud.talk.models.database.CapabilitiesUtil;
import com.nextcloud.talk.models.database.UserEntity;
+import com.nextcloud.talk.models.domain.SearchMessageEntry;
import com.nextcloud.talk.models.json.conversations.Conversation;
import com.nextcloud.talk.models.json.status.Status;
import com.nextcloud.talk.models.json.statuses.StatusesOverall;
+import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository;
import com.nextcloud.talk.ui.dialog.ChooseAccountDialogFragment;
import com.nextcloud.talk.ui.dialog.ConversationsListBottomDialog;
import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.AttendeePermissionsUtil;
import com.nextcloud.talk.utils.ClosedInterfaceImpl;
import com.nextcloud.talk.utils.ConductorRemapping;
+import com.nextcloud.talk.utils.Debouncer;
import com.nextcloud.talk.utils.DisplayUtils;
import com.nextcloud.talk.utils.UriUtils;
import com.nextcloud.talk.utils.bundle.BundleKeys;
@@ -131,6 +138,8 @@ import butterknife.BindView;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager;
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
+import eu.davidea.flexibleadapter.items.IHeader;
+import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@@ -145,6 +154,10 @@ public class ConversationsListController extends BaseController implements Searc
public static final int ID_DELETE_CONVERSATION_DIALOG = 0;
public static final int UNREAD_BUBBLE_DELAY = 2500;
private static final String KEY_SEARCH_QUERY = "ContactsController.searchQuery";
+
+ public static final int SEARCH_DEBOUNCE_INTERVAL_MS = 300;
+ public static final int SEARCH_MIN_CHARS = 2;
+
private final Bundle bundle;
@Inject
UserUtils userUtils;
@@ -161,6 +174,9 @@ public class ConversationsListController extends BaseController implements Searc
@Inject
AppPreferences appPreferences;
+ @Inject
+ UnifiedSearchRepository unifiedSearchRepository;
+
@BindView(R.id.recycler_view)
RecyclerView recyclerView;
@@ -220,6 +236,10 @@ public class ConversationsListController extends BaseController implements Searc
private HashMap userStatuses = new HashMap<>();
+ private Debouncer searchDebouncer = new Debouncer(SEARCH_DEBOUNCE_INTERVAL_MS);
+
+ private MessageSearchHelper searchHelper;
+
public ConversationsListController(Bundle bundle) {
super();
setHasOptionsMenu(true);
@@ -306,6 +326,8 @@ public class ConversationsListController extends BaseController implements Searc
return;
}
+ searchHelper = new MessageSearchHelper(currentUser, unifiedSearchRepository);
+
credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken());
if (getActivity() != null && getActivity() instanceof MainActivity) {
loadUserAvatar(((MainActivity) getActivity()).binding.switchAccountButton);
@@ -419,6 +441,11 @@ public class ConversationsListController extends BaseController implements Searc
adapter.setHeadersShown(false);
adapter.updateDataSet(conversationItems, false);
adapter.hideAllHeaders();
+ if (searchHelper != null) {
+ // cancel any pending searches
+ searchHelper.cancelSearch();
+ swipeRefreshLayout.setRefreshing(false);
+ }
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setEnabled(true);
}
@@ -427,8 +454,8 @@ public class ConversationsListController extends BaseController implements Searc
MainActivity activity = (MainActivity) getActivity();
if (activity != null) {
activity.binding.appBar.setStateListAnimator(AnimatorInflater.loadStateListAnimator(
- activity.binding.appBar.getContext(),
- R.animator.appbar_elevation_off)
+ activity.binding.appBar.getContext(),
+ R.animator.appbar_elevation_off)
);
activity.binding.toolbar.setVisibility(View.GONE);
activity.binding.searchToolbar.setVisibility(View.VISIBLE);
@@ -845,20 +872,73 @@ public class ConversationsListController extends BaseController implements Searc
}
@Override
- public boolean onQueryTextChange(String newText) {
- if (adapter.hasNewFilter(newText) || !TextUtils.isEmpty(searchQuery)) {
- if (!TextUtils.isEmpty(searchQuery)) {
- adapter.setFilter(searchQuery);
- searchQuery = "";
- adapter.filterItems();
- } else {
- adapter.setFilter(newText);
- adapter.filterItems(300);
- }
+ public boolean onQueryTextChange(final String newText) {
+ if (!TextUtils.isEmpty(searchQuery)) {
+ final String filter = searchQuery;
+ searchQuery = "";
+ performFilterAndSearch(filter);
+ } else if (adapter.hasNewFilter(newText)) {
+ new Handler();
+ searchDebouncer.debounce(() -> {
+ performFilterAndSearch(newText);
+ });
}
return true;
}
+ private void performFilterAndSearch(String filter) {
+ if (filter.length() >= SEARCH_MIN_CHARS) {
+ clearMessageSearchResults();
+ adapter.setFilter(filter);
+ adapter.filterItems();
+ startMessageSearch(filter);
+ } else {
+ resetSearchResults();
+ }
+ }
+
+ private void resetSearchResults() {
+ clearMessageSearchResults();
+ adapter.setFilter("");
+ adapter.filterItems();
+ }
+
+ private void clearMessageSearchResults() {
+ final IHeader firstHeader = adapter.getSectionHeader(0);
+ if (firstHeader != null && firstHeader.getItemViewType() == MessagesTextHeaderItem.VIEW_TYPE) {
+ adapter.removeSection(firstHeader);
+ } else {
+ adapter.removeItemsOfType(MessageResultItem.VIEW_TYPE);
+ }
+ adapter.removeItemsOfType(LoadMoreResultsItem.VIEW_TYPE);
+ }
+
+ @SuppressLint("CheckResult") // handled by helper
+ private void startMessageSearch(final String search) {
+ swipeRefreshLayout.setRefreshing(true);
+ searchHelper
+ .startMessageSearch(search)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ this::onMessageSearchResult,
+ this::onMessageSearchError);
+ }
+
+ @SuppressLint("CheckResult") // handled by helper
+ private void loadMoreMessages() {
+ swipeRefreshLayout.setRefreshing(true);
+ final Observable observable = searchHelper.loadMore();
+ if (observable != null) {
+ observable
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ this::onMessageSearchResult,
+ this::onMessageSearchError);
+ }
+ }
+
+
@Override
public boolean onQueryTextSubmit(String query) {
return onQueryTextChange(query);
@@ -871,38 +951,59 @@ public class ConversationsListController extends BaseController implements Searc
@Override
public boolean onItemClick(View view, int position) {
- try {
- selectedConversation = ((ConversationItem) Objects.requireNonNull(adapter.getItem(position))).getModel();
-
- if (selectedConversation != null && getActivity() != null) {
- boolean hasChatPermission =
- new AttendeePermissionsUtil(selectedConversation.getPermissions()).hasChatPermission(currentUser);
-
- if (showShareToScreen) {
- if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) {
- handleSharedData();
- showShareToScreen = false;
- } else {
- Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show();
- }
- } else if (forwardMessage) {
- if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) {
- openConversation(bundle.getString(BundleKeys.INSTANCE.getKEY_FORWARD_MSG_TEXT()));
- forwardMessage = false;
- } else {
- Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show();
- }
- } else {
- openConversation();
- }
- }
- } catch (ClassCastException e) {
- Log.w(TAG, "failed to cast clicked item to ConversationItem. Most probably a heading was clicked. This is" +
- " just ignored.", e);
+ final AbstractFlexibleItem item = adapter.getItem(position);
+ if (item instanceof ConversationItem) {
+ showConversation(((ConversationItem) Objects.requireNonNull(item)).getModel());
+ } else if (item instanceof MessageResultItem) {
+ MessageResultItem messageItem = (MessageResultItem) item;
+ String conversationToken = messageItem.getMessageEntry().getConversationToken();
+ showConversationByToken(conversationToken);
+ } else if (item instanceof LoadMoreResultsItem) {
+ loadMoreMessages();
}
+
return true;
}
+ private void showConversationByToken(String conversationToken) {
+ Conversation conversation = null;
+ for (AbstractFlexibleItem absItem : conversationItems) {
+ ConversationItem conversationItem = ((ConversationItem) absItem);
+ if (conversationItem.getModel().getToken().equals(conversationToken)) {
+ conversation = conversationItem.getModel();
+ }
+ }
+ if (conversation != null) {
+ showConversation(conversation);
+ }
+ }
+
+ private void showConversation(@Nullable final Conversation conversation) {
+ selectedConversation = conversation;
+ if (selectedConversation != null && getActivity() != null) {
+ boolean hasChatPermission =
+ new AttendeePermissionsUtil(selectedConversation.getPermissions()).hasChatPermission(currentUser);
+
+ if (showShareToScreen) {
+ if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) {
+ handleSharedData();
+ showShareToScreen = false;
+ } else {
+ Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show();
+ }
+ } else if (forwardMessage) {
+ if (hasChatPermission && !isReadOnlyConversation(selectedConversation)) {
+ openConversation(bundle.getString(BundleKeys.INSTANCE.getKEY_FORWARD_MSG_TEXT()));
+ forwardMessage = false;
+ } else {
+ Toast.makeText(context, R.string.send_to_forbidden, Toast.LENGTH_LONG).show();
+ }
+ } else {
+ openConversation();
+ }
+ }
+ }
+
private Boolean isReadOnlyConversation(Conversation conversation) {
return conversation.getConversationReadOnlyState() ==
Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY;
@@ -1274,4 +1375,29 @@ public class ConversationsListController extends BaseController implements Searc
public AppBarLayoutType getAppBarLayoutType() {
return AppBarLayoutType.SEARCH_BAR;
}
+
+ public void onMessageSearchResult(@NonNull MessageSearchHelper.MessageSearchResults results) {
+ if (searchView.getQuery().length() > 0) {
+ clearMessageSearchResults();
+ final List entries = results.getMessages();
+ if (entries.size() > 0) {
+ List adapterItems = new ArrayList<>();
+ for (int i = 0; i < entries.size(); i++) {
+ final boolean showHeader = i == 0;
+ adapterItems.add(new MessageResultItem(context, currentUser, entries.get(i), showHeader));
+ }
+ if (results.getHasMore()) {
+ adapterItems.add(LoadMoreResultsItem.INSTANCE);
+ }
+ adapter.addItems(0, adapterItems);
+ recyclerView.scrollToPosition(0);
+ }
+ }
+ swipeRefreshLayout.setRefreshing(false);
+ }
+
+ public void onMessageSearchError(@NonNull Throwable throwable) {
+ handleHttpExceptions(throwable);
+ swipeRefreshLayout.setRefreshing(false);
+ }
}
diff --git a/app/src/main/java/com/nextcloud/talk/controllers/util/MessageSearchHelper.kt b/app/src/main/java/com/nextcloud/talk/controllers/util/MessageSearchHelper.kt
new file mode 100644
index 000000000..1432b02ac
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/controllers/util/MessageSearchHelper.kt
@@ -0,0 +1,99 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.controllers.util
+
+import android.util.Log
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.models.domain.SearchMessageEntry
+import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
+import io.reactivex.Observable
+import io.reactivex.disposables.Disposable
+
+class MessageSearchHelper(
+ private val user: UserEntity,
+ private val unifiedSearchRepository: UnifiedSearchRepository,
+) {
+
+ data class MessageSearchResults(val messages: List, val hasMore: Boolean)
+
+ private var unifiedSearchDisposable: Disposable? = null
+ private var previousSearch: String? = null
+ private var previousCursor: Int = 0
+ private var previousResults: List = emptyList()
+
+ fun startMessageSearch(search: String): Observable {
+ return doSearch(search)
+ }
+
+ fun loadMore(): Observable? {
+ previousSearch?.let {
+ return doSearch(it, previousCursor)
+ }
+ return null
+ }
+
+ fun cancelSearch() {
+ disposeIfPossible()
+ }
+
+ private fun doSearch(search: String, cursor: Int = 0): Observable {
+ resetResultsIfNeeded(search)
+ disposeIfPossible()
+ return unifiedSearchRepository.searchMessages(user, search, cursor)
+ .map { results ->
+ previousSearch = search
+ previousCursor = results.cursor
+ previousResults = previousResults + results.entries
+ MessageSearchResults(previousResults, results.hasMore)
+ }
+ .doOnSubscribe {
+ unifiedSearchDisposable = it
+ }
+ .doOnError { throwable ->
+ Log.e(TAG, "message search - ERROR", throwable)
+ resetCachedData()
+ disposeIfPossible()
+ }
+ .doOnComplete(this::disposeIfPossible)
+ }
+
+ private fun resetResultsIfNeeded(search: String) {
+ if (search != previousSearch) {
+ resetCachedData()
+ }
+ }
+
+ private fun resetCachedData() {
+ previousSearch = null
+ previousCursor = 0
+ previousResults = emptyList()
+ }
+
+ private fun disposeIfPossible() {
+ unifiedSearchDisposable?.dispose()
+ unifiedSearchDisposable = null
+ }
+
+ companion object {
+ private val TAG = MessageSearchHelper::class.simpleName
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt
index 50da11477..79d3489de 100644
--- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt
+++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt
@@ -22,6 +22,8 @@
package com.nextcloud.talk.dagger.modules
import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
+import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepositoryImpl
import com.nextcloud.talk.shareditems.repositories.SharedItemsRepository
import com.nextcloud.talk.shareditems.repositories.SharedItemsRepositoryImpl
import dagger.Module
@@ -33,4 +35,9 @@ class RepositoryModule {
fun provideSharedItemsRepository(ncApi: NcApi): SharedItemsRepository {
return SharedItemsRepositoryImpl(ncApi)
}
+
+ @Provides
+ fun provideUnifiedSearchRepository(ncApi: NcApi): UnifiedSearchRepository {
+ return UnifiedSearchRepositoryImpl(ncApi)
+ }
}
diff --git a/app/src/main/java/com/nextcloud/talk/models/domain/SearchMessageEntry.kt b/app/src/main/java/com/nextcloud/talk/models/domain/SearchMessageEntry.kt
new file mode 100644
index 000000000..2b3d233d4
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/models/domain/SearchMessageEntry.kt
@@ -0,0 +1,31 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.models.domain
+
+data class SearchMessageEntry(
+ val searchTerm: String,
+ val thumbnailURL: String?,
+ val title: String,
+ val messageExcerpt: String,
+ val conversationToken: String,
+ val messageId: String?
+)
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchEntry.kt b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchEntry.kt
new file mode 100644
index 000000000..0cfe1d256
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchEntry.kt
@@ -0,0 +1,48 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.models.json.unifiedsearch
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class UnifiedSearchEntry(
+ @JsonField(name = ["thumbnailUrl"])
+ var thumbnailUrl: String?,
+ @JsonField(name = ["title"])
+ var title: String?,
+ @JsonField(name = ["subline"])
+ var subline: String?,
+ @JsonField(name = ["resourceUrl"])
+ var resourceUrl: String?,
+ @JsonField(name = ["icon"])
+ var icon: String?,
+ @JsonField(name = ["rounded"])
+ var rounded: Boolean?,
+ @JsonField(name = ["attributes"])
+ var attributes: Map?,
+) : Parcelable {
+ constructor() : this(null, null, null, null, null, null, null)
+}
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOCS.kt b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOCS.kt
new file mode 100644
index 000000000..ba6e7a89a
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOCS.kt
@@ -0,0 +1,38 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.talk.models.json.unifiedsearch
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import com.nextcloud.talk.models.json.generic.GenericMeta
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class UnifiedSearchOCS(
+ @JsonField(name = ["meta"])
+ var meta: GenericMeta?,
+ @JsonField(name = ["data"])
+ var data: UnifiedSearchResponseData?
+) : Parcelable {
+ // Empty constructor needed for JsonObject
+ constructor() : this(null, null)
+}
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOverall.kt b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOverall.kt
new file mode 100644
index 000000000..d1db94f5e
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchOverall.kt
@@ -0,0 +1,35 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2022 Marcel Hibbe
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.nextcloud.talk.models.json.unifiedsearch
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class UnifiedSearchOverall(
+ @JsonField(name = ["ocs"])
+ var ocs: UnifiedSearchOCS?
+) : Parcelable {
+ // Empty constructor needed for JsonObject
+ constructor() : this(null)
+}
diff --git a/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchResponseData.kt b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchResponseData.kt
new file mode 100644
index 000000000..a05857d4b
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/models/json/unifiedsearch/UnifiedSearchResponseData.kt
@@ -0,0 +1,43 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.models.json.unifiedsearch
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.android.parcel.Parcelize
+
+@Parcelize
+@JsonObject
+data class UnifiedSearchResponseData(
+ @JsonField(name = ["name"])
+ var name: String?,
+ @JsonField(name = ["isPaginated"])
+ var paginated: Boolean?,
+ @JsonField(name = ["entries"])
+ var entries: List?,
+ @JsonField(name = ["cursor"])
+ var cursor: Int?
+) : Parcelable {
+ // empty constructor needed for JsonObject
+ constructor() : this(null, null, null, null)
+}
diff --git a/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepository.kt b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepository.kt
new file mode 100644
index 000000000..43a416395
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepository.kt
@@ -0,0 +1,26 @@
+package com.nextcloud.talk.repositories.unifiedsearch
+
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.models.domain.SearchMessageEntry
+import io.reactivex.Observable
+
+interface UnifiedSearchRepository {
+ data class UnifiedSearchResults(
+ val cursor: Int,
+ val hasMore: Boolean,
+ val entries: List
+ )
+
+ fun searchMessages(
+ userEntity: UserEntity,
+ searchTerm: String,
+ cursor: Int = 0,
+ limit: Int = DEFAULT_PAGE_SIZE
+ ): Observable>
+
+ fun searchInRoom(text: String, roomId: String): Observable>
+
+ companion object {
+ private const val DEFAULT_PAGE_SIZE = 5
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt
new file mode 100644
index 000000000..430aca3d4
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/repositories/unifiedsearch/UnifiedSearchRepositoryImpl.kt
@@ -0,0 +1,83 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.repositories.unifiedsearch
+
+import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.models.database.UserEntity
+import com.nextcloud.talk.models.domain.SearchMessageEntry
+import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchEntry
+import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchResponseData
+import com.nextcloud.talk.utils.ApiUtils
+import io.reactivex.Observable
+
+class UnifiedSearchRepositoryImpl(private val api: NcApi) : UnifiedSearchRepository {
+
+ override fun searchMessages(
+ userEntity: UserEntity,
+ searchTerm: String,
+ cursor: Int,
+ limit: Int
+ ): Observable> {
+ val apiObservable = api.performUnifiedSearch(
+ ApiUtils.getCredentials(userEntity.username, userEntity.token),
+ ApiUtils.getUrlForUnifiedSearch(userEntity.baseUrl, PROVIDER_TALK_MESSAGE),
+ searchTerm,
+ null,
+ limit,
+ cursor
+ )
+ return apiObservable.map { mapToMessageResults(it.ocs?.data!!, searchTerm, limit) }
+ }
+
+ override fun searchInRoom(text: String, roomId: String): Observable> {
+ TODO()
+ }
+
+ companion object {
+ private const val PROVIDER_TALK_MESSAGE = "talk-message"
+ private const val PROVIDER_TALK_MESSAGE_CURRENT = "talk-message-current"
+
+ private const val ATTRIBUTE_CONVERSATION = "conversation"
+ private const val ATTRIBUTE_MESSAGE_ID = "messageId"
+
+ private fun mapToMessageResults(data: UnifiedSearchResponseData, searchTerm: String, limit: Int):
+ UnifiedSearchRepository.UnifiedSearchResults {
+ val entries = data.entries?.map { it -> mapToMessage(it, searchTerm) }
+ val cursor = data.cursor ?: 0
+ val hasMore = entries?.size == limit
+ return UnifiedSearchRepository.UnifiedSearchResults(cursor, hasMore, entries ?: emptyList())
+ }
+
+ private fun mapToMessage(unifiedSearchEntry: UnifiedSearchEntry, searchTerm: String): SearchMessageEntry {
+ val conversation = unifiedSearchEntry.attributes?.get(ATTRIBUTE_CONVERSATION)!!
+ val messageId = unifiedSearchEntry.attributes?.get(ATTRIBUTE_MESSAGE_ID)
+ return SearchMessageEntry(
+ searchTerm,
+ unifiedSearchEntry.thumbnailUrl,
+ unifiedSearchEntry.title!!,
+ unifiedSearchEntry.subline!!,
+ conversation,
+ messageId
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
index 2199191f6..9ca98882d 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
@@ -32,10 +32,11 @@ import com.nextcloud.talk.models.RetrofitBucket;
import com.nextcloud.talk.models.database.CapabilitiesUtil;
import com.nextcloud.talk.models.database.UserEntity;
+import org.jetbrains.annotations.NotNull;
+
import java.util.HashMap;
import java.util.Map;
-import androidx.annotation.DimenRes;
import androidx.annotation.Nullable;
import okhttp3.Credentials;
@@ -456,4 +457,8 @@ public class ApiUtils {
return baseUrl + ocsApiVersion + spreedApiVersion + "/reaction/" + roomToken + "/" + messageId;
}
+ @NotNull
+ public static String getUrlForUnifiedSearch(@NotNull String baseUrl, @NotNull String providerId) {
+ return baseUrl + ocsApiVersion + "/search/providers/" + providerId + "/search";
+ }
}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/Debouncer.kt b/app/src/main/java/com/nextcloud/talk/utils/Debouncer.kt
new file mode 100644
index 000000000..b0ef01bb5
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/utils/Debouncer.kt
@@ -0,0 +1,34 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.utils
+
+import android.os.Handler
+import android.os.Looper
+
+class Debouncer(var delay: Long) {
+ private val handler = Handler(Looper.getMainLooper())
+
+ fun debounce(runnable: Runnable) {
+ handler.removeCallbacksAndMessages(null) // clear handler
+ handler.postDelayed(runnable, delay)
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
index 5b910ceed..b51b589e2 100644
--- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
+++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
@@ -36,6 +36,7 @@ import android.graphics.Typeface;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.VectorDrawable;
import android.net.Uri;
import android.os.Build;
@@ -592,6 +593,30 @@ public class DisplayUtils {
avatarImageView.setController(draweeController);
}
+ public static void loadAvatarPlaceholder(final SimpleDraweeView targetView) {
+ final Context context = targetView.getContext();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Drawable[] layers = new Drawable[2];
+ layers[0] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_background);
+ layers[1] = ContextCompat.getDrawable(context, R.drawable.ic_launcher_foreground);
+ LayerDrawable layerDrawable = new LayerDrawable(layers);
+
+ targetView.getHierarchy().setPlaceholderImage(
+ DisplayUtils.getRoundedDrawable(layerDrawable));
+ } else {
+ targetView.getHierarchy().setPlaceholderImage(R.mipmap.ic_launcher);
+ }
+ }
+
+ public static void loadImage(final SimpleDraweeView targetView, final ImageRequest imageRequest) {
+ final DraweeController newController = Fresco.newDraweeControllerBuilder()
+ .setOldController(targetView.getController())
+ .setAutoPlayAnimations(true)
+ .setImageRequest(imageRequest)
+ .build();
+ targetView.setController(newController);
+ }
+
public static @StringRes
int getSortOrderStringId(FileSortOrder sortOrder) {
switch (sortOrder.name) {
diff --git a/app/src/main/res/layout/rv_item_load_more.xml b/app/src/main/res/layout/rv_item_load_more.xml
new file mode 100644
index 000000000..d045da70b
--- /dev/null
+++ b/app/src/main/res/layout/rv_item_load_more.xml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/rv_item_search_message.xml b/app/src/main/res/layout/rv_item_search_message.xml
new file mode 100644
index 000000000..a8ec4aaaf
--- /dev/null
+++ b/app/src/main/res/layout/rv_item_search_message.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 83b3cceb4..4beee18ec 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -523,5 +523,7 @@
All
Send without notification
Call without notification
+ Messages
+ Load more results