Add chat filter support behind a flag.
This commit is contained in:
parent
3e2ecdaaa9
commit
bba1315906
26 changed files with 681 additions and 237 deletions
|
@ -0,0 +1,26 @@
|
|||
package org.thoughtcrime.securesms.conversationlist
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
|
||||
class ConversationFilterBehavior(context: Context, attributeSet: AttributeSet) : AppBarLayout.Behavior(context, attributeSet) {
|
||||
|
||||
override fun onStartNestedScroll(parent: CoordinatorLayout, child: AppBarLayout, directTargetChild: View, target: View, nestedScrollAxes: Int, type: Int): Boolean {
|
||||
if (type == ViewCompat.TYPE_NON_TOUCH || !FeatureFlags.chatFilters()) {
|
||||
return false
|
||||
} else {
|
||||
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, child: AppBarLayout, target: View, type: Int) {
|
||||
super.onStopNestedScroll(coordinatorLayout, child, target, type)
|
||||
child.setExpanded(false, true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.thoughtcrime.securesms.conversationlist
|
||||
|
||||
/**
|
||||
* Small state machine that describes moving and triggering actions
|
||||
* based off pulling down the conversation filter.
|
||||
*/
|
||||
enum class ConversationFilterLatch {
|
||||
SET,
|
||||
RESET;
|
||||
}
|
|
@ -30,10 +30,13 @@ import java.util.Set;
|
|||
|
||||
class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.ViewHolder> {
|
||||
|
||||
private static final int TYPE_THREAD = 1;
|
||||
private static final int TYPE_ACTION = 2;
|
||||
private static final int TYPE_PLACEHOLDER = 3;
|
||||
private static final int TYPE_HEADER = 4;
|
||||
private static final int TYPE_THREAD = 1;
|
||||
private static final int TYPE_ACTION = 2;
|
||||
private static final int TYPE_PLACEHOLDER = 3;
|
||||
private static final int TYPE_HEADER = 4;
|
||||
private static final int TYPE_EMPTY = 5;
|
||||
private static final int TYPE_CLEAR_FILTER_FOOTER = 6;
|
||||
private static final int TYPE_CLEAR_FILTER_EMPTY = 7;
|
||||
|
||||
private enum Payload {
|
||||
TYPING_INDICATOR,
|
||||
|
@ -43,6 +46,7 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
private final LifecycleOwner lifecycleOwner;
|
||||
private final GlideRequests glideRequests;
|
||||
private final OnConversationClickListener onConversationClickListener;
|
||||
private final OnClearFilterClickListener onClearFilterClicked;
|
||||
private ConversationSet selectedConversations = new ConversationSet();
|
||||
private final Set<Long> typingSet = new HashSet<>();
|
||||
|
||||
|
@ -50,13 +54,15 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
|
||||
protected ConversationListAdapter(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull OnConversationClickListener onConversationClickListener)
|
||||
@NonNull OnConversationClickListener onConversationClickListener,
|
||||
@NonNull OnClearFilterClickListener onClearFilterClicked)
|
||||
{
|
||||
super(new ConversationDiffCallback());
|
||||
|
||||
this.lifecycleOwner = lifecycleOwner;
|
||||
this.glideRequests = glideRequests;
|
||||
this.onConversationClickListener = onConversationClickListener;
|
||||
this.onClearFilterClicked = onClearFilterClicked;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -101,6 +107,15 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
} else if (viewType == TYPE_HEADER) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.dsl_section_header, parent, false);
|
||||
return new HeaderViewHolder(v);
|
||||
} else if (viewType == TYPE_EMPTY) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_list_empty_state, parent, false);
|
||||
return new HeaderViewHolder(v);
|
||||
} else if (viewType == TYPE_CLEAR_FILTER_FOOTER) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_list_item_clear_filter, parent, false);
|
||||
return new ClearFilterViewHolder(v, onClearFilterClicked);
|
||||
} else if (viewType == TYPE_CLEAR_FILTER_EMPTY) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_list_item_clear_filter_empty, parent, false);
|
||||
return new ClearFilterViewHolder(v, onClearFilterClicked);
|
||||
} else {
|
||||
throw new IllegalStateException("Unknown type! " + viewType);
|
||||
}
|
||||
|
@ -197,8 +212,14 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
return TYPE_HEADER;
|
||||
case ARCHIVED_FOOTER:
|
||||
return TYPE_ACTION;
|
||||
case CONVERSATION_FILTER_FOOTER:
|
||||
return TYPE_CLEAR_FILTER_FOOTER;
|
||||
case CONVERSATION_FILTER_EMPTY:
|
||||
return TYPE_CLEAR_FILTER_EMPTY;
|
||||
case THREAD:
|
||||
return TYPE_THREAD;
|
||||
case EMPTY:
|
||||
return TYPE_EMPTY;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
@ -247,9 +268,22 @@ class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.Vie
|
|||
}
|
||||
}
|
||||
|
||||
static class ClearFilterViewHolder extends RecyclerView.ViewHolder {
|
||||
ClearFilterViewHolder(@NonNull View itemView, OnClearFilterClickListener listener) {
|
||||
super(itemView);
|
||||
itemView.findViewById(R.id.clear_filter).setOnClickListener(v -> {
|
||||
listener.onClearFilterClick();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface OnConversationClickListener {
|
||||
void onConversationClick(@NonNull Conversation conversation);
|
||||
boolean onConversationLongClick(@NonNull Conversation conversation, @NonNull View view);
|
||||
void onShowArchiveClick();
|
||||
}
|
||||
|
||||
interface OnClearFilterClickListener {
|
||||
void onClearFilterClick();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MergeCursor;
|
||||
|
@ -9,9 +8,11 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.paging.PagedDataSource;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
|
@ -22,9 +23,9 @@ import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -35,15 +36,17 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
|
|||
|
||||
private static final String TAG = Log.tag(ConversationListDataSource.class);
|
||||
|
||||
protected final ThreadDatabase threadDatabase;
|
||||
protected final ThreadDatabase threadDatabase;
|
||||
protected final ConversationFilter conversationFilter;
|
||||
|
||||
protected ConversationListDataSource(@NonNull Context context) {
|
||||
this.threadDatabase = SignalDatabase.threads();
|
||||
protected ConversationListDataSource(@NonNull ConversationFilter conversationFilter) {
|
||||
this.threadDatabase = SignalDatabase.threads();
|
||||
this.conversationFilter = conversationFilter;
|
||||
}
|
||||
|
||||
public static ConversationListDataSource create(@NonNull Context context, boolean isArchived) {
|
||||
if (!isArchived) return new UnarchivedConversationListDataSource(context);
|
||||
else return new ArchivedConversationListDataSource(context);
|
||||
public static ConversationListDataSource create(@NonNull ConversationFilter conversationFilter, boolean isArchived) {
|
||||
if (!isArchived) return new UnarchivedConversationListDataSource(conversationFilter);
|
||||
else return new ArchivedConversationListDataSource(conversationFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -51,13 +54,17 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
|
|||
long startTime = System.currentTimeMillis();
|
||||
int count = getTotalCount();
|
||||
|
||||
Log.d(TAG, "[size(), " + getClass().getSimpleName() + "] " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
return count;
|
||||
if (conversationFilter != ConversationFilter.OFF) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
Log.d(TAG, "[size(), " + getClass().getSimpleName() + ", " + conversationFilter + "] " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
return Math.max(1, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Conversation> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
|
||||
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), " + getClass().getSimpleName());
|
||||
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), " + getClass().getSimpleName() + ", " + conversationFilter);
|
||||
|
||||
List<Conversation> conversations = new ArrayList<>(length);
|
||||
List<Recipient> recipients = new LinkedList<>();
|
||||
|
@ -89,7 +96,15 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
|
|||
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
return conversations;
|
||||
if (conversations.isEmpty() && start == 0 && length == 1) {
|
||||
if (conversationFilter == ConversationFilter.OFF) {
|
||||
return Collections.singletonList(new Conversation(ConversationReader.buildThreadRecordForType(Conversation.Type.EMPTY, 0)));
|
||||
} else {
|
||||
return Collections.singletonList(new Conversation(ConversationReader.buildThreadRecordForType(Conversation.Type.CONVERSATION_FILTER_EMPTY, 0)));
|
||||
}
|
||||
} else {
|
||||
return conversations;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -107,18 +122,31 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
|
|||
|
||||
private static class ArchivedConversationListDataSource extends ConversationListDataSource {
|
||||
|
||||
ArchivedConversationListDataSource(@NonNull Context context) {
|
||||
super(context);
|
||||
private int totalCount;
|
||||
|
||||
ArchivedConversationListDataSource(@NonNull ConversationFilter conversationFilter) {
|
||||
super(conversationFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTotalCount() {
|
||||
return threadDatabase.getArchivedConversationListCount();
|
||||
totalCount = threadDatabase.getArchivedConversationListCount(conversationFilter);
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor(long offset, long limit) {
|
||||
return threadDatabase.getArchivedConversationList(offset, limit);
|
||||
List<Cursor> cursors = new ArrayList<>(2);
|
||||
Cursor cursor = threadDatabase.getArchivedConversationList(conversationFilter, offset, limit);
|
||||
|
||||
cursors.add(cursor);
|
||||
if (offset + limit >= totalCount && totalCount > 0 && conversationFilter != ConversationFilter.OFF) {
|
||||
MatrixCursor conversationFilterFooter = new MatrixCursor(ConversationReader.HEADER_COLUMN);
|
||||
conversationFilterFooter.addRow(ConversationReader.CONVERSATION_FILTER_FOOTER);
|
||||
cursors.add(conversationFilterFooter);
|
||||
}
|
||||
|
||||
return new MergeCursor(cursors.toArray(new Cursor[]{}));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,16 +158,16 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
|
|||
private int archivedCount;
|
||||
private int unpinnedCount;
|
||||
|
||||
UnarchivedConversationListDataSource(@NonNull Context context) {
|
||||
super(context);
|
||||
UnarchivedConversationListDataSource(@NonNull ConversationFilter conversationFilter) {
|
||||
super(conversationFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTotalCount() {
|
||||
int unarchivedCount = threadDatabase.getUnarchivedConversationListCount();
|
||||
int unarchivedCount = threadDatabase.getUnarchivedConversationListCount(conversationFilter);
|
||||
|
||||
pinnedCount = threadDatabase.getPinnedConversationListCount();
|
||||
archivedCount = threadDatabase.getArchivedConversationListCount();
|
||||
pinnedCount = threadDatabase.getPinnedConversationListCount(conversationFilter);
|
||||
archivedCount = threadDatabase.getArchivedConversationListCount(conversationFilter);
|
||||
unpinnedCount = unarchivedCount - pinnedCount;
|
||||
totalCount = unarchivedCount;
|
||||
|
||||
|
@ -170,7 +198,7 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
|
|||
limit--;
|
||||
}
|
||||
|
||||
Cursor pinnedCursor = threadDatabase.getUnarchivedConversationList(true, offset, limit);
|
||||
Cursor pinnedCursor = threadDatabase.getUnarchivedConversationList(conversationFilter, true, offset, limit);
|
||||
cursors.add(pinnedCursor);
|
||||
limit -= pinnedCursor.getCount();
|
||||
|
||||
|
@ -182,15 +210,23 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
|
|||
}
|
||||
|
||||
long unpinnedOffset = Math.max(0, offset - pinnedCount - getHeaderOffset());
|
||||
Cursor unpinnedCursor = threadDatabase.getUnarchivedConversationList(false, unpinnedOffset, limit);
|
||||
Cursor unpinnedCursor = threadDatabase.getUnarchivedConversationList(conversationFilter, false, unpinnedOffset, limit);
|
||||
cursors.add(unpinnedCursor);
|
||||
|
||||
if (offset + originalLimit >= totalCount && hasArchivedFooter()) {
|
||||
boolean shouldInsertConversationFilterFooter = offset + originalLimit >= totalCount && hasConversationFilterFooter();
|
||||
boolean shouldInsertArchivedFooter = offset + originalLimit >= totalCount - (shouldInsertConversationFilterFooter ? 1 : 0) && hasArchivedFooter();
|
||||
if (shouldInsertArchivedFooter) {
|
||||
MatrixCursor archivedFooterCursor = new MatrixCursor(ConversationReader.ARCHIVED_COLUMNS);
|
||||
archivedFooterCursor.addRow(ConversationReader.createArchivedFooterRow(archivedCount));
|
||||
cursors.add(archivedFooterCursor);
|
||||
}
|
||||
|
||||
if (shouldInsertConversationFilterFooter) {
|
||||
MatrixCursor conversationFilterFooter = new MatrixCursor(ConversationReader.HEADER_COLUMN);
|
||||
conversationFilterFooter.addRow(ConversationReader.CONVERSATION_FILTER_FOOTER);
|
||||
cursors.add(conversationFilterFooter);
|
||||
}
|
||||
|
||||
return new MergeCursor(cursors.toArray(new Cursor[]{}));
|
||||
}
|
||||
|
||||
|
@ -213,5 +249,9 @@ abstract class ConversationListDataSource implements PagedDataSource<Long, Conve
|
|||
boolean hasArchivedFooter() {
|
||||
return archivedCount != 0;
|
||||
}
|
||||
|
||||
boolean hasConversationFilterFooter() {
|
||||
return totalCount > 1 && conversationFilter != ConversationFilter.OFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package org.thoughtcrime.securesms.conversationlist
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.util.AttributeSet
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.databinding.ConversationListFilterPullViewBinding
|
||||
|
||||
/**
|
||||
* Encapsulates the push / pull latch for enabling and disabling
|
||||
* filters into a convenient view.
|
||||
*/
|
||||
class ConversationListFilterPullView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : FrameLayout(context, attrs) {
|
||||
|
||||
private val colorPull = ContextCompat.getColor(context, R.color.signal_colorSurface1)
|
||||
private val colorRelease = ContextCompat.getColor(context, R.color.signal_colorSecondaryContainer)
|
||||
private var state: State = State.PULL
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.conversation_list_filter_pull_view, this)
|
||||
setBackgroundColor(colorPull)
|
||||
}
|
||||
|
||||
private val binding = ConversationListFilterPullViewBinding.bind(this)
|
||||
|
||||
fun setToPull() {
|
||||
if (state == State.PULL) {
|
||||
return
|
||||
}
|
||||
|
||||
state = State.PULL
|
||||
setBackgroundColor(colorPull)
|
||||
binding.arrow.setImageResource(R.drawable.ic_arrow_down)
|
||||
binding.text.setText(R.string.ConversationListFilterPullView__pull_down_to_filter)
|
||||
}
|
||||
|
||||
fun setToRelease() {
|
||||
if (state == State.RELEASE) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Settings.System.getInt(context.contentResolver, Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
||||
performHapticFeedback(if (Build.VERSION.SDK_INT >= 30) HapticFeedbackConstants.CONFIRM else HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
}
|
||||
|
||||
state = State.RELEASE
|
||||
setBackgroundColor(colorRelease)
|
||||
binding.arrow.setImageResource(R.drawable.ic_arrow_up_16)
|
||||
binding.text.setText(R.string.ConversationListFilterPullView__release_to_filter)
|
||||
}
|
||||
|
||||
enum class State {
|
||||
RELEASE,
|
||||
PULL
|
||||
}
|
||||
}
|
|
@ -32,6 +32,8 @@ import android.net.Uri;
|
|||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
@ -40,7 +42,6 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
|
@ -68,6 +69,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import com.airbnb.lottie.SimpleColorFilter;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.material.animation.ArgbEvaluatorCompat;
|
||||
import com.google.android.material.appbar.AppBarLayout;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
|
@ -136,8 +138,6 @@ import org.thoughtcrime.securesms.mms.GlideApp;
|
|||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
|
||||
import org.thoughtcrime.securesms.payments.preferences.details.PaymentDetailsFragmentArgs;
|
||||
import org.thoughtcrime.securesms.payments.preferences.details.PaymentDetailsParcelable;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -191,7 +191,7 @@ import static android.app.Activity.RESULT_OK;
|
|||
public class ConversationListFragment extends MainFragment implements ActionMode.Callback,
|
||||
ConversationListAdapter.OnConversationClickListener,
|
||||
ConversationListSearchAdapter.EventListener,
|
||||
MegaphoneActionController
|
||||
MegaphoneActionController, ConversationListAdapter.OnClearFilterClickListener
|
||||
{
|
||||
public static final short MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME = 32562;
|
||||
public static final short SMS_ROLE_REQUEST_CODE = 32563;
|
||||
|
@ -207,8 +207,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
private RecyclerView list;
|
||||
private Stub<ReminderView> reminderView;
|
||||
private Stub<UnreadPaymentsView> paymentNotificationView;
|
||||
private Stub<ViewGroup> emptyState;
|
||||
private TextView searchEmptyState;
|
||||
private PulsingFloatingActionButton fab;
|
||||
private PulsingFloatingActionButton cameraFab;
|
||||
private ConversationListViewModel viewModel;
|
||||
|
@ -263,10 +261,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
coordinator = view.findViewById(R.id.coordinator);
|
||||
list = view.findViewById(R.id.list);
|
||||
searchEmptyState = view.findViewById(R.id.search_no_results);
|
||||
bottomActionBar = view.findViewById(R.id.conversation_list_bottom_action_bar);
|
||||
reminderView = new Stub<>(view.findViewById(R.id.reminder));
|
||||
emptyState = new Stub<>(view.findViewById(R.id.empty_state));
|
||||
megaphoneContainer = new Stub<>(view.findViewById(R.id.megaphone_container));
|
||||
paymentNotificationView = new Stub<>(view.findViewById(R.id.payments_notification));
|
||||
voiceNotePlayerViewStub = new Stub<>(view.findViewById(R.id.voice_note_player));
|
||||
|
@ -276,6 +272,19 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
fab.setVisibility(View.VISIBLE);
|
||||
cameraFab.setVisibility(View.VISIBLE);
|
||||
|
||||
ConversationListFilterPullView pullView = view.findViewById(R.id.pull_view);
|
||||
|
||||
AppBarLayout appBarLayout = view.findViewById(R.id.recycler_coordinator_app_bar);
|
||||
appBarLayout.addOnOffsetChangedListener((layout, verticalOffset) -> {
|
||||
if (verticalOffset == 0) {
|
||||
viewModel.setConversationFilterLatch(ConversationFilterLatch.SET);
|
||||
pullView.setToRelease();
|
||||
} else if (verticalOffset == -layout.getHeight()) {
|
||||
viewModel.setConversationFilterLatch(ConversationFilterLatch.RESET);
|
||||
pullView.setToPull();
|
||||
}
|
||||
});
|
||||
|
||||
fab.show();
|
||||
cameraFab.show();
|
||||
|
||||
|
@ -345,10 +354,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
public void onDestroyView() {
|
||||
coordinator = null;
|
||||
list = null;
|
||||
searchEmptyState = null;
|
||||
bottomActionBar = null;
|
||||
reminderView = null;
|
||||
emptyState = null;
|
||||
megaphoneContainer = null;
|
||||
paymentNotificationView = null;
|
||||
voiceNotePlayerViewStub = null;
|
||||
|
@ -486,6 +493,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
handleInsights(); return true;
|
||||
case R.id.menu_notification_profile:
|
||||
handleNotificationProfile(); return true;
|
||||
case R.id.menu_filter_unread_chats:
|
||||
handleFilterUnreadChats(); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -691,7 +700,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
|
||||
|
||||
private void initializeListAdapters() {
|
||||
defaultAdapter = new ConversationListAdapter(getViewLifecycleOwner(), GlideApp.with(this), this);
|
||||
defaultAdapter = new ConversationListAdapter(getViewLifecycleOwner(), GlideApp.with(this), this, this);
|
||||
searchAdapter = new ConversationListSearchAdapter(getViewLifecycleOwner(), GlideApp.with(this), this, Locale.getDefault());
|
||||
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false, 0);
|
||||
|
||||
|
@ -727,7 +736,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
}
|
||||
|
||||
if (adapter instanceof ConversationListAdapter) {
|
||||
((ConversationListAdapter) adapter).setPagingController(viewModel.getPagingController());
|
||||
viewModel.getPagingController()
|
||||
.observe(getViewLifecycleOwner(),
|
||||
controller -> ((ConversationListAdapter) adapter).setPagingController(controller));
|
||||
}
|
||||
|
||||
list.setAdapter(adapter);
|
||||
|
@ -826,13 +837,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
private void onSearchResultChanged(@Nullable SearchResult result) {
|
||||
result = result != null ? result : SearchResult.EMPTY;
|
||||
searchAdapter.updateResults(result);
|
||||
|
||||
if (result.isEmpty() && activeAdapter == searchAdapter) {
|
||||
searchEmptyState.setText(getString(R.string.SearchFragment_no_results, result.getQuery()));
|
||||
searchEmptyState.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
searchEmptyState.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onMegaphoneChanged(@Nullable Megaphone megaphone) {
|
||||
|
@ -966,6 +970,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
NotificationProfileSelectionFragment.show(getParentFragmentManager());
|
||||
}
|
||||
|
||||
private void handleFilterUnreadChats() {
|
||||
viewModel.toggleUnreadChatsFilter();
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleArchive(@NonNull Collection<Long> ids, boolean showProgress) {
|
||||
Set<Long> selectedConversations = new HashSet<>(ids);
|
||||
|
@ -1173,21 +1181,14 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
void updateEmptyState(boolean isConversationEmpty) {
|
||||
if (isConversationEmpty) {
|
||||
Log.i(TAG, "Received an empty data set.");
|
||||
list.setVisibility(View.INVISIBLE);
|
||||
emptyState.get().setVisibility(View.VISIBLE);
|
||||
fab.startPulse(3 * 1000);
|
||||
cameraFab.startPulse(3 * 1000);
|
||||
|
||||
SignalStore.onboarding().setShowNewGroup(true);
|
||||
SignalStore.onboarding().setShowInviteFriends(true);
|
||||
} else {
|
||||
list.setVisibility(View.VISIBLE);
|
||||
fab.stopPulse();
|
||||
cameraFab.stopPulse();
|
||||
|
||||
if (emptyState.resolved()) {
|
||||
emptyState.get().setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1456,6 +1457,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
}.executeOnExecutor(SignalExecutors.BOUNDED, threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClearFilterClick() {
|
||||
viewModel.toggleUnreadChatsFilter();
|
||||
}
|
||||
|
||||
private class PaymentNotificationListener implements UnreadPaymentsView.Listener {
|
||||
|
||||
private final UnreadPayments unreadPayments;
|
||||
|
|
|
@ -22,9 +22,12 @@ import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
|||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
class ConversationListSearchAdapter extends RecyclerView.Adapter<ConversationListSearchAdapter.SearchResultViewHolder>
|
||||
class ConversationListSearchAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||
implements StickyHeaderDecoration.StickyHeaderAdapter<ConversationListSearchAdapter.HeaderViewHolder>
|
||||
{
|
||||
private static final int VIEW_TYPE_EMPTY = 0;
|
||||
private static final int VIEW_TYPE_NON_EMPTY = 1;
|
||||
|
||||
private static final int TYPE_CONVERSATIONS = 1;
|
||||
private static final int TYPE_CONTACTS = 2;
|
||||
private static final int TYPE_MESSAGES = 3;
|
||||
|
@ -49,47 +52,69 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter<Conversation
|
|||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SearchResultViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new SearchResultViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_item_view, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SearchResultViewHolder holder, int position) {
|
||||
ThreadRecord conversationResult = getConversationResult(position);
|
||||
|
||||
if (conversationResult != null) {
|
||||
holder.bind(lifecycleOwner, conversationResult, glideRequests, eventListener, locale, searchResult.getQuery());
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient contactResult = getContactResult(position);
|
||||
|
||||
if (contactResult != null) {
|
||||
holder.bind(lifecycleOwner, contactResult, glideRequests, eventListener, locale, searchResult.getQuery());
|
||||
return;
|
||||
}
|
||||
|
||||
MessageResult messageResult = getMessageResult(position);
|
||||
|
||||
if (messageResult != null) {
|
||||
holder.bind(lifecycleOwner, messageResult, glideRequests, eventListener, locale, searchResult.getQuery());
|
||||
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
if (viewType == VIEW_TYPE_EMPTY) {
|
||||
return new EmptyViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_empty_search_state, parent, false));
|
||||
} else {
|
||||
return new SearchResultViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.conversation_list_item_view, parent, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull SearchResultViewHolder holder) {
|
||||
holder.recycle();
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (holder instanceof SearchResultViewHolder) {
|
||||
SearchResultViewHolder viewHolder = (SearchResultViewHolder) holder;
|
||||
ThreadRecord conversationResult = getConversationResult(position);
|
||||
|
||||
if (conversationResult != null) {
|
||||
viewHolder.bind(lifecycleOwner, conversationResult, glideRequests, eventListener, locale, searchResult.getQuery());
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient contactResult = getContactResult(position);
|
||||
|
||||
if (contactResult != null) {
|
||||
viewHolder.bind(lifecycleOwner, contactResult, glideRequests, eventListener, locale, searchResult.getQuery());
|
||||
return;
|
||||
}
|
||||
|
||||
MessageResult messageResult = getMessageResult(position);
|
||||
|
||||
if (messageResult != null) {
|
||||
viewHolder.bind(lifecycleOwner, messageResult, glideRequests, eventListener, locale, searchResult.getQuery());
|
||||
}
|
||||
} else if (holder instanceof EmptyViewHolder) {
|
||||
EmptyViewHolder viewHolder = (EmptyViewHolder) holder;
|
||||
viewHolder.bind(searchResult.getQuery());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (searchResult.isEmpty()) {
|
||||
return VIEW_TYPE_EMPTY;
|
||||
} else {
|
||||
return VIEW_TYPE_NON_EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
|
||||
if (holder instanceof SearchResultViewHolder) {
|
||||
((SearchResultViewHolder) holder).recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return searchResult.size();
|
||||
return searchResult.isEmpty() ? 1 : searchResult.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getHeaderId(int position) {
|
||||
if (position < 0) {
|
||||
if (position < 0 || searchResult.isEmpty()) {
|
||||
return StickyHeaderDecoration.StickyHeaderAdapter.NO_HEADER_ID;
|
||||
} else if (getConversationResult(position) != null) {
|
||||
return TYPE_CONVERSATIONS;
|
||||
|
@ -154,6 +179,20 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter<Conversation
|
|||
void onMessageClicked(@NonNull MessageResult message);
|
||||
}
|
||||
|
||||
static class EmptyViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final TextView textView;
|
||||
|
||||
public EmptyViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
textView = itemView.findViewById(R.id.search_no_results);
|
||||
}
|
||||
|
||||
public void bind(@NonNull String query) {
|
||||
textView.setText(textView.getContext().getString(R.string.SearchFragment_no_results, query));
|
||||
}
|
||||
}
|
||||
|
||||
static class SearchResultViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ConversationListItem root;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package org.thoughtcrime.securesms.conversationlist;
|
||||
|
||||
import android.app.Application;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.LiveDataReactiveStreams;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
|
@ -17,6 +17,7 @@ import org.signal.paging.PagingConfig;
|
|||
import org.signal.paging.PagingController;
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData;
|
||||
|
@ -40,6 +41,7 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -50,6 +52,7 @@ import io.reactivex.rxjava3.core.Observable;
|
|||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import kotlin.Pair;
|
||||
|
||||
class ConversationListViewModel extends ViewModel {
|
||||
|
||||
|
@ -57,30 +60,32 @@ class ConversationListViewModel extends ViewModel {
|
|||
|
||||
private static boolean coldStart = true;
|
||||
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final MutableLiveData<ConversationSet> selectedConversations;
|
||||
private final Set<Conversation> internalSelection;
|
||||
private final ConversationListDataSource conversationListDataSource;
|
||||
private final LivePagedData<Long, Conversation> pagedData;
|
||||
private final LiveData<Boolean> hasNoConversations;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final Debouncer messageSearchDebouncer;
|
||||
private final Debouncer contactSearchDebouncer;
|
||||
private final ThrottledDebouncer updateDebouncer;
|
||||
private final DatabaseObserver.Observer observer;
|
||||
private final Invalidator invalidator;
|
||||
private final CompositeDisposable disposables;
|
||||
private final UnreadPaymentsLiveData unreadPaymentsLiveData;
|
||||
private final UnreadPaymentsRepository unreadPaymentsRepository;
|
||||
private final NotificationProfilesRepository notificationProfilesRepository;
|
||||
private final MutableLiveData<Megaphone> megaphone;
|
||||
private final MutableLiveData<SearchResult> searchResult;
|
||||
private final MutableLiveData<ConversationSet> selectedConversations;
|
||||
private final MutableLiveData<ConversationFilter> conversationFilter;
|
||||
private final LiveData<ConversationListDataSource> conversationListDataSource;
|
||||
private final Set<Conversation> internalSelection;
|
||||
private final LiveData<LivePagedData<Long, Conversation>> pagedData;
|
||||
private final LiveData<Boolean> hasNoConversations;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
private final Debouncer messageSearchDebouncer;
|
||||
private final Debouncer contactSearchDebouncer;
|
||||
private final ThrottledDebouncer updateDebouncer;
|
||||
private final DatabaseObserver.Observer observer;
|
||||
private final Invalidator invalidator;
|
||||
private final CompositeDisposable disposables;
|
||||
private final UnreadPaymentsLiveData unreadPaymentsLiveData;
|
||||
private final UnreadPaymentsRepository unreadPaymentsRepository;
|
||||
private final NotificationProfilesRepository notificationProfilesRepository;
|
||||
|
||||
private String activeQuery;
|
||||
private SearchResult activeSearchResult;
|
||||
private int pinnedCount;
|
||||
private String activeQuery;
|
||||
private SearchResult activeSearchResult;
|
||||
private int pinnedCount;
|
||||
private ConversationFilterLatch conversationFilterLatch;
|
||||
|
||||
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
|
||||
private ConversationListViewModel(@NonNull SearchRepository searchRepository, boolean isArchived) {
|
||||
this.megaphone = new MutableLiveData<>();
|
||||
this.searchResult = new MutableLiveData<>();
|
||||
this.internalSelection = new HashSet<>();
|
||||
|
@ -95,29 +100,37 @@ class ConversationListViewModel extends ViewModel {
|
|||
this.activeSearchResult = SearchResult.EMPTY;
|
||||
this.invalidator = new Invalidator();
|
||||
this.disposables = new CompositeDisposable();
|
||||
this.conversationListDataSource = ConversationListDataSource.create(application, isArchived);
|
||||
this.pagedData = PagedData.createForLiveData(conversationListDataSource,
|
||||
new PagingConfig.Builder()
|
||||
.setPageSize(15)
|
||||
.setBufferPages(2)
|
||||
.build());
|
||||
this.conversationFilter = new MutableLiveData<>(ConversationFilter.OFF);
|
||||
this.conversationFilterLatch = ConversationFilterLatch.RESET;
|
||||
this.conversationListDataSource = Transformations.map(conversationFilter, filter -> ConversationListDataSource.create(filter, isArchived));
|
||||
this.pagedData = Transformations.map(conversationListDataSource, source -> PagedData.createForLiveData(source,
|
||||
new PagingConfig.Builder()
|
||||
.setPageSize(15)
|
||||
.setBufferPages(2)
|
||||
.build()));
|
||||
this.unreadPaymentsLiveData = new UnreadPaymentsLiveData();
|
||||
this.observer = () -> {
|
||||
updateDebouncer.publish(() -> {
|
||||
if (!TextUtils.isEmpty(activeQuery)) {
|
||||
onSearchQueryUpdated(activeQuery);
|
||||
}
|
||||
pagedData.getController().onDataInvalidated();
|
||||
|
||||
LivePagedData<Long, Conversation> data = pagedData.getValue();
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.getController().onDataInvalidated();
|
||||
});
|
||||
};
|
||||
|
||||
this.hasNoConversations = LiveDataUtil.mapAsync(pagedData.getData(), conversations -> {
|
||||
pinnedCount = SignalDatabase.threads().getPinnedConversationListCount();
|
||||
this.hasNoConversations = LiveDataUtil.mapAsync(LiveDataUtil.combineLatest(conversationFilter, getConversationList(), Pair::new), filterAndData -> {
|
||||
pinnedCount = SignalDatabase.threads().getPinnedConversationListCount(ConversationFilter.OFF);
|
||||
|
||||
if (conversations.size() > 0) {
|
||||
if (filterAndData.getSecond().size() > 0) {
|
||||
return false;
|
||||
} else {
|
||||
return SignalDatabase.threads().getArchivedConversationListCount() == 0;
|
||||
return SignalDatabase.threads().getArchivedConversationListCount(filterAndData.getFirst()) == 0;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -137,11 +150,11 @@ class ConversationListViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
@NonNull LiveData<List<Conversation>> getConversationList() {
|
||||
return pagedData.getData();
|
||||
return Transformations.switchMap(pagedData, LivePagedData::getData);
|
||||
}
|
||||
|
||||
@NonNull PagingController getPagingController() {
|
||||
return pagedData.getController();
|
||||
@NonNull LiveData<PagingController<Long>> getPagingController() {
|
||||
return Transformations.map(pagedData, LivePagedData::getController);
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<NotificationProfile>> getNotificationProfiles() {
|
||||
|
@ -199,6 +212,25 @@ class ConversationListViewModel extends ViewModel {
|
|||
setSelection(newSelection);
|
||||
}
|
||||
|
||||
void setConversationFilterLatch(@NonNull ConversationFilterLatch latch) {
|
||||
ConversationFilterLatch previous = conversationFilterLatch;
|
||||
conversationFilterLatch = latch;
|
||||
if (previous != latch && latch == ConversationFilterLatch.RESET) {
|
||||
toggleUnreadChatsFilter();
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleUnreadChatsFilter() {
|
||||
ConversationFilter filter = Objects.requireNonNull(conversationFilter.getValue());
|
||||
if (filter == ConversationFilter.UNREAD) {
|
||||
Log.d(TAG, "Setting filter to OFF");
|
||||
conversationFilter.setValue(ConversationFilter.OFF);
|
||||
} else {
|
||||
Log.d(TAG, "Setting filter to UNREAD");
|
||||
conversationFilter.setValue(ConversationFilter.UNREAD);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSelection(@NonNull Collection<Conversation> newSelection) {
|
||||
internalSelection.clear();
|
||||
internalSelection.addAll(newSelection);
|
||||
|
@ -206,8 +238,13 @@ class ConversationListViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
void onSelectAllClick() {
|
||||
ConversationListDataSource dataSource = conversationListDataSource.getValue();
|
||||
if (dataSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
disposables.add(
|
||||
Single.fromCallable(() -> conversationListDataSource.load(0, conversationListDataSource.size(), disposables::isDisposed))
|
||||
Single.fromCallable(() -> dataSource.load(0, dataSource.size(), disposables::isDisposed))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::setSelection)
|
||||
|
@ -301,7 +338,7 @@ class ConversationListViewModel extends ViewModel {
|
|||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository(noteToSelfTitle), isArchived));
|
||||
return modelClass.cast(new ConversationListViewModel(new SearchRepository(noteToSelfTitle), isArchived));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,9 @@ public class Conversation {
|
|||
THREAD,
|
||||
PINNED_HEADER,
|
||||
UNPINNED_HEADER,
|
||||
ARCHIVED_FOOTER
|
||||
ARCHIVED_FOOTER,
|
||||
CONVERSATION_FILTER_FOOTER,
|
||||
CONVERSATION_FILTER_EMPTY,
|
||||
EMPTY
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package org.thoughtcrime.securesms.conversationlist.model
|
||||
|
||||
/**
|
||||
* Describes what conversations should display in the
|
||||
* conversation list.
|
||||
*/
|
||||
enum class ConversationFilter {
|
||||
/**
|
||||
* No filtering is applied to the conversation list
|
||||
*/
|
||||
OFF,
|
||||
|
||||
/**
|
||||
* Only unread chats will be displayed in the conversation list
|
||||
*/
|
||||
UNREAD,
|
||||
|
||||
/**
|
||||
* Only muted chats will be displayed in the conversation list
|
||||
*/
|
||||
MUTED,
|
||||
|
||||
/**
|
||||
* Only group chats will be displayed in the conversation list
|
||||
*/
|
||||
GROUPS
|
||||
}
|
|
@ -12,10 +12,11 @@ import org.signal.core.util.CursorUtil;
|
|||
|
||||
public class ConversationReader extends ThreadDatabase.StaticReader {
|
||||
|
||||
public static final String[] HEADER_COLUMN = {"header"};
|
||||
public static final String[] ARCHIVED_COLUMNS = {"header", "count"};
|
||||
public static final String[] PINNED_HEADER = {Conversation.Type.PINNED_HEADER.toString()};
|
||||
public static final String[] UNPINNED_HEADER = {Conversation.Type.UNPINNED_HEADER.toString()};
|
||||
public static final String[] HEADER_COLUMN = { "header" };
|
||||
public static final String[] ARCHIVED_COLUMNS = { "header", "count" };
|
||||
public static final String[] PINNED_HEADER = { Conversation.Type.PINNED_HEADER.toString() };
|
||||
public static final String[] UNPINNED_HEADER = { Conversation.Type.UNPINNED_HEADER.toString() };
|
||||
public static final String[] CONVERSATION_FILTER_FOOTER = { Conversation.Type.CONVERSATION_FILTER_FOOTER.toString() };
|
||||
|
||||
private final Cursor cursor;
|
||||
|
||||
|
@ -43,11 +44,16 @@ public class ConversationReader extends ThreadDatabase.StaticReader {
|
|||
if (type == Conversation.Type.ARCHIVED_FOOTER) {
|
||||
count = CursorUtil.requireInt(cursor, ARCHIVED_COLUMNS[1]);
|
||||
}
|
||||
|
||||
return buildThreadRecordForType(type, count);
|
||||
}
|
||||
|
||||
public static ThreadRecord buildThreadRecordForType(@NonNull Conversation.Type type, int count) {
|
||||
return new ThreadRecord.Builder(-(100 + type.ordinal()))
|
||||
.setBody(type.toString())
|
||||
.setDate(100)
|
||||
.setRecipient(Recipient.UNKNOWN)
|
||||
.setUnreadCount(count)
|
||||
.build();
|
||||
.setBody(type.toString())
|
||||
.setDate(100)
|
||||
.setRecipient(Recipient.UNKNOWN)
|
||||
.setUnreadCount(count)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.signal.core.util.update
|
|||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.drafts
|
||||
|
@ -132,7 +133,8 @@ class ThreadDatabase(context: Context, databaseHelper: SignalDatabase) : Databas
|
|||
val CREATE_INDEXS = arrayOf(
|
||||
"CREATE INDEX IF NOT EXISTS thread_recipient_id_index ON $TABLE_NAME ($RECIPIENT_ID);",
|
||||
"CREATE INDEX IF NOT EXISTS archived_count_index ON $TABLE_NAME ($ARCHIVED, $MEANINGFUL_MESSAGES);",
|
||||
"CREATE INDEX IF NOT EXISTS thread_pinned_index ON $TABLE_NAME ($PINNED);"
|
||||
"CREATE INDEX IF NOT EXISTS thread_pinned_index ON $TABLE_NAME ($PINNED);",
|
||||
"CREATE INDEX IF NOT EXISTS thread_read ON $TABLE_NAME ($READ);"
|
||||
)
|
||||
|
||||
private val THREAD_PROJECTION = arrayOf(
|
||||
|
@ -754,7 +756,7 @@ class ThreadDatabase(context: Context, databaseHelper: SignalDatabase) : Databas
|
|||
}
|
||||
|
||||
fun getArchivedRecipients(): Set<RecipientId> {
|
||||
return getArchivedConversationList().readToList { cursor ->
|
||||
return getArchivedConversationList(ConversationFilter.OFF).readToList { cursor ->
|
||||
RecipientId.from(cursor.requireLong(RECIPIENT_ID))
|
||||
}.toSet()
|
||||
}
|
||||
|
@ -775,16 +777,18 @@ class ThreadDatabase(context: Context, databaseHelper: SignalDatabase) : Databas
|
|||
return positions
|
||||
}
|
||||
|
||||
fun getArchivedConversationList(offset: Long = 0, limit: Long = 0): Cursor {
|
||||
val query = createQuery("$ARCHIVED = ? AND $MEANINGFUL_MESSAGES != 0", offset, limit, preferPinned = false)
|
||||
fun getArchivedConversationList(conversationFilter: ConversationFilter, offset: Long = 0, limit: Long = 0): Cursor {
|
||||
val filterQuery = conversationFilter.toQuery()
|
||||
val query = createQuery("$ARCHIVED = ? AND $MEANINGFUL_MESSAGES != 0 $filterQuery", offset, limit, preferPinned = false)
|
||||
return readableDatabase.rawQuery(query, arrayOf("1"))
|
||||
}
|
||||
|
||||
fun getUnarchivedConversationList(pinned: Boolean, offset: Long, limit: Long): Cursor {
|
||||
fun getUnarchivedConversationList(conversationFilter: ConversationFilter, pinned: Boolean, offset: Long, limit: Long): Cursor {
|
||||
val filterQuery = conversationFilter.toQuery()
|
||||
val where = if (pinned) {
|
||||
"$ARCHIVED = 0 AND $PINNED != 0"
|
||||
"$ARCHIVED = 0 AND $PINNED != 0 $filterQuery"
|
||||
} else {
|
||||
"$ARCHIVED = 0 AND $PINNED = 0 AND $MEANINGFUL_MESSAGES != 0"
|
||||
"$ARCHIVED = 0 AND $PINNED = 0 AND $MEANINGFUL_MESSAGES != 0 $filterQuery"
|
||||
}
|
||||
|
||||
val query = if (pinned) {
|
||||
|
@ -796,11 +800,12 @@ class ThreadDatabase(context: Context, databaseHelper: SignalDatabase) : Databas
|
|||
return readableDatabase.rawQuery(query, null)
|
||||
}
|
||||
|
||||
fun getArchivedConversationListCount(): Int {
|
||||
fun getArchivedConversationListCount(conversationFilter: ConversationFilter): Int {
|
||||
val filterQuery = conversationFilter.toQuery()
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$ARCHIVED = 1 AND $MEANINGFUL_MESSAGES != 0")
|
||||
.where("$ARCHIVED = 1 AND $MEANINGFUL_MESSAGES != 0 $filterQuery")
|
||||
.run()
|
||||
.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
|
@ -811,11 +816,12 @@ class ThreadDatabase(context: Context, databaseHelper: SignalDatabase) : Databas
|
|||
}
|
||||
}
|
||||
|
||||
fun getPinnedConversationListCount(): Int {
|
||||
fun getPinnedConversationListCount(conversationFilter: ConversationFilter): Int {
|
||||
val filterQuery = conversationFilter.toQuery()
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$ARCHIVED = 0 AND $PINNED != 0")
|
||||
.where("$ARCHIVED = 0 AND $PINNED != 0 $filterQuery")
|
||||
.run()
|
||||
.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
|
@ -826,11 +832,12 @@ class ThreadDatabase(context: Context, databaseHelper: SignalDatabase) : Databas
|
|||
}
|
||||
}
|
||||
|
||||
fun getUnarchivedConversationListCount(): Int {
|
||||
fun getUnarchivedConversationListCount(conversationFilter: ConversationFilter): Int {
|
||||
val filterQuery = conversationFilter.toQuery()
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$ARCHIVED = 0 AND ($MEANINGFUL_MESSAGES != 0 OR $PINNED != 0)")
|
||||
.where("$ARCHIVED = 0 AND ($MEANINGFUL_MESSAGES != 0 OR $PINNED != 0) $filterQuery")
|
||||
.run()
|
||||
.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
|
@ -888,7 +895,7 @@ class ThreadDatabase(context: Context, databaseHelper: SignalDatabase) : Databas
|
|||
.run()
|
||||
}
|
||||
|
||||
var pinnedCount = getPinnedConversationListCount()
|
||||
var pinnedCount = getPinnedConversationListCount(ConversationFilter.OFF)
|
||||
|
||||
for (threadId in threadIds) {
|
||||
pinnedCount++
|
||||
|
@ -1587,6 +1594,15 @@ class ThreadDatabase(context: Context, databaseHelper: SignalDatabase) : Databas
|
|||
return this.trimIndent().split("\n").joinToString(separator = " ")
|
||||
}
|
||||
|
||||
private fun ConversationFilter.toQuery(): String {
|
||||
return when (this) {
|
||||
ConversationFilter.OFF -> ""
|
||||
ConversationFilter.UNREAD -> " AND $READ != ${ReadStatus.READ.serialize()}"
|
||||
ConversationFilter.MUTED -> error("This filter selection isn't supported yet.")
|
||||
ConversationFilter.GROUPS -> error("This filter selection isn't supported yet.")
|
||||
}
|
||||
}
|
||||
|
||||
object DistributionTypes {
|
||||
const val DEFAULT = 2
|
||||
const val BROADCAST = 1
|
||||
|
|
|
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V160_SmsMmsExported
|
|||
import org.thoughtcrime.securesms.database.helpers.migration.V161_StorySendMessageIdIndex
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V162_ThreadUnreadSelfMentionCountFixup
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V163_RemoteMegaphoneSnoozeSupportMigration
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V164_ThreadDatabaseReadIndexMigration
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
|
@ -27,7 +28,7 @@ object SignalDatabaseMigrations {
|
|||
|
||||
val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass)
|
||||
|
||||
const val DATABASE_VERSION = 163
|
||||
const val DATABASE_VERSION = 164
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
@ -90,6 +91,10 @@ object SignalDatabaseMigrations {
|
|||
if (oldVersion < 163) {
|
||||
V163_RemoteMegaphoneSnoozeSupportMigration.migrate(context, db, oldVersion, newVersion)
|
||||
}
|
||||
|
||||
if (oldVersion < 164) {
|
||||
V164_ThreadDatabaseReadIndexMigration.migrate(context, db, oldVersion, newVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
|
||||
object V164_ThreadDatabaseReadIndexMigration : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS thread_read ON thread (read);")
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import org.signal.core.util.Hex
|
|||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase
|
||||
import org.thoughtcrime.securesms.database.RemoteMegaphoneDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
|
@ -150,7 +151,7 @@ class RetrieveRemoteAnnouncementsJob private constructor(private val force: Bool
|
|||
}
|
||||
|
||||
if (!values.hasMetConversationRequirement) {
|
||||
if ((SignalDatabase.threads.getArchivedConversationListCount() + SignalDatabase.threads.getUnarchivedConversationListCount()) < 6) {
|
||||
if ((SignalDatabase.threads.getArchivedConversationListCount(ConversationFilter.OFF) + SignalDatabase.threads.getUnarchivedConversationListCount(ConversationFilter.OFF)) < 6) {
|
||||
Log.i(TAG, "User does not have enough conversations to show release channel")
|
||||
values.nextScheduledCheck = System.currentTimeMillis() + RETRIEVE_FREQUENCY
|
||||
return
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.signal.core.util.logging.Log;
|
|||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.NewConversationActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
|
@ -76,8 +77,8 @@ public class UserNotificationMigrationJob extends MigrationJob {
|
|||
|
||||
ThreadDatabase threadDatabase = SignalDatabase.threads();
|
||||
|
||||
int threadCount = threadDatabase.getUnarchivedConversationListCount() +
|
||||
threadDatabase.getArchivedConversationListCount();
|
||||
int threadCount = threadDatabase.getUnarchivedConversationListCount(ConversationFilter.OFF) +
|
||||
threadDatabase.getArchivedConversationListCount(ConversationFilter.OFF);
|
||||
|
||||
if (threadCount >= 3) {
|
||||
Log.w(TAG, "Already have 3 or more threads. Skipping.");
|
||||
|
|
|
@ -108,6 +108,7 @@ public final class FeatureFlags {
|
|||
public static final String PAYPAL_DISABLED_REGIONS = "global.donations.paypalDisabledRegions";
|
||||
private static final String CDS_HARD_LIMIT = "android.cds.hardLimit";
|
||||
private static final String PAYMENTS_IN_CHAT_MESSAGES = "android.payments.inChatMessages";
|
||||
private static final String CHAT_FILTERS = "android.chat.filters";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -168,7 +169,8 @@ public final class FeatureFlags {
|
|||
PAYPAL_DISABLED_REGIONS,
|
||||
KEEP_MUTED_CHATS_ARCHIVED,
|
||||
CDS_HARD_LIMIT,
|
||||
PAYMENTS_IN_CHAT_MESSAGES
|
||||
PAYMENTS_IN_CHAT_MESSAGES,
|
||||
CHAT_FILTERS
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -608,6 +610,13 @@ public final class FeatureFlags {
|
|||
return getInteger(CDS_HARD_LIMIT, 50_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables chat filters. Note that this UI is incomplete.
|
||||
*/
|
||||
public static boolean chatFilters() {
|
||||
return getBoolean(CHAT_FILTERS, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/search_no_results"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
tools:text="@string/SearchFragment_no_results" />
|
|
@ -1,19 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/empty_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="32dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar_barrier"
|
||||
tools:visibility="visible">
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<TextView
|
||||
style="@style/Signal.Text.Body"
|
||||
|
@ -26,4 +19,4 @@
|
|||
android:gravity="center"
|
||||
android:text="@string/conversation_list_fragment__no_chats_yet_get_started_by_messaging_a_friend" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge 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"
|
||||
tools:parentTag="android.widget.FrameLayout">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginBottom="11dp"
|
||||
android:text="@string/ConversationListFilterPullView__pull_down_to_filter"
|
||||
android:textAppearance="@style/Signal.Text.LabelLarge" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/arrow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_arrow_down"
|
||||
app:tint="@color/signal_colorOnSurface" />
|
||||
|
||||
</merge>
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/constraint_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:windowBackground">
|
||||
android:background="?android:windowBackground"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/voice_note_player"
|
||||
|
@ -17,29 +17,6 @@
|
|||
android:layout="@layout/voice_note_player_stub"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/search_no_results"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/signal_background_primary"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/SearchFragment_no_results" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/empty_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout="@layout/conversation_list_empty_state"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/reminder"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -63,19 +40,43 @@
|
|||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="reminder,payments_notification,voice_note_player" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:nextFocusDown="@+id/fab"
|
||||
android:nextFocusForward="@+id/fab"
|
||||
android:paddingBottom="160dp"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/banner_barrier"
|
||||
tools:listitem="@layout/conversation_list_item_view"
|
||||
tools:visibility="visible" />
|
||||
app:layout_constraintTop_toBottomOf="@id/banner_barrier">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/recycler_coordinator_app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:expanded="false"
|
||||
app:layout_behavior="org.thoughtcrime.securesms.conversationlist.ConversationFilterBehavior">
|
||||
|
||||
<org.thoughtcrime.securesms.conversationlist.ConversationListFilterPullView
|
||||
android:id="@+id/pull_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:background="@color/signal_colorSurface1"
|
||||
app:layout_scrollFlags="scroll|enterAlwaysCollapsed"
|
||||
app:layout_scrollInterpolator="@android:anim/decelerate_interpolator" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/signal_colorBackground"
|
||||
android:clipToPadding="false"
|
||||
android:nextFocusDown="@+id/fab"
|
||||
android:nextFocusForward="@+id/fab"
|
||||
android:paddingBottom="160dp"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:listitem="@layout/conversation_list_item_view"
|
||||
tools:visibility="visible" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/coordinator"
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="88dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/clear_filter"
|
||||
style="@style/Signal.Widget.Button.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/ConversationListFragment__clear_filter"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant" />
|
||||
|
||||
</FrameLayout>
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/ConversationListFragment__no_unread_chats"
|
||||
android:textAppearance="@style/Signal.Text.BodyLarge"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/clear_filter"
|
||||
style="@style/Signal.Widget.Button.Small.Primary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/ConversationListFragment__clear_filter"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant" />
|
||||
</LinearLayout>
|
|
@ -13,6 +13,9 @@
|
|||
<item android:title="@string/text_secure_normal__invite_friends"
|
||||
android:id="@+id/menu_invite" />
|
||||
|
||||
<item android:title="@string/text_secure_normal__filter_unread_chats"
|
||||
android:id="@+id/menu_filter_unread_chats" />
|
||||
|
||||
<item android:title="@string/text_secure_normal__menu_settings"
|
||||
android:id="@+id/menu_settings" />
|
||||
|
||||
|
|
|
@ -486,7 +486,10 @@
|
|||
<string name="ConversationFragment__cancel">Cancel</string>
|
||||
<!-- Message shown after successfully blocking join requests for a user -->
|
||||
<string name="ConversationFragment__blocked">Blocked</string>
|
||||
|
||||
<!-- Label for a button displayed in conversation list to clear the chat filter -->
|
||||
<string name="ConversationListFragment__clear_filter">Clear filter</string>
|
||||
<!-- Notice on chat list when no unread chats are available, centered on display -->
|
||||
<string name="ConversationListFragment__no_unread_chats">No unread chats</string>
|
||||
<plurals name="ConversationListFragment_delete_selected_conversations">
|
||||
<item quantity="one">Delete selected conversation?</item>
|
||||
<item quantity="other">Delete selected conversations?</item>
|
||||
|
@ -572,6 +575,12 @@
|
|||
<!-- CreateGroupActivity -->
|
||||
<string name="CreateGroupActivity__select_members">Select members</string>
|
||||
|
||||
<!-- ConversationListFilterPullView -->
|
||||
<!-- Note in revealable view before fully revealed -->
|
||||
<string name="ConversationListFilterPullView__pull_down_to_filter">Pull down to filter</string>
|
||||
<!-- Note in revealable view after fully revealed -->
|
||||
<string name="ConversationListFilterPullView__release_to_filter">Release to filter</string>
|
||||
|
||||
<!-- CreateProfileActivity -->
|
||||
<string name="CreateProfileActivity__profile">Profile</string>
|
||||
<string name="CreateProfileActivity_error_setting_profile_photo">Error setting profile photo</string>
|
||||
|
@ -3197,6 +3206,8 @@
|
|||
<string name="text_secure_normal__menu_clear_passphrase">Lock</string>
|
||||
<string name="text_secure_normal__mark_all_as_read">Mark all read</string>
|
||||
<string name="text_secure_normal__invite_friends">Invite friends</string>
|
||||
<!-- Overflow menu entry to filter unread chats -->
|
||||
<string name="text_secure_normal__filter_unread_chats">Filter unread chats</string>
|
||||
|
||||
<!-- verify_display_fragment -->
|
||||
<string name="verify_display_fragment_context_menu__copy_to_clipboard">Copy to clipboard</string>
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.mockito.junit.MockitoJUnit;
|
|||
import org.mockito.junit.MockitoRule;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader;
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
|
@ -22,6 +23,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
@ -52,7 +54,7 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
when(SignalDatabase.threads()).thenReturn(threadDatabase);
|
||||
when(ApplicationDependencies.getDatabaseObserver()).thenReturn(mock(DatabaseObserver.class));
|
||||
|
||||
testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(mock(Application.class));
|
||||
testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(ConversationFilter.OFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -65,13 +67,14 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
assertEquals(0, testSubject.getHeaderOffset());
|
||||
assertFalse(testSubject.hasPinnedHeader());
|
||||
assertFalse(testSubject.hasUnpinnedHeader());
|
||||
assertFalse(testSubject.hasConversationFilterFooter());
|
||||
assertFalse(testSubject.hasArchivedFooter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenArchivedConversations_whenIGetTotalCount_thenIExpectOne() {
|
||||
// GIVEN
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
when(threadDatabase.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
|
||||
|
||||
// WHEN
|
||||
int result = testSubject.getTotalCount();
|
||||
|
@ -81,15 +84,16 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
assertEquals(0, testSubject.getHeaderOffset());
|
||||
assertFalse(testSubject.hasPinnedHeader());
|
||||
assertFalse(testSubject.hasUnpinnedHeader());
|
||||
assertFalse(testSubject.hasConversationFilterFooter());
|
||||
assertTrue(testSubject.hasArchivedFooter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSinglePinnedAndArchivedConversations_whenIGetTotalCount_thenIExpectThree() {
|
||||
// GIVEN
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
when(threadDatabase.getPinnedConversationListCount(ConversationFilter.OFF)).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount(ConversationFilter.OFF)).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
|
||||
|
||||
// WHEN
|
||||
int result = testSubject.getTotalCount();
|
||||
|
@ -99,14 +103,15 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
assertEquals(1, testSubject.getHeaderOffset());
|
||||
assertTrue(testSubject.hasPinnedHeader());
|
||||
assertFalse(testSubject.hasUnpinnedHeader());
|
||||
assertFalse(testSubject.hasConversationFilterFooter());
|
||||
assertTrue(testSubject.hasArchivedFooter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSingleUnpinnedAndArchivedConversations_whenIGetTotalCount_thenIExpectTwo() {
|
||||
// GIVEN
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
when(threadDatabase.getUnarchivedConversationListCount(ConversationFilter.OFF)).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
|
||||
|
||||
// WHEN
|
||||
int result = testSubject.getTotalCount();
|
||||
|
@ -116,14 +121,15 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
assertEquals(0, testSubject.getHeaderOffset());
|
||||
assertFalse(testSubject.hasPinnedHeader());
|
||||
assertFalse(testSubject.hasUnpinnedHeader());
|
||||
assertFalse(testSubject.hasConversationFilterFooter());
|
||||
assertTrue(testSubject.hasArchivedFooter());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenSinglePinnedAndSingleUnpinned_whenIGetTotalCount_thenIExpectFour() {
|
||||
// GIVEN
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(2);
|
||||
when(threadDatabase.getPinnedConversationListCount(ConversationFilter.OFF)).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount(ConversationFilter.OFF)).thenReturn(2);
|
||||
|
||||
// WHEN
|
||||
int result = testSubject.getTotalCount();
|
||||
|
@ -133,6 +139,7 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
assertEquals(2, testSubject.getHeaderOffset());
|
||||
assertTrue(testSubject.hasPinnedHeader());
|
||||
assertTrue(testSubject.hasUnpinnedHeader());
|
||||
assertFalse(testSubject.hasConversationFilterFooter());
|
||||
assertFalse(testSubject.hasArchivedFooter());
|
||||
}
|
||||
|
||||
|
@ -145,8 +152,8 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
Cursor cursor = testSubject.getCursor(0, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 100);
|
||||
assertEquals(0, cursor.getCount());
|
||||
}
|
||||
|
||||
|
@ -154,15 +161,15 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
public void givenArchivedConversations_whenIGetCursor_thenIExpectOne() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(0, 0);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
when(threadDatabase.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(0, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 100);
|
||||
assertEquals(1, cursor.getCount());
|
||||
}
|
||||
|
||||
|
@ -170,17 +177,17 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
public void givenSinglePinnedAndArchivedConversations_whenIGetCursor_thenIExpectThree() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(1, 0);
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
when(threadDatabase.getPinnedConversationListCount(ConversationFilter.OFF)).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount(ConversationFilter.OFF)).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(0, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 0, 99);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 0, 98);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 99);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 98);
|
||||
assertEquals(3, cursor.getCount());
|
||||
}
|
||||
|
||||
|
@ -188,16 +195,16 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
public void givenSingleUnpinnedAndArchivedConversations_whenIGetCursor_thenIExpectTwo() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(0, 1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
when(threadDatabase.getUnarchivedConversationListCount(ConversationFilter.OFF)).thenReturn(1);
|
||||
when(threadDatabase.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(0, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 100);
|
||||
assertEquals(2, cursor.getCount());
|
||||
}
|
||||
|
||||
|
@ -205,16 +212,16 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
public void givenSinglePinnedAndSingleUnpinned_whenIGetCursor_thenIExpectFour() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(1, 1);
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(2);
|
||||
when(threadDatabase.getPinnedConversationListCount(ConversationFilter.OFF)).thenReturn(1);
|
||||
when(threadDatabase.getUnarchivedConversationListCount(ConversationFilter.OFF)).thenReturn(2);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(0, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 0, 99);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 0, 97);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 99);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, false, 0, 97);
|
||||
assertEquals(4, cursor.getCount());
|
||||
}
|
||||
|
||||
|
@ -222,16 +229,16 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
public void givenLoadingSecondPage_whenIGetCursor_thenIExpectProperOffsetAndCursorCount() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(0, 100);
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(4);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(104);
|
||||
when(threadDatabase.getPinnedConversationListCount(ConversationFilter.OFF)).thenReturn(4);
|
||||
when(threadDatabase.getUnarchivedConversationListCount(ConversationFilter.OFF)).thenReturn(104);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(50, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 50, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 44, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, true, 50, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, false, 44, 100);
|
||||
assertEquals(100, cursor.getCount());
|
||||
}
|
||||
|
||||
|
@ -239,23 +246,44 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
public void givenHasArchivedAndLoadingLastPage_whenIGetCursor_thenIExpectProperOffsetAndCursorCount() {
|
||||
// GIVEN
|
||||
setupThreadDatabaseCursors(0, 99);
|
||||
when(threadDatabase.getPinnedConversationListCount()).thenReturn(4);
|
||||
when(threadDatabase.getUnarchivedConversationListCount()).thenReturn(103);
|
||||
when(threadDatabase.getArchivedConversationListCount()).thenReturn(12);
|
||||
when(threadDatabase.getPinnedConversationListCount(ConversationFilter.OFF)).thenReturn(4);
|
||||
when(threadDatabase.getUnarchivedConversationListCount(ConversationFilter.OFF)).thenReturn(103);
|
||||
when(threadDatabase.getArchivedConversationListCount(ConversationFilter.OFF)).thenReturn(12);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(50, 100);
|
||||
|
||||
// THEN
|
||||
verify(threadDatabase).getUnarchivedConversationList(true, 50, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(false, 44, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, true, 50, 100);
|
||||
verify(threadDatabase).getUnarchivedConversationList(ConversationFilter.OFF, false, 44, 100);
|
||||
assertEquals(100, cursor.getCount());
|
||||
|
||||
cursor.moveToLast();
|
||||
assertEquals(0, cursor.getColumnIndex(ConversationReader.HEADER_COLUMN[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenHasNoArchivedAndIsFiltered_whenIGetCursor_thenIExpectConversationFilterFooter() {
|
||||
// GIVEN
|
||||
ConversationListDataSource.UnarchivedConversationListDataSource testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(ConversationFilter.UNREAD);
|
||||
setupThreadDatabaseCursors(0, 3);
|
||||
when(threadDatabase.getPinnedConversationListCount(ConversationFilter.UNREAD)).thenReturn(0);
|
||||
when(threadDatabase.getUnarchivedConversationListCount(ConversationFilter.UNREAD)).thenReturn(3);
|
||||
when(threadDatabase.getArchivedConversationListCount(ConversationFilter.UNREAD)).thenReturn(0);
|
||||
testSubject.getTotalCount();
|
||||
|
||||
// WHEN
|
||||
Cursor cursor = testSubject.getCursor(0, 5);
|
||||
|
||||
// THEN
|
||||
assertEquals(4, cursor.getCount());
|
||||
assertTrue(testSubject.hasConversationFilterFooter());
|
||||
|
||||
cursor.moveToLast();
|
||||
assertEquals(0, cursor.getColumnIndex(ConversationReader.HEADER_COLUMN[0]));
|
||||
}
|
||||
|
||||
|
||||
private void setupThreadDatabaseCursors(int pinned, int unpinned) {
|
||||
Cursor pinnedCursor = mock(Cursor.class);
|
||||
|
@ -264,7 +292,7 @@ public class UnarchivedConversationListDataSourceTest {
|
|||
Cursor unpinnedCursor = mock(Cursor.class);
|
||||
when(unpinnedCursor.getCount()).thenReturn(unpinned);
|
||||
|
||||
when(threadDatabase.getUnarchivedConversationList(eq(true), anyLong(), anyLong())).thenReturn(pinnedCursor);
|
||||
when(threadDatabase.getUnarchivedConversationList(eq(false), anyLong(), anyLong())).thenReturn(unpinnedCursor);
|
||||
when(threadDatabase.getUnarchivedConversationList(any(), eq(true), anyLong(), anyLong())).thenReturn(pinnedCursor);
|
||||
when(threadDatabase.getUnarchivedConversationList(any(), eq(false), anyLong(), anyLong())).thenReturn(unpinnedCursor);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue