From 85c9a9050a4bfd3163ee3bfc777d8ae69ee655e0 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Mon, 26 Aug 2019 19:49:16 +0100 Subject: [PATCH] Add an invite button in the new conversation screen. --- res/drawable/ic_invite_24dp.xml | 5 + res/drawable/ic_invite_circle.xml | 14 + .../contact_selection_invite_action_item.xml | 17 + res/values/strings.xml | 1 + .../ContactSelectionListFragment.java | 110 +++++-- .../securesms/MediaDocumentsAdapter.java | 2 +- .../securesms/NewConversationActivity.java | 9 +- .../contacts/ContactSelectionListAdapter.java | 6 +- .../conversation/ConversationAdapter.java | 2 +- .../securesms/search/SearchListAdapter.java | 2 +- .../util/StickyHeaderDecoration.java | 4 +- .../util/adapter/FixedViewsAdapter.java | 67 ++++ .../RecyclerViewConcatenateAdapter.java | 292 ++++++++++++++++++ ...lerViewConcatenateAdapterStickyHeader.java | 61 ++++ 14 files changed, 550 insertions(+), 42 deletions(-) create mode 100644 res/drawable/ic_invite_24dp.xml create mode 100644 res/drawable/ic_invite_circle.xml create mode 100644 res/layout/contact_selection_invite_action_item.xml create mode 100644 src/org/thoughtcrime/securesms/util/adapter/FixedViewsAdapter.java create mode 100644 src/org/thoughtcrime/securesms/util/adapter/RecyclerViewConcatenateAdapter.java create mode 100644 src/org/thoughtcrime/securesms/util/adapter/RecyclerViewConcatenateAdapterStickyHeader.java diff --git a/res/drawable/ic_invite_24dp.xml b/res/drawable/ic_invite_24dp.xml new file mode 100644 index 0000000000..6cb119f1fa --- /dev/null +++ b/res/drawable/ic_invite_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_invite_circle.xml b/res/drawable/ic_invite_circle.xml new file mode 100644 index 0000000000..9f46e97c92 --- /dev/null +++ b/res/drawable/ic_invite_circle.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/res/layout/contact_selection_invite_action_item.xml b/res/layout/contact_selection_invite_action_item.xml new file mode 100644 index 0000000000..e8cfd20df2 --- /dev/null +++ b/res/layout/contact_selection_invite_action_item.xml @@ -0,0 +1,17 @@ + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 2be4080de9..797ede34e1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -933,6 +933,7 @@ Enter name or number + Invite to Signal Clear entered text diff --git a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 43d3c02c98..e1e95cd5aa 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -19,16 +19,10 @@ package org.thoughtcrime.securesms; import android.Manifest; import android.annotation.SuppressLint; +import android.content.Context; import android.database.Cursor; import android.os.AsyncTask; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.Loader; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -37,6 +31,15 @@ import android.widget.Button; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + import com.pnikosis.materialishprogress.ProgressWheel; import org.thoughtcrime.securesms.components.RecyclerViewFastScroller; @@ -44,7 +47,6 @@ import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter; import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode; -import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.permissions.Permissions; @@ -52,6 +54,8 @@ import org.thoughtcrime.securesms.util.DirectoryHelper; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ViewUtil; +import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter; +import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader; import java.io.IOException; import java.util.LinkedList; @@ -64,28 +68,41 @@ import java.util.Set; * @author Moxie Marlinspike * */ -public class ContactSelectionListFragment extends Fragment - implements LoaderManager.LoaderCallbacks +public final class ContactSelectionListFragment extends Fragment + implements LoaderManager.LoaderCallbacks { @SuppressWarnings("unused") - private static final String TAG = ContactSelectionListFragment.class.getSimpleName(); + private static final String TAG = Log.tag(ContactSelectionListFragment.class); public static final String DISPLAY_MODE = "display_mode"; public static final String MULTI_SELECT = "multi_select"; public static final String REFRESHABLE = "refreshable"; public static final String RECENTS = "recents"; - private TextView emptyText; - private Set selectedContacts; - private OnContactSelectedListener onContactSelectedListener; - private SwipeRefreshLayout swipeRefresh; - private View showContactsLayout; - private Button showContactsButton; - private TextView showContactsDescription; - private ProgressWheel showContactsProgress; - private String cursorFilter; - private RecyclerView recyclerView; - private RecyclerViewFastScroller fastScroller; + private TextView emptyText; + private Set selectedContacts; + private OnContactSelectedListener onContactSelectedListener; + private SwipeRefreshLayout swipeRefresh; + private View showContactsLayout; + private Button showContactsButton; + private TextView showContactsDescription; + private ProgressWheel showContactsProgress; + private String cursorFilter; + private RecyclerView recyclerView; + private RecyclerViewFastScroller fastScroller; + private ContactSelectionListAdapter cursorRecyclerViewAdapter; + + @Nullable private FixedViewsAdapter footerAdapter; + @Nullable private InviteCallback inviteCallback; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + if (context instanceof InviteCallback) { + inviteCallback = (InviteCallback) context; + } + } @Override public void onActivityCreated(Bundle icicle) { @@ -158,14 +175,31 @@ public class ContactSelectionListFragment extends Fragment } private void initializeCursor() { - ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(), - GlideApp.with(this), - null, - new ListClickListener(), - isMulti()); - selectedContacts = adapter.getSelectedContacts(); - recyclerView.setAdapter(adapter); - recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true, true)); + cursorRecyclerViewAdapter = new ContactSelectionListAdapter(requireContext(), + GlideApp.with(this), + null, + new ListClickListener(), + isMulti()); + selectedContacts = cursorRecyclerViewAdapter.getSelectedContacts(); + + RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader(); + + concatenateAdapter.addAdapter(cursorRecyclerViewAdapter); + if (inviteCallback != null) { + footerAdapter = new FixedViewsAdapter(createInviteActionView(inviteCallback)); + footerAdapter.hide(); + concatenateAdapter.addAdapter(footerAdapter); + } + + recyclerView.setAdapter(concatenateAdapter); + recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true)); + } + + private View createInviteActionView(@NonNull InviteCallback inviteCallback) { + View view = LayoutInflater.from(requireContext()) + .inflate(R.layout.contact_selection_invite_action_item, (ViewGroup) requireView(), false); + view.setOnClickListener(v -> inviteCallback.onInvite()); + return view; } private void initializeNoContactsPermission() { @@ -192,7 +226,7 @@ public class ContactSelectionListFragment extends Fragment public void setQueryFilter(String filter) { this.cursorFilter = filter; - this.getLoaderManager().restartLoader(0, null, this); + LoaderManager.getInstance(this).restartLoader(0, null, this); } public void resetQueryFilter() { @@ -224,9 +258,14 @@ public class ContactSelectionListFragment extends Fragment swipeRefresh.setVisibility(View.VISIBLE); showContactsLayout.setVisibility(View.GONE); - ((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(data); + cursorRecyclerViewAdapter.changeCursor(data); + + if (footerAdapter != null) { + footerAdapter.show(); + } + emptyText.setText(R.string.contact_selection_group_activity__no_contacts); - boolean useFastScroller = (recyclerView.getAdapter().getItemCount() > 20); + boolean useFastScroller = data.getCount() > 20; recyclerView.setVerticalScrollBarEnabled(!useFastScroller); if (useFastScroller) { fastScroller.setVisibility(View.VISIBLE); @@ -239,7 +278,7 @@ public class ContactSelectionListFragment extends Fragment @Override public void onLoaderReset(@NonNull Loader loader) { - ((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(null); + cursorRecyclerViewAdapter.changeCursor(null); fastScroller.setVisibility(View.GONE); } @@ -309,4 +348,7 @@ public class ContactSelectionListFragment extends Fragment void onContactDeselected(String number); } + public interface InviteCallback { + void onInvite(); + } } diff --git a/src/org/thoughtcrime/securesms/MediaDocumentsAdapter.java b/src/org/thoughtcrime/securesms/MediaDocumentsAdapter.java index e67d3b9d96..93cf95d869 100644 --- a/src/org/thoughtcrime/securesms/MediaDocumentsAdapter.java +++ b/src/org/thoughtcrime/securesms/MediaDocumentsAdapter.java @@ -93,7 +93,7 @@ public class MediaDocumentsAdapter extends CursorRecyclerViewAdapter } @Override - public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) { + public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) { return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.media_overview_document_item_header, parent, false)); } diff --git a/src/org/thoughtcrime/securesms/NewConversationActivity.java b/src/org/thoughtcrime/securesms/NewConversationActivity.java index f77b385d98..54c8729d11 100644 --- a/src/org/thoughtcrime/securesms/NewConversationActivity.java +++ b/src/org/thoughtcrime/securesms/NewConversationActivity.java @@ -35,7 +35,9 @@ import org.thoughtcrime.securesms.recipients.Recipient; * @author Moxie Marlinspike * */ -public class NewConversationActivity extends ContactSelectionActivity { +public class NewConversationActivity extends ContactSelectionActivity + implements ContactSelectionListFragment.InviteCallback +{ @SuppressWarnings("unused") private static final String TAG = NewConversationActivity.class.getSimpleName(); @@ -99,4 +101,9 @@ public class NewConversationActivity extends ContactSelectionActivity { super.onPrepareOptionsMenu(menu); return true; } + + @Override + public void onInvite() { + handleInvite(); + } } diff --git a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java index dfbdddea9e..21cb5ce536 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java @@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.components.RecyclerViewFastScroller.FastScroll import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.HeaderViewHolder; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter; import org.thoughtcrime.securesms.util.Util; @@ -53,7 +54,8 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter { - private final static String TAG = ContactSelectionListAdapter.class.getSimpleName(); + @SuppressWarnings("unused") + private final static String TAG = Log.tag(ContactSelectionListAdapter.class); private static final int VIEW_TYPE_CONTACT = 0; private static final int VIEW_TYPE_DIVIDER = 1; @@ -198,7 +200,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter } @Override - public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) { + public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) { return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.conversation_item_header, parent, false)); } diff --git a/src/org/thoughtcrime/securesms/search/SearchListAdapter.java b/src/org/thoughtcrime/securesms/search/SearchListAdapter.java index 68983c3b80..e5f2594da9 100644 --- a/src/org/thoughtcrime/securesms/search/SearchListAdapter.java +++ b/src/org/thoughtcrime/securesms/search/SearchListAdapter.java @@ -94,7 +94,7 @@ class SearchListAdapter extends RecyclerView.Adapter { + + private final List viewList; + + private boolean hidden; + + public FixedViewsAdapter(@NonNull View... viewList) { + this.viewList = Arrays.asList(viewList); + } + + @Override + public int getItemCount() { + return hidden ? 0 : viewList.size(); + } + + /** + * @return View type is the index. + */ + @Override + public int getItemViewType(int position) { + return position; + } + + /** + * @param viewType The index in the list of views. + */ + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + return new RecyclerView.ViewHolder(viewList.get(viewType)) { + }; + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + } + + @Override + public long getItemId(int position) { + return position; + } + + public void hide() { + setHidden(true); + } + + public void show() { + setHidden(false); + } + + private void setHidden(boolean hidden) { + if (this.hidden != hidden) { + this.hidden = hidden; + notifyDataSetChanged(); + } + } +} diff --git a/src/org/thoughtcrime/securesms/util/adapter/RecyclerViewConcatenateAdapter.java b/src/org/thoughtcrime/securesms/util/adapter/RecyclerViewConcatenateAdapter.java new file mode 100644 index 0000000000..c61bec40e2 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/adapter/RecyclerViewConcatenateAdapter.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2017 Martijn van der Woude + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Original source: https://github.com/martijnvdwoude/recycler-view-merge-adapter + * + * This file has been modified by Signal. + */ + +package org.thoughtcrime.securesms.util.adapter; + +import android.util.LongSparseArray; +import android.util.SparseIntArray; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.LinkedList; +import java.util.List; + +public class RecyclerViewConcatenateAdapter extends RecyclerView.Adapter { + + private final List adapters = new LinkedList<>(); + + private long nextUnassignedItemId; + + /** + * Map of global view type to local adapter. + *

+ * Not the same as {@link #adapters}, it may have duplicates and may be in a different order. + */ + private final List viewTypes = new LinkedList<>(); + + /** Observes a single sub adapter and maps the positions on the events to global positions. */ + private static class AdapterDataObserver extends RecyclerView.AdapterDataObserver { + + private final RecyclerViewConcatenateAdapter mergeAdapter; + private final RecyclerView.Adapter adapter; + + AdapterDataObserver(RecyclerViewConcatenateAdapter mergeAdapter, RecyclerView.Adapter adapter) { + this.mergeAdapter = mergeAdapter; + this.adapter = adapter; + } + + @Override + public void onChanged() { + mergeAdapter.notifyDataSetChanged(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + int subAdapterOffset = mergeAdapter.getSubAdapterFirstGlobalPosition(adapter); + + mergeAdapter.notifyItemRangeChanged(subAdapterOffset + positionStart, itemCount); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + int subAdapterOffset = mergeAdapter.getSubAdapterFirstGlobalPosition(adapter); + + mergeAdapter.notifyItemRangeInserted(subAdapterOffset + positionStart, itemCount); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + int subAdapterOffset = mergeAdapter.getSubAdapterFirstGlobalPosition(adapter); + + mergeAdapter.notifyItemRangeRemoved(subAdapterOffset + positionStart, itemCount); + } + } + + private static class ChildAdapter { + + final RecyclerView.Adapter adapter; + + /** Map of global view types to local view types */ + private final SparseIntArray globalViewTypesMap = new SparseIntArray(); + + /** Map of local view types to global view types */ + private final SparseIntArray localViewTypesMap = new SparseIntArray(); + + private final AdapterDataObserver adapterDataObserver; + + /** Map of local ids to global ids. */ + private final LongSparseArray localItemIdMap = new LongSparseArray<>(); + + ChildAdapter(@NonNull RecyclerView.Adapter adapter, @NonNull AdapterDataObserver adapterDataObserver) { + this.adapter = adapter; + this.adapterDataObserver = adapterDataObserver; + + this.adapter.registerAdapterDataObserver(this.adapterDataObserver); + } + + int getGlobalItemViewType(int localPosition, int defaultValue) { + int localViewType = adapter.getItemViewType(localPosition); + int globalViewType = localViewTypesMap.get(localViewType, defaultValue); + + if (globalViewType == defaultValue) { + globalViewTypesMap.append(globalViewType, localViewType); + localViewTypesMap.append(localViewType, globalViewType); + } + + return globalViewType; + } + + long getGlobalItemId(int localPosition, long defaultGlobalValue) { + final long localItemId = adapter.getItemId(localPosition); + + if (RecyclerView.NO_ID == localItemId) { + return RecyclerView.NO_ID; + } + + final Long globalItemId = localItemIdMap.get(localItemId); + + if (globalItemId == null) { + localItemIdMap.put(localItemId, defaultGlobalValue); + return defaultGlobalValue; + } + + return globalItemId; + } + + void unregister() { + adapter.unregisterAdapterDataObserver(adapterDataObserver); + } + + RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int globalViewType) { + int localViewType = globalViewTypesMap.get(globalViewType); + + return adapter.onCreateViewHolder(viewGroup, localViewType); + } + } + + static class ChildAdapterPositionPair { + + final ChildAdapter childAdapter; + final int localPosition; + + ChildAdapterPositionPair(@NonNull ChildAdapter adapter, int position) { + childAdapter = adapter; + localPosition = position; + } + + RecyclerView.Adapter getAdapter() { + return childAdapter.adapter; + } + } + + /** + * @param adapter Append an adapter to the list of adapters. + */ + public void addAdapter(@NonNull RecyclerView.Adapter adapter) { + addAdapter(adapters.size(), adapter); + } + + /** + * @param index The index at which to add an adapter to the list of adapters. + * @param adapter The adapter to add. + */ + public void addAdapter(int index, @NonNull RecyclerView.Adapter adapter) { + AdapterDataObserver adapterDataObserver = new AdapterDataObserver(this, adapter); + adapters.add(index, new ChildAdapter(adapter, adapterDataObserver)); + notifyDataSetChanged(); + } + + /** + * Clear all adapters from the list of adapters. + */ + public void clearAdapters() { + for (ChildAdapter childAdapter : adapters) { + childAdapter.unregister(); + } + + adapters.clear(); + notifyDataSetChanged(); + } + + /** + * Return a childAdapterPositionPair object for a given global position. + * + * @param globalPosition The global position in the entire set of items. + * @return A childAdapterPositionPair object containing a reference to the adapter and the local + * position in that adapter that corresponds to the given global position. + */ + @NonNull + ChildAdapterPositionPair getLocalPosition(final int globalPosition) { + int count = 0; + + for (ChildAdapter childAdapter : adapters) { + int newCount = count + childAdapter.adapter.getItemCount(); + + if (globalPosition < newCount) { + return new ChildAdapterPositionPair(childAdapter, globalPosition - count); + } + + count = newCount; + } + + throw new AssertionError("Position out of range"); + } + + @Override + @NonNull + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + ChildAdapter childAdapter = viewTypes.get(viewType); + if (childAdapter == null) { + throw new AssertionError("Unknown view type"); + } + + return childAdapter.onCreateViewHolder(viewGroup, viewType); + } + + /** + * Return the first global position in the entire set of items for a given adapter. + * + * @param adapter The adapter for which to the return the first global position. + * @return The first global position for the given adapter, or -1 if no such position could be found. + */ + private int getSubAdapterFirstGlobalPosition(@NonNull RecyclerView.Adapter adapter) { + int count = 0; + + for (ChildAdapter childAdapterWrapper : adapters) { + RecyclerView.Adapter childAdapter = childAdapterWrapper.adapter; + + if (childAdapter == adapter) { + return count; + } + + count += childAdapter.getItemCount(); + } + + throw new AssertionError("Adapter not found in list of adapters"); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + ChildAdapterPositionPair childAdapterPositionPair = getLocalPosition(position); + RecyclerView.Adapter adapter = childAdapterPositionPair.getAdapter(); + //noinspection unchecked + adapter.onBindViewHolder(viewHolder, childAdapterPositionPair.localPosition); + } + + @Override + public int getItemViewType(int position) { + int nextUnassignedViewType = viewTypes.size(); + ChildAdapterPositionPair localPosition = getLocalPosition(position); + + int viewType = localPosition.childAdapter.getGlobalItemViewType(localPosition.localPosition, nextUnassignedViewType); + + if (viewType == nextUnassignedViewType) { + viewTypes.add(viewType, localPosition.childAdapter); + } + + return viewType; + } + + @Override + public long getItemId(int position) { + ChildAdapterPositionPair localPosition = getLocalPosition(position); + + long itemId = localPosition.childAdapter.getGlobalItemId(localPosition.localPosition, nextUnassignedItemId); + + if (itemId == nextUnassignedItemId) { + nextUnassignedItemId++; + } + + return itemId; + } + + @Override + public int getItemCount() { + int count = 0; + + for (ChildAdapter adapter : adapters) { + count += adapter.adapter.getItemCount(); + } + + return count; + } +} diff --git a/src/org/thoughtcrime/securesms/util/adapter/RecyclerViewConcatenateAdapterStickyHeader.java b/src/org/thoughtcrime/securesms/util/adapter/RecyclerViewConcatenateAdapterStickyHeader.java new file mode 100644 index 0000000000..e6152507a9 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/adapter/RecyclerViewConcatenateAdapterStickyHeader.java @@ -0,0 +1,61 @@ +package org.thoughtcrime.securesms.util.adapter; + +import android.view.ViewGroup; + +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.components.RecyclerViewFastScroller; +import org.thoughtcrime.securesms.util.StickyHeaderDecoration; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.libsignal.util.guava.Optional; + +public final class RecyclerViewConcatenateAdapterStickyHeader extends RecyclerViewConcatenateAdapter + implements StickyHeaderDecoration.StickyHeaderAdapter, + RecyclerViewFastScroller.FastScrollAdapter +{ + + @Override + public long getHeaderId(int position) { + return getForPosition(position).transform(p -> p.first().getHeaderId(p.second())).or(-1L); + } + + @Override + public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) { + return getForPosition(position).transform(p -> p.first().onCreateHeaderViewHolder(parent, p.second())).orNull(); + } + + @Override + public void onBindHeaderViewHolder(RecyclerView.ViewHolder viewHolder, int position) { + Optional> forPosition = getForPosition(position); + + if (forPosition.isPresent()) { + Pair stickyHeaderAdapterIntegerPair = forPosition.get(); + //noinspection unchecked + stickyHeaderAdapterIntegerPair.first().onBindHeaderViewHolder(viewHolder, stickyHeaderAdapterIntegerPair.second()); + } + } + + @Override + public CharSequence getBubbleText(int position) { + Optional> forPosition = getForPosition(position); + + return forPosition.transform(a -> { + if (a.first() instanceof RecyclerViewFastScroller.FastScrollAdapter) { + return ((RecyclerViewFastScroller.FastScrollAdapter) a.first()).getBubbleText(a.second()); + } else { + return ""; + } + }).or(""); + } + + private Optional> getForPosition(int position) { + ChildAdapterPositionPair localAdapterPosition = getLocalPosition(position); + RecyclerView.Adapter adapter = localAdapterPosition.getAdapter(); + + if (adapter instanceof StickyHeaderDecoration.StickyHeaderAdapter) { + StickyHeaderDecoration.StickyHeaderAdapter sticky = (StickyHeaderDecoration.StickyHeaderAdapter) adapter; + return Optional.of(new Pair<>(sticky, localAdapterPosition.localPosition)); + } + return Optional.absent(); + } +}