From 296a113c65dbacc114d474f28babf3b11ab3d22c Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 4 Jan 2023 09:57:14 -0400 Subject: [PATCH] Add "You can pull to filter" tip. --- .../app/internal/InternalSettingsFragment.kt | 12 ++++ .../ClearFilterViewHolder.java | 35 +++++++++ .../ConversationListAdapter.java | 27 +++---- .../ConversationListDataSource.java | 38 +++++----- .../ConversationListFragment.java | 14 ++-- .../ConversationListSearchAdapter.java | 71 ++++++++++++++----- .../ConversationListViewModel.java | 35 +++++---- .../chatfilter/ConversationFilterRequest.kt | 8 +++ .../chatfilter/ConversationFilterSource.kt | 16 +++++ .../ConversationListFilterPullView.kt | 44 ++++++------ .../model/ConversationReader.java | 19 ++++- .../securesms/database/ThreadTable.kt | 10 ++- .../securesms/keyvalue/UiHints.java | 22 +++++- .../securesms/search/SearchRepository.java | 16 ++--- .../securesms/search/SearchResult.kt | 10 ++- .../securesms/util/FeatureFlags.java | 3 +- .../conversation_list_item_clear_filter.xml | 18 ++++- ...versation_list_item_clear_filter_empty.xml | 10 +++ app/src/main/res/values/strings.xml | 2 + ...rchivedConversationListDataSourceTest.java | 4 +- 20 files changed, 297 insertions(+), 117 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversationlist/ClearFilterViewHolder.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterRequest.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterSource.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index bc74b178a5..9ddaf1e27d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.megaphone.Megaphones import org.thoughtcrime.securesms.payments.DataExportUtil import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.util.ConversationUtil +import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.navigation.safeNavigate import java.util.Optional @@ -576,6 +577,17 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter viewModel.resetPnpInitializedState() } ) + + if (FeatureFlags.chatFilters()) { + dividerPref() + sectionHeaderPref(DSLSettingsText.from("Chat Filters")) + clickPref( + title = DSLSettingsText.from("Reset pull to refresh tip count"), + onClick = { + SignalStore.uiHints().resetNeverDisplayPullToRefreshCount() + } + ) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ClearFilterViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ClearFilterViewHolder.java new file mode 100644 index 0000000000..2da1da4b0f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ClearFilterViewHolder.java @@ -0,0 +1,35 @@ +package org.thoughtcrime.securesms.conversationlist; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.conversationlist.model.Conversation; +import org.thoughtcrime.securesms.conversationlist.model.ConversationReader; + +class ClearFilterViewHolder extends RecyclerView.ViewHolder { + + private final View tip; + + ClearFilterViewHolder(@NonNull View itemView, OnClearFilterClickListener listener) { + super(itemView); + tip = itemView.findViewById(R.id.clear_filter_tip); + itemView.findViewById(R.id.clear_filter).setOnClickListener(v -> { + listener.onClearFilterClick(); + }); + } + + void bind(@NonNull Conversation conversation) { + if (conversation.getThreadRecord().getType() == ConversationReader.TYPE_SHOW_TIP) { + tip.setVisibility(View.VISIBLE); + } else { + tip.setVisibility(View.GONE); + } + } + + interface OnClearFilterClickListener { + void onClearFilterClick(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java index 63428c8376..bb47ac5db3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java @@ -17,6 +17,7 @@ import org.signal.paging.PagingController; import org.thoughtcrime.securesms.BindableConversationListItem; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.conversationlist.model.Conversation; +import org.thoughtcrime.securesms.conversationlist.model.ConversationReader; import org.thoughtcrime.securesms.conversationlist.model.ConversationSet; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.util.CachedInflater; @@ -45,9 +46,9 @@ class ConversationListAdapter extends ListAdapter typingSet = new HashSet<>(); private PagingController pagingController; @@ -55,7 +56,7 @@ class ConversationListAdapter extends ListAdapter { - listener.onClearFilterClick(); - }); - } - } - interface OnConversationClickListener { void onConversationClick(@NonNull Conversation conversation); boolean onConversationLongClick(@NonNull Conversation conversation, @NonNull View view); void onShowArchiveClick(); } - - interface OnClearFilterClickListener { - void onClearFilterClick(); - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java index 7de282ad48..e21a507d46 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java @@ -38,15 +38,17 @@ abstract class ConversationListDataSource implements PagedDataSource= totalCount && totalCount > 0 && conversationFilter != ConversationFilter.OFF) { - MatrixCursor conversationFilterFooter = new MatrixCursor(ConversationReader.HEADER_COLUMN); - conversationFilterFooter.addRow(ConversationReader.CONVERSATION_FILTER_FOOTER); + MatrixCursor conversationFilterFooter = new MatrixCursor(ConversationReader.FILTER_FOOTER_COLUMNS); + conversationFilterFooter.addRow(ConversationReader.createConversationFilterFooterRow(showConversationFooterTip)); cursors.add(conversationFilterFooter); } @@ -158,8 +162,8 @@ abstract class ConversationListDataSource implements PagedDataSource 1 && conversationFilter != ConversationFilter.OFF; + return totalCount >= 1 && conversationFilter != ConversationFilter.OFF; } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 2ccfa54201..e4a0f3b3fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -114,6 +114,7 @@ import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView; import org.thoughtcrime.securesms.contacts.sync.CdsPermanentErrorBottomSheet; import org.thoughtcrime.securesms.contacts.sync.CdsTemporaryErrorBottomSheet; import org.thoughtcrime.securesms.conversation.ConversationFragment; +import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource; import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationListFilterPullView; import org.thoughtcrime.securesms.conversationlist.chatfilter.FilterLerp; import org.thoughtcrime.securesms.conversationlist.model.Conversation; @@ -195,7 +196,7 @@ import static android.app.Activity.RESULT_OK; public class ConversationListFragment extends MainFragment implements ActionMode.Callback, ConversationListAdapter.OnConversationClickListener, ConversationListSearchAdapter.EventListener, - MegaphoneActionController, ConversationListAdapter.OnClearFilterClickListener + MegaphoneActionController, ClearFilterViewHolder.OnClearFilterClickListener { public static final short MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME = 32562; public static final short SMS_ROLE_REQUEST_CODE = 32563; @@ -283,16 +284,19 @@ public class ConversationListFragment extends MainFragment implements ActionMode CollapsingToolbarLayout collapsingToolbarLayout = view.findViewById(R.id.collapsing_toolbar); int openHeight = (int) DimensionUnit.DP.toPixels(FilterLerp.FILTER_OPEN_HEIGHT); - pullView.setOnFilterStateChanged(state -> { + pullView.setOnFilterStateChanged((state, source) -> { switch (state) { case CLOSING: - viewModel.setFiltered(false); + viewModel.setFiltered(false, source); break; case OPENING: - viewModel.setFiltered(true); + viewModel.setFiltered(true, source); break; case OPEN_APEX: ViewUtil.setMinimumHeight(collapsingToolbarLayout, openHeight); + if (source == ConversationFilterSource.DRAG) { + SignalStore.uiHints().incrementNeverDisplayPullToFilterTip(); + } break; case CLOSE_APEX: ViewUtil.setMinimumHeight(collapsingToolbarLayout, 0); @@ -747,7 +751,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode private void initializeListAdapters() { defaultAdapter = new ConversationListAdapter(getViewLifecycleOwner(), GlideApp.with(this), this, this); - searchAdapter = new ConversationListSearchAdapter(getViewLifecycleOwner(), GlideApp.with(this), this, Locale.getDefault()); + searchAdapter = new ConversationListSearchAdapter(getViewLifecycleOwner(), GlideApp.with(this), this, Locale.getDefault(), this); searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false, 0); setAdapter(defaultAdapter); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.java index b5583600e9..52fe0b38bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.java @@ -10,7 +10,9 @@ import androidx.annotation.Nullable; import androidx.lifecycle.LifecycleOwner; import androidx.recyclerview.widget.RecyclerView; +import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter; import org.thoughtcrime.securesms.conversationlist.model.ConversationSet; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.mms.GlideRequests; @@ -25,30 +27,35 @@ import java.util.Locale; class ConversationListSearchAdapter extends RecyclerView.Adapter implements StickyHeaderDecoration.StickyHeaderAdapter { - private static final int VIEW_TYPE_EMPTY = 0; - private static final int VIEW_TYPE_NON_EMPTY = 1; + private static final int VIEW_TYPE_EMPTY = 0; + private static final int VIEW_TYPE_NON_EMPTY = 1; + private static final int VIEW_TYPE_CHAT_FILTER = 2; + private static final int VIEW_TYPE_CHAT_FILTER_EMPTY = 3; private static final int TYPE_CONVERSATIONS = 1; private static final int TYPE_CONTACTS = 2; private static final int TYPE_MESSAGES = 3; - private final LifecycleOwner lifecycleOwner; - private final GlideRequests glideRequests; - private final EventListener eventListener; - private final Locale locale; + private final LifecycleOwner lifecycleOwner; + private final GlideRequests glideRequests; + private final EventListener eventListener; + private final Locale locale; + private final ClearFilterViewHolder.OnClearFilterClickListener onClearFilterClicked; @NonNull private SearchResult searchResult = SearchResult.EMPTY; - ConversationListSearchAdapter(@NonNull LifecycleOwner lifecycleOwner, - @NonNull GlideRequests glideRequests, - @NonNull EventListener eventListener, - @NonNull Locale locale) + ConversationListSearchAdapter(@NonNull LifecycleOwner lifecycleOwner, + @NonNull GlideRequests glideRequests, + @NonNull EventListener eventListener, + @NonNull Locale locale, + @NonNull ClearFilterViewHolder.OnClearFilterClickListener onClearFilterClicked) { - this.lifecycleOwner = lifecycleOwner; - this.glideRequests = glideRequests; - this.eventListener = eventListener; - this.locale = locale; + this.lifecycleOwner = lifecycleOwner; + this.glideRequests = glideRequests; + this.eventListener = eventListener; + this.locale = locale; + this.onClearFilterClicked = onClearFilterClicked; } @Override @@ -56,6 +63,12 @@ class ConversationListSearchAdapter extends RecyclerView.Adapter megaphone; private final MutableLiveData searchResult; private final MutableLiveData selectedConversations; - private final MutableLiveData conversationFilter; + private final MutableLiveData conversationFilterRequest; private final LiveData conversationListDataSource; private final Set internalSelection; private final LiveData> pagedData; @@ -99,9 +102,11 @@ class ConversationListViewModel extends ViewModel { this.activeSearchResult = SearchResult.EMPTY; this.invalidator = new Invalidator(); this.disposables = new CompositeDisposable(); - this.conversationFilter = new MutableLiveData<>(ConversationFilter.OFF); - this.conversationListDataSource = Transformations.map(Transformations.distinctUntilChanged(conversationFilter), - filter -> ConversationListDataSource.create(filter, isArchived)); + this.conversationFilterRequest = new MutableLiveData<>(new ConversationFilterRequest(ConversationFilter.OFF, ConversationFilterSource.DRAG)); + this.conversationListDataSource = Transformations.map(Transformations.distinctUntilChanged(conversationFilterRequest), + request -> ConversationListDataSource.create(request.getFilter(), + isArchived, + SignalStore.uiHints().canDisplayPullToFilterTip() && request.getSource() == ConversationFilterSource.OVERFLOW)); this.pagedData = Transformations.map(conversationListDataSource, source -> PagedData.createForLiveData(source, new PagingConfig.Builder() .setPageSize(15) @@ -123,13 +128,13 @@ class ConversationListViewModel extends ViewModel { }); }; - this.hasNoConversations = LiveDataUtil.mapAsync(LiveDataUtil.combineLatest(conversationFilter, getConversationList(), Pair::new), filterAndData -> { + this.hasNoConversations = LiveDataUtil.mapAsync(LiveDataUtil.combineLatest(conversationFilterRequest, getConversationList(), Pair::new), filterAndData -> { pinnedCount = SignalDatabase.threads().getPinnedConversationListCount(ConversationFilter.OFF); if (filterAndData.getSecond().size() > 0) { return false; } else { - return SignalDatabase.threads().getArchivedConversationListCount(filterAndData.getFirst()) == 0; + return SignalDatabase.threads().getArchivedConversationListCount(filterAndData.getFirst().getFilter()) == 0; } }); @@ -211,14 +216,14 @@ class ConversationListViewModel extends ViewModel { setSelection(newSelection); } - void setFiltered(boolean isFiltered) { + void setFiltered(boolean isFiltered, @NonNull ConversationFilterSource conversationFilterSource) { if (isFiltered) { - conversationFilter.setValue(ConversationFilter.UNREAD); + conversationFilterRequest.setValue(new ConversationFilterRequest(ConversationFilter.UNREAD, conversationFilterSource)); if (activeQuery != null) { onSearchQueryUpdated(activeQuery); } } else { - conversationFilter.setValue(ConversationFilter.OFF); + conversationFilterRequest.setValue(new ConversationFilterRequest(ConversationFilter.OFF, conversationFilterSource)); if (activeQuery != null) { onSearchQueryUpdated(activeQuery); } @@ -266,14 +271,14 @@ class ConversationListViewModel extends ViewModel { void onSearchQueryUpdated(String query) { activeQuery = query; - ConversationFilter filter = conversationFilter.getValue(); + ConversationFilter filter = Objects.requireNonNull(conversationFilterRequest.getValue()).getFilter(); if (filter != ConversationFilter.OFF) { - contactSearchDebouncer.publish(() -> submitConversationSearch(query)); + contactSearchDebouncer.publish(() -> submitConversationSearch(query, filter)); return; } contactSearchDebouncer.publish(() -> { - submitConversationSearch(query); + submitConversationSearch(query, filter); searchRepository.queryContacts(query, result -> { if (!result.getQuery().equals(activeQuery)) { @@ -305,8 +310,8 @@ class ConversationListViewModel extends ViewModel { }); } - private void submitConversationSearch(@NonNull String query) { - searchRepository.queryThreads(query, result -> { + private void submitConversationSearch(@NonNull String query, @NonNull ConversationFilter conversationFilter) { + searchRepository.queryThreads(query, conversationFilter == ConversationFilter.UNREAD, result -> { if (!result.getQuery().equals(activeQuery)) { return; } @@ -315,7 +320,7 @@ class ConversationListViewModel extends ViewModel { activeSearchResult = SearchResult.EMPTY; } - activeSearchResult = activeSearchResult.merge(result); + activeSearchResult = activeSearchResult.merge(result).merge(conversationFilter); searchResult.postValue(activeSearchResult); }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterRequest.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterRequest.kt new file mode 100644 index 0000000000..6b061482c8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterRequest.kt @@ -0,0 +1,8 @@ +package org.thoughtcrime.securesms.conversationlist.chatfilter + +import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter + +data class ConversationFilterRequest( + val filter: ConversationFilter, + val source: ConversationFilterSource +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterSource.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterSource.kt new file mode 100644 index 0000000000..a9c055bee1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationFilterSource.kt @@ -0,0 +1,16 @@ +package org.thoughtcrime.securesms.conversationlist.chatfilter + +/** + * Describes where the chat filter was applied from. + */ +enum class ConversationFilterSource { + /** + * User pulled and released the pull view. + */ + DRAG, + + /** + * User utilized the menu item in the overflow. + */ + OVERFLOW +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt index 206421cf6e..80cd099b91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/chatfilter/ConversationListFilterPullView.kt @@ -65,14 +65,14 @@ class ConversationListFilterPullView @JvmOverloads constructor( binding.filterCircle.progress = progress if (state == FilterPullState.CLOSED && progress <= 0) { - setState(FilterPullState.CLOSED) + setState(FilterPullState.CLOSED, ConversationFilterSource.DRAG) } else if (state == FilterPullState.CLOSED && progress >= 1f) { - setState(FilterPullState.OPEN_APEX) + setState(FilterPullState.OPEN_APEX, ConversationFilterSource.DRAG) vibrate() resetHelpText() resetPillColor() } else if (state == FilterPullState.OPEN && progress >= 1f) { - setState(FilterPullState.CLOSE_APEX) + setState(FilterPullState.CLOSE_APEX, ConversationFilterSource.DRAG) vibrate() animatePillColor() } @@ -111,19 +111,19 @@ class ConversationListFilterPullView @JvmOverloads constructor( fun onUserDragFinished() { if (state == FilterPullState.OPEN_APEX) { - open() + open(ConversationFilterSource.DRAG) } else if (state == FilterPullState.CLOSE_APEX) { - close() + close(ConversationFilterSource.DRAG) } } fun toggle() { if (state == FilterPullState.OPEN) { - setState(FilterPullState.CLOSE_APEX) - close() + setState(FilterPullState.CLOSE_APEX, ConversationFilterSource.OVERFLOW) + close(ConversationFilterSource.OVERFLOW) } else if (state == FilterPullState.CLOSED) { - setState(FilterPullState.OPEN_APEX) - open() + setState(FilterPullState.OPEN_APEX, ConversationFilterSource.OVERFLOW) + open(ConversationFilterSource.OVERFLOW) } } @@ -131,14 +131,14 @@ class ConversationListFilterPullView @JvmOverloads constructor( return state == FilterPullState.OPEN } - private fun open() { - setState(FilterPullState.OPENING) - animatePillIn() + private fun open(source: ConversationFilterSource) { + setState(FilterPullState.OPENING, source) + animatePillIn(source) } - private fun close() { - setState(FilterPullState.CLOSING) - animatePillOut() + private fun close(source: ConversationFilterSource) { + setState(FilterPullState.CLOSING, source) + animatePillOut(source) } private fun resetHelpText() { @@ -152,7 +152,7 @@ class ConversationListFilterPullView @JvmOverloads constructor( }) } - private fun animatePillIn() { + private fun animatePillIn(source: ConversationFilterSource) { binding.filterText.visibility = VISIBLE binding.filterText.alpha = 0f binding.filterText.isEnabled = true @@ -162,20 +162,20 @@ class ConversationListFilterPullView @JvmOverloads constructor( startDelay = 300 duration = 300 doOnEnd { - setState(FilterPullState.OPEN) + setState(FilterPullState.OPEN, source) } start() } } - private fun animatePillOut() { + private fun animatePillOut(source: ConversationFilterSource) { pillAnimator?.cancel() pillAnimator = ObjectAnimator.ofFloat(binding.filterText, ALPHA, 0f).apply { duration = 150 doOnEnd { binding.filterText.visibility = GONE binding.filterText.isEnabled = false - postDelayed({ setState(FilterPullState.CLOSED) }, 150) + postDelayed({ setState(FilterPullState.CLOSED, source) }, 150) } start() } @@ -198,10 +198,10 @@ class ConversationListFilterPullView @JvmOverloads constructor( binding.filterText.chipBackgroundColor = ColorStateList.valueOf(pillDefaultBackgroundTint) } - private fun setState(state: FilterPullState) { + private fun setState(state: FilterPullState, source: ConversationFilterSource) { this.state = state binding.filterCircle.state = state - onFilterStateChanged?.newState(state) + onFilterStateChanged?.newState(state, source) } private fun vibrate() { @@ -211,7 +211,7 @@ class ConversationListFilterPullView @JvmOverloads constructor( } interface OnFilterStateChanged { - fun newState(state: FilterPullState) + fun newState(state: FilterPullState, source: ConversationFilterSource) } interface OnCloseClicked { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/model/ConversationReader.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/model/ConversationReader.java index d973a2be00..cdea1718bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/model/ConversationReader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/model/ConversationReader.java @@ -14,9 +14,12 @@ public class ConversationReader extends ThreadTable.StaticReader { public static final String[] HEADER_COLUMN = { "header" }; public static final String[] ARCHIVED_COLUMNS = { "header", "count" }; + public static final String[] FILTER_FOOTER_COLUMNS = { "header", "show_tip" }; 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() }; + + public static final long TYPE_NONE = 0x0; + public static final long TYPE_SHOW_TIP = 0x1; private final Cursor cursor; @@ -29,6 +32,10 @@ public class ConversationReader extends ThreadTable.StaticReader { return new String[]{Conversation.Type.ARCHIVED_FOOTER.toString(), String.valueOf(archivedCount)}; } + public static String[] createConversationFilterFooterRow(boolean showTip) { + return new String[]{Conversation.Type.CONVERSATION_FILTER_FOOTER.toString(), String.valueOf(showTip ? 1 : 0)}; + } + @Override public ThreadRecord getCurrent() { if (cursor.getColumnIndex(HEADER_COLUMN[0]) == -1) { @@ -45,15 +52,21 @@ public class ConversationReader extends ThreadTable.StaticReader { count = CursorUtil.requireInt(cursor, ARCHIVED_COLUMNS[1]); } - return buildThreadRecordForType(type, count); + boolean showTip = false; + if (type == Conversation.Type.CONVERSATION_FILTER_FOOTER) { + showTip = CursorUtil.requireBoolean(cursor, FILTER_FOOTER_COLUMNS[1]); + } + + return buildThreadRecordForType(type, count, showTip); } - public static ThreadRecord buildThreadRecordForType(@NonNull Conversation.Type type, int count) { + public static ThreadRecord buildThreadRecordForType(@NonNull Conversation.Type type, int count, boolean showTip) { return new ThreadRecord.Builder(-(100 + type.ordinal())) .setBody(type.toString()) .setDate(100) .setRecipient(Recipient.UNKNOWN) .setUnreadCount(count) + .setType(showTip ? TYPE_SHOW_TIP : TYPE_NONE) .build(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index 8b96db04fd..df1f90d34a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -615,7 +615,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa } } - fun getFilteredConversationList(filter: List): Cursor? { + fun getFilteredConversationList(filter: List, unreadOnly: Boolean): Cursor? { if (filter.isEmpty()) { return null } @@ -625,7 +625,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa val cursors: MutableList = LinkedList() for (recipientIds in splitRecipientIds) { - var selection = "$TABLE_NAME.$RECIPIENT_ID = ?" + var selection = "($TABLE_NAME.$RECIPIENT_ID = ?" val selectionArgs = arrayOfNulls(recipientIds.size) for (i in 0 until recipientIds.size - 1) { @@ -638,6 +638,12 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa i++ } + selection += if (unreadOnly) { + ") AND $TABLE_NAME.$READ != ${ReadStatus.READ.serialize()}" + } else { + ")" + } + val query = createQuery(selection, "$DATE DESC", 0, 0) cursors.add(db.rawQuery(query, selectionArgs)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java index bf87fda54c..bab344c25e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/UiHints.java @@ -7,9 +7,12 @@ import java.util.List; public class UiHints extends SignalStoreValues { + private static final int NEVER_DISPLAY_PULL_TO_FILTER_TIP_THRESHOLD = 3; + private static final String HAS_SEEN_GROUP_SETTINGS_MENU_TOAST = "uihints.has_seen_group_settings_menu_toast"; private static final String HAS_CONFIRMED_DELETE_FOR_EVERYONE_ONCE = "uihints.has_confirmed_delete_for_everyone_once"; private static final String HAS_SET_OR_SKIPPED_USERNAME_CREATION = "uihints.has_set_or_skipped_username_creation"; + private static final String NEVER_DISPLAY_PULL_TO_FILTER_TIP = "uihints.never_display_pull_to_filter_tip"; UiHints(@NonNull KeyValueStore store) { super(store); @@ -22,7 +25,7 @@ public class UiHints extends SignalStoreValues { @Override @NonNull List getKeysToIncludeInBackup() { - return Collections.emptyList(); + return Collections.singletonList(NEVER_DISPLAY_PULL_TO_FILTER_TIP); } public void markHasSeenGroupSettingsMenuToast() { @@ -48,4 +51,21 @@ public class UiHints extends SignalStoreValues { public void markHasSetOrSkippedUsernameCreation() { putBoolean(HAS_SET_OR_SKIPPED_USERNAME_CREATION, true); } + + public void resetNeverDisplayPullToRefreshCount() { + putInteger(NEVER_DISPLAY_PULL_TO_FILTER_TIP, 0); + } + + public boolean canDisplayPullToFilterTip() { + return getNeverDisplayPullToFilterTip() < NEVER_DISPLAY_PULL_TO_FILTER_TIP_THRESHOLD; + } + + public void incrementNeverDisplayPullToFilterTip() { + int inc = Math.min(NEVER_DISPLAY_PULL_TO_FILTER_TIP_THRESHOLD, getNeverDisplayPullToFilterTip() + 1); + putInteger(NEVER_DISPLAY_PULL_TO_FILTER_TIP, inc); + } + + private int getNeverDisplayPullToFilterTip() { + return getInteger(NEVER_DISPLAY_PULL_TO_FILTER_TIP, 0); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java index f06807b170..d9c457038d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java @@ -80,10 +80,10 @@ public class SearchRepository { this.serialExecutor = new SerialExecutor(SignalExecutors.BOUNDED); } - public void queryThreads(@NonNull String query, @NonNull Consumer callback) { + public void queryThreads(@NonNull String query, boolean unreadOnly, @NonNull Consumer callback) { searchExecutor.execute(2, () -> { long start = System.currentTimeMillis(); - List result = queryConversations(query); + List result = queryConversations(query, unreadOnly); Log.d(TAG, "[threads] Search took " + (System.currentTimeMillis() - start) + " ms"); @@ -155,7 +155,7 @@ public class SearchRepository { } } - private @NonNull List queryConversations(@NonNull String query) { + private @NonNull List queryConversations(@NonNull String query, boolean unreadOnly) { if (Util.isEmpty(query)) { return Collections.emptyList(); } @@ -192,15 +192,15 @@ public class SearchRepository { List output = new ArrayList<>(contactIds.size() + groupsByTitleIds.size() + groupsByMemberIds.size()); - output.addAll(getMatchingThreads(contactIds)); - output.addAll(getMatchingThreads(groupsByTitleIds)); - output.addAll(getMatchingThreads(groupsByMemberIds)); + output.addAll(getMatchingThreads(contactIds, unreadOnly)); + output.addAll(getMatchingThreads(groupsByTitleIds, unreadOnly)); + output.addAll(getMatchingThreads(groupsByMemberIds, unreadOnly)); return output; } - private List getMatchingThreads(@NonNull Collection recipientIds) { - try (Cursor cursor = threadTable.getFilteredConversationList(new ArrayList<>(recipientIds))) { + private List getMatchingThreads(@NonNull Collection recipientIds, boolean unreadOnly) { + try (Cursor cursor = threadTable.getFilteredConversationList(new ArrayList<>(recipientIds), unreadOnly)) { return readToList(cursor, new ThreadModelBuilder(threadTable)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchResult.kt b/app/src/main/java/org/thoughtcrime/securesms/search/SearchResult.kt index b11b1f27f3..4099c93b4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchResult.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.search +import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.recipients.Recipient @@ -11,7 +12,8 @@ data class SearchResult( val query: String, val contacts: List, val conversations: List, - val messages: List + val messages: List, + val conversationFilter: ConversationFilter ) { fun size(): Int { return contacts.size + conversations.size + messages.size @@ -32,8 +34,12 @@ data class SearchResult( return this.copy(messages = result.results, query = result.query) } + fun merge(conversationFilter: ConversationFilter): SearchResult { + return this.copy(conversationFilter = conversationFilter) + } + companion object { @JvmField - val EMPTY = SearchResult("", emptyList(), emptyList(), emptyList()) + val EMPTY = SearchResult("", emptyList(), emptyList(), emptyList(), ConversationFilter.OFF) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 52b92ec25b..520530c690 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -114,8 +114,7 @@ public final class FeatureFlags { @VisibleForTesting static final Set REMOTE_CAPABLE = SetUtil.newHashSet( PAYMENTS_KILL_SWITCH, - GROUPS_V2_RECOMMENDED_LIMIT, - GROUPS_V2_HARD_LIMIT, + GROUPS_V2_RECOMMENDED_LIMIT, GROUPS_V2_HARD_LIMIT, INTERNAL_USER, VERIFY_V2, CLIENT_EXPIRATION, diff --git a/app/src/main/res/layout/conversation_list_item_clear_filter.xml b/app/src/main/res/layout/conversation_list_item_clear_filter.xml index 27005df466..bc7aafb282 100644 --- a/app/src/main/res/layout/conversation_list_item_clear_filter.xml +++ b/app/src/main/res/layout/conversation_list_item_clear_filter.xml @@ -1,8 +1,10 @@ - + android:gravity="center" + android:minHeight="88dp" + android:orientation="vertical"> - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conversation_list_item_clear_filter_empty.xml b/app/src/main/res/layout/conversation_list_item_clear_filter_empty.xml index f74377d2da..c2598927ba 100644 --- a/app/src/main/res/layout/conversation_list_item_clear_filter_empty.xml +++ b/app/src/main/res/layout/conversation_list_item_clear_filter_empty.xml @@ -21,4 +21,14 @@ android:layout_gravity="center" android:text="@string/ConversationListFragment__clear_filter" android:textColor="@color/signal_colorOnSurfaceVariant" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bdf059a0cc..a0165f71c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5469,5 +5469,7 @@ Filtered by unread Pull to filter + + Tip: Pull down on the chat list to filter diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversationlist/UnarchivedConversationListDataSourceTest.java b/app/src/test/java/org/thoughtcrime/securesms/conversationlist/UnarchivedConversationListDataSourceTest.java index b7f293adfd..6f37b52d13 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversationlist/UnarchivedConversationListDataSourceTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/conversationlist/UnarchivedConversationListDataSourceTest.java @@ -54,7 +54,7 @@ public class UnarchivedConversationListDataSourceTest { when(SignalDatabase.threads()).thenReturn(threadTable); when(ApplicationDependencies.getDatabaseObserver()).thenReturn(mock(DatabaseObserver.class)); - testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(ConversationFilter.OFF); + testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(ConversationFilter.OFF, false); } @Test @@ -266,7 +266,7 @@ public class UnarchivedConversationListDataSourceTest { @Test public void givenHasNoArchivedAndIsFiltered_whenIGetCursor_thenIExpectConversationFilterFooter() { // GIVEN - ConversationListDataSource.UnarchivedConversationListDataSource testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(ConversationFilter.UNREAD); + ConversationListDataSource.UnarchivedConversationListDataSource testSubject = new ConversationListDataSource.UnarchivedConversationListDataSource(ConversationFilter.UNREAD, false); setupThreadDatabaseCursors(0, 3); when(threadTable.getPinnedConversationListCount(ConversationFilter.UNREAD)).thenReturn(0); when(threadTable.getUnarchivedConversationListCount(ConversationFilter.UNREAD)).thenReturn(3);