mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-19 19:49:33 +01:00
Implement global message search
Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>
This commit is contained in:
parent
6718cf7663
commit
1d632f3c96
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<LoadMoreResultsItem.ViewHolder>(),
|
||||
IFilterable<String> {
|
||||
|
||||
// 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<IFlexible<RecyclerView.ViewHolder>>
|
||||
): ViewHolder = ViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>?
|
||||
) {
|
||||
// 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
|
||||
}
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<MessageResultItem.ViewHolder>(),
|
||||
IFilterable<String>,
|
||||
ISectionable<MessageResultItem.ViewHolder, GenericTextHeaderItem> {
|
||||
|
||||
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<IFlexible<RecyclerView.ViewHolder>>
|
||||
): ViewHolder = ViewHolder(view, adapter)
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>?
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
@ -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<ReactionsOverall> getReactions(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("reaction") String reaction);
|
||||
|
||||
// TODO use path params instead of passing URL
|
||||
@GET
|
||||
Observable<UnifiedSearchOverall> performUnifiedSearch(@Header("Authorization") String authorization,
|
||||
@Url String url,
|
||||
@Query("term") String term,
|
||||
@Query("from") String fromUrl,
|
||||
@Query("limit") Integer limit,
|
||||
@Query("cursor") Integer cursor);
|
||||
}
|
||||
|
@ -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<String, Status> 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<MessageSearchHelper.MessageSearchResults> 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<SearchMessageEntry> entries = results.getMessages();
|
||||
if (entries.size() > 0) {
|
||||
List<AbstractFlexibleItem> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<SearchMessageEntry>, val hasMore: Boolean)
|
||||
|
||||
private var unifiedSearchDisposable: Disposable? = null
|
||||
private var previousSearch: String? = null
|
||||
private var previousCursor: Int = 0
|
||||
private var previousResults: List<SearchMessageEntry> = emptyList()
|
||||
|
||||
fun startMessageSearch(search: String): Observable<MessageSearchResults> {
|
||||
return doSearch(search)
|
||||
}
|
||||
|
||||
fun loadMore(): Observable<MessageSearchResults>? {
|
||||
previousSearch?.let {
|
||||
return doSearch(it, previousCursor)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun cancelSearch() {
|
||||
disposeIfPossible()
|
||||
}
|
||||
|
||||
private fun doSearch(search: String, cursor: Int = 0): Observable<MessageSearchResults> {
|
||||
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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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?
|
||||
)
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String, String>?,
|
||||
) : Parcelable {
|
||||
constructor() : this(null, null, null, null, null, null, null)
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Marcel Hibbe
|
||||
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
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)
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Marcel Hibbe
|
||||
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
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)
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<UnifiedSearchEntry>?,
|
||||
@JsonField(name = ["cursor"])
|
||||
var cursor: Int?
|
||||
) : Parcelable {
|
||||
// empty constructor needed for JsonObject
|
||||
constructor() : this(null, null, null, null)
|
||||
}
|
@ -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<T>(
|
||||
val cursor: Int,
|
||||
val hasMore: Boolean,
|
||||
val entries: List<T>
|
||||
)
|
||||
|
||||
fun searchMessages(
|
||||
userEntity: UserEntity,
|
||||
searchTerm: String,
|
||||
cursor: Int = 0,
|
||||
limit: Int = DEFAULT_PAGE_SIZE
|
||||
): Observable<UnifiedSearchResults<SearchMessageEntry>>
|
||||
|
||||
fun searchInRoom(text: String, roomId: String): Observable<List<SearchMessageEntry>>
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_PAGE_SIZE = 5
|
||||
}
|
||||
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<UnifiedSearchRepository.UnifiedSearchResults<SearchMessageEntry>> {
|
||||
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<List<SearchMessageEntry>> {
|
||||
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<SearchMessageEntry> {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
34
app/src/main/java/com/nextcloud/talk/utils/Debouncer.kt
Normal file
34
app/src/main/java/com/nextcloud/talk/utils/Debouncer.kt
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
62
app/src/main/res/layout/rv_item_load_more.xml
Normal file
62
app/src/main/res/layout/rv_item_load_more.xml
Normal file
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Nextcloud Talk application
|
||||
~
|
||||
~ @author Mario Danic
|
||||
~ @author Andy Scherzinger
|
||||
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
~
|
||||
~ 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/>.
|
||||
~
|
||||
~
|
||||
~
|
||||
~ Adapted from https://github.com/stfalcon-studio/ChatKit/blob/master/chatkit/src/main/res/layout/item_dialog.xml
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/double_margin_between_elements"
|
||||
tools:background="@color/white">
|
||||
|
||||
<Space
|
||||
android:id="@+id/load_more_spacer"
|
||||
android:layout_width="@dimen/small_item_height"
|
||||
android:layout_height="@dimen/small_item_height"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:roundAsCircle="true" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/double_margin_between_elements"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/textColorMaxContrast"
|
||||
android:textSize="@dimen/two_line_primary_text_size"
|
||||
android:text="@string/load_more_results"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/load_more_spacer"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
80
app/src/main/res/layout/rv_item_search_message.xml
Normal file
80
app/src/main/res/layout/rv_item_search_message.xml
Normal file
@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Nextcloud Talk application
|
||||
~
|
||||
~ @author Mario Danic
|
||||
~ @author Andy Scherzinger
|
||||
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
~ Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||
~
|
||||
~ 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/>.
|
||||
~
|
||||
~
|
||||
~
|
||||
~ Adapted from https://github.com/stfalcon-studio/ChatKit/blob/master/chatkit/src/main/res/layout/item_dialog.xml
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/double_margin_between_elements"
|
||||
tools:background="@color/white">
|
||||
|
||||
<com.facebook.drawee.view.SimpleDraweeView
|
||||
android:id="@+id/thumbnail"
|
||||
android:layout_width="@dimen/small_item_height"
|
||||
android:layout_height="@dimen/small_item_height"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:roundAsCircle="true" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/conversation_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/double_margin_between_elements"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/conversation_item_header"
|
||||
android:textSize="@dimen/two_line_primary_text_size"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/thumbnail"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Message title goes here" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/message_excerpt"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="start|top"
|
||||
android:lines="1"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/textColorMaxContrast"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="@id/conversation_title"
|
||||
app:layout_constraintStart_toStartOf="@+id/conversation_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/conversation_title"
|
||||
tools:text="...this is a message result from unified search, which includes ellipses..." />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -523,5 +523,7 @@
|
||||
<string name="reactions_tab_all">All</string>
|
||||
<string name="send_without_notification">Send without notification</string>
|
||||
<string name="call_without_notification">Call without notification</string>
|
||||
<string name="messages">Messages</string>
|
||||
<string name="load_more_results">Load more results</string>
|
||||
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user