From 829c06ab1a533da65cbae277ffa49a68849bb944 Mon Sep 17 00:00:00 2001 From: Rashad Sookram Date: Fri, 7 Jan 2022 11:47:57 -0500 Subject: [PATCH] Implement bottom selection menu in chat. --- .../securesms/components/InputPanel.java | 8 +- .../conversation/ConversationActivity.java | 5 + .../conversation/ConversationFragment.java | 142 ++++++++++-------- .../drawable-ldrtl/ic_forward_24_tinted.xml | 5 + .../res/drawable-ldrtl/ic_reply_24_tinted.xml | 9 ++ .../drawable-night/ic_delete_tinted_24.xml | 9 ++ .../main/res/drawable/ic_delete_tinted_24.xml | 9 ++ .../res/drawable/ic_forward_24_tinted.xml | 10 ++ .../main/res/drawable/ic_reply_24_tinted.xml | 5 + .../main/res/layout/conversation_activity.xml | 2 +- .../main/res/layout/conversation_fragment.xml | 13 +- .../main/res/menu/conversation_context.xml | 46 ------ app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 28 +++- 14 files changed, 172 insertions(+), 120 deletions(-) create mode 100644 app/src/main/res/drawable-ldrtl/ic_forward_24_tinted.xml create mode 100644 app/src/main/res/drawable-ldrtl/ic_reply_24_tinted.xml create mode 100644 app/src/main/res/drawable-night/ic_delete_tinted_24.xml create mode 100644 app/src/main/res/drawable/ic_delete_tinted_24.xml create mode 100644 app/src/main/res/drawable/ic_forward_24_tinted.xml create mode 100644 app/src/main/res/drawable/ic_reply_24_tinted.xml delete mode 100644 app/src/main/res/menu/conversation_context.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java index 9188b3323c..696c8bcb7c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java @@ -96,6 +96,7 @@ public class InputPanel extends LinearLayout private boolean hideForGroupState; private boolean hideForBlockedState; private boolean hideForSearch; + private boolean hideForSelection; private ConversationStickerSuggestionAdapter stickerSuggestionAdapter; @@ -336,6 +337,11 @@ public class InputPanel extends LinearLayout updateVisibility(); } + public void setHideForSelection(boolean hideForSelection) { + this.hideForSelection = hideForSelection; + updateVisibility(); + } + @Override public void onRecordPermissionRequired() { if (listener != null) listener.onRecorderPermissionRequired(); @@ -515,7 +521,7 @@ public class InputPanel extends LinearLayout } private void updateVisibility() { - if (hideForGroupState || hideForBlockedState || hideForSearch) { + if (hideForGroupState || hideForBlockedState || hideForSearch || hideForSelection) { setVisibility(GONE); } else { setVisibility(VISIBLE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index f451b90ee1..45af537578 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -3775,6 +3775,11 @@ public class ConversationActivity extends PassphraseRequiredActivity searchViewItem.collapseActionView(); } + @Override + public void onBottomActionBarVisibilityChanged(int visibility) { + inputPanel.setHideForSelection(visibility == View.VISIBLE); + } + @Override public void onForwardClicked() { inputPanel.clearQuote(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index b662a23c96..98b991c388 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -25,6 +25,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; +import android.graphics.Color; import android.graphics.Rect; import android.net.Uri; import android.os.AsyncTask; @@ -33,7 +34,6 @@ import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; -import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -65,16 +65,19 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; +import org.signal.core.util.DimensionUnit; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.menu.ActionItem; +import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar; import org.thoughtcrime.securesms.verify.VerifyIdentityActivity; import org.thoughtcrime.securesms.components.ConversationScrollToView; import org.thoughtcrime.securesms.components.ConversationTypingView; -import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.TypingStatusRepository; import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager; import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity; @@ -162,12 +165,12 @@ import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.WindowUtil; import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; -import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar; import org.thoughtcrime.securesms.wallpaper.ChatWallpaper; import org.whispersystems.libsignal.util.guava.Optional; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -224,6 +227,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect private LayoutTransition layoutTransition; private TransitionListener transitionListener; private View reactionsShade; + private SignalBottomActionBar bottomActionBar; private GiphyMp4ProjectionRecycler giphyMp4ProjectionRecycler; private Colorizer colorizer; @@ -265,6 +269,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect scrollDateHeader = view.findViewById(R.id.scroll_date_header); toolbarShadow = requireActivity().findViewById(R.id.conversation_toolbar_shadow); reactionsShade = view.findViewById(R.id.reactions_shade); + bottomActionBar = view.findViewById(R.id.conversation_bottom_action_bar); final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true); final ConversationItemAnimator conversationItemAnimator = new ConversationItemAnimator( @@ -283,6 +288,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect () -> conversationViewModel.getWallpaper().getValue()); list.setHasFixedSize(false); + layoutManager.setStackFromEnd(true); list.setLayoutManager(layoutManager); RecyclerViewColorizer recyclerViewColorizer = new RecyclerViewColorizer(list); @@ -739,7 +745,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect }); } - private void setCorrectActionModeMenuVisibility(@NonNull Menu menu) { + private void setCorrectActionModeMenuVisibility() { Set selectedParts = getListAdapter().getSelectedItems(); if (actionMode != null && selectedParts.size() == 0) { @@ -747,17 +753,56 @@ public class ConversationFragment extends LoggingFragment implements Multiselect return; } + setBottomActionBarVisibility(true); + MenuState menuState = MenuState.getMenuState(recipient.get(), selectedParts, messageRequestViewModel.shouldShowMessageRequest(), groupViewModel.isNonAdminInAnnouncementGroup()); - menu.findItem(R.id.menu_context_forward).setVisible(menuState.shouldShowForwardAction()); - menu.findItem(R.id.menu_context_reply).setVisible(menuState.shouldShowReplyAction()); - menu.findItem(R.id.menu_context_details).setVisible(menuState.shouldShowDetailsAction()); - menu.findItem(R.id.menu_context_save_attachment).setVisible(menuState.shouldShowSaveAttachmentAction()); - menu.findItem(R.id.menu_context_resend).setVisible(menuState.shouldShowResendAction()); - menu.findItem(R.id.menu_context_copy).setVisible(menuState.shouldShowCopyAction()); - menu.findItem(R.id.menu_context_delete_message).setVisible(menuState.shouldShowDeleteAction()); + List items = new ArrayList<>(); - AdaptiveActionsToolbar.adjustMenuActions(menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth()); + if (menuState.shouldShowReplyAction()) { + items.add(new ActionItem(R.drawable.ic_reply_24_tinted, getResources().getString(R.string.conversation_context__menu_reply_to_message), () -> { + maybeShowSwipeToReplyTooltip(); + handleReplyMessage(getSelectedConversationMessage()); + actionMode.finish(); + })); + } + + if (menuState.shouldShowForwardAction()) { + items.add(new ActionItem(R.drawable.ic_forward_24_tinted, getResources().getString(R.string.conversation_context__menu_forward_message), () -> handleForwardMessageParts(selectedParts))); + } + + if (menuState.shouldShowSaveAttachmentAction()) { + items.add(new ActionItem(R.drawable.ic_save_24, getResources().getString(R.string.conversation_context_image__save_attachment), () -> { + handleSaveAttachment((MediaMmsMessageRecord) getSelectedConversationMessage().getMessageRecord()); + })); + } + + if (menuState.shouldShowCopyAction()) { + items.add(new ActionItem(R.drawable.ic_copy_24_tinted, getResources().getString(R.string.conversation_context__menu_copy_text), () -> { + handleCopyMessage(selectedParts); + })); + } + + if (menuState.shouldShowDetailsAction()) { + items.add(new ActionItem(R.drawable.ic_info_tinted_24, getResources().getString(R.string.conversation_context__menu_message_details), () -> { + handleDisplayDetails(getSelectedConversationMessage()); + })); + } + + if (menuState.shouldShowDeleteAction()) { + items.add(new ActionItem(R.drawable.ic_delete_tinted_24, getResources().getString(R.string.conversation_context__menu_delete_message), () -> handleDeleteMessages(selectedParts))); + } + + bottomActionBar.setItems(items); + } + + private void setBottomActionBarVisibility(boolean isVisible) { + int visibility = isVisible ? View.VISIBLE : View.GONE; + bottomActionBar.setVisibility(visibility); + listener.onBottomActionBarVisibilityChanged(visibility); + + int bottomPadding = isVisible ? (int) DimensionUnit.DP.toPixels(84) : getResources().getDimensionPixelSize(R.dimen.conversation_bottom_padding); + list.setPadding(list.getPaddingLeft(), list.getPaddingTop(), list.getPaddingRight(), bottomPadding); } private @Nullable ConversationAdapter getListAdapter() { @@ -769,9 +814,12 @@ public class ConversationFragment extends LoggingFragment implements Multiselect } private ConversationMessage getSelectedConversationMessage() { - Set messageRecords = getListAdapter().getSelectedItems(); + Set messageRecords = Stream.of(getListAdapter().getSelectedItems()) + .map(MultiselectPart::getConversationMessage) + .distinct() + .collect(Collectors.toSet()); - if (messageRecords.size() == 1) return messageRecords.stream().findFirst().get().getConversationMessage(); + if (messageRecords.size() == 1) return messageRecords.stream().findFirst().get(); else throw new AssertionError(); } @@ -1130,11 +1178,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect if (!TextSecurePreferences.hasSeenSwipeToReplyTooltip(requireContext())) { int text = ViewUtil.isLtr(requireContext()) ? R.string.ConversationFragment_you_can_swipe_to_the_right_reply : R.string.ConversationFragment_you_can_swipe_to_the_left_reply; - TooltipPopup.forTarget(requireActivity().findViewById(R.id.menu_context_reply)) - .setText(text) - .setTextColor(getResources().getColor(R.color.core_white)) - .setBackgroundTint(getResources().getColor(R.color.core_ultramarine)) - .show(TooltipPopup.POSITION_BELOW); + Snackbar.make(list, text, Snackbar.LENGTH_LONG) + .setTextColor(Color.WHITE) + .show(); TextSecurePreferences.setHasSeenSwipeToReplyTooltip(requireContext(), true); } @@ -1204,15 +1250,17 @@ public class ConversationFragment extends LoggingFragment implements Multiselect private @NonNull String calculateSelectedItemCount() { ConversationAdapter adapter = getListAdapter(); - if (adapter == null || adapter.getSelectedItems().isEmpty()) { - return String.valueOf(0); + int count = 0; + if (adapter != null && !adapter.getSelectedItems().isEmpty()) { + count = (int) adapter.getSelectedItems() + .stream() + .map(MultiselectPart::getConversationMessage) + .distinct() + .count(); } - return String.valueOf(adapter.getSelectedItems() - .stream() - .map(MultiselectPart::getConversationMessage) - .distinct() - .count()); + return requireContext().getResources().getQuantityString(R.plurals.conversation_context__s_selected, count, count); + } @Override @@ -1227,6 +1275,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect void setThreadId(long threadId); void handleReplyMessage(ConversationMessage conversationMessage); void onMessageActionToolbarOpened(); + void onBottomActionBarVisibilityChanged(int visibility); void onForwardClicked(); void onMessageRequest(@NonNull MessageRequestViewModel viewModel); void handleReaction(@NonNull ConversationMessage conversationMessage, @@ -1322,7 +1371,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect if (getListAdapter().getSelectedItems().size() == 0) { actionMode.finish(); } else { - setCorrectActionModeMenuVisibility(actionMode.getMenu()); + setCorrectActionModeMenuVisibility(); actionMode.setTitle(calculateSelectedItemCount()); } } @@ -1806,12 +1855,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { - MenuInflater inflater = mode.getMenuInflater(); - inflater.inflate(R.menu.conversation_context, menu); - mode.setTitle(calculateSelectedItemCount()); - setCorrectActionModeMenuVisibility(menu); + setCorrectActionModeMenuVisibility(); listener.onMessageActionToolbarOpened(); return true; } @@ -1825,44 +1871,12 @@ public class ConversationFragment extends LoggingFragment implements Multiselect public void onDestroyActionMode(ActionMode mode) { ((ConversationAdapter)list.getAdapter()).clearSelection(); list.invalidateItemDecorations(); + setBottomActionBarVisibility(false); actionMode = null; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - if (actionMode == null) return false; - - switch(item.getItemId()) { - case R.id.menu_context_copy: - handleCopyMessage(getListAdapter().getSelectedItems()); - actionMode.finish(); - return true; - case R.id.menu_context_delete_message: - handleDeleteMessages(getListAdapter().getSelectedItems()); - actionMode.finish(); - return true; - case R.id.menu_context_details: - handleDisplayDetails(getSelectedConversationMessage()); - actionMode.finish(); - return true; - case R.id.menu_context_forward: - handleForwardMessageParts(getListAdapter().getSelectedItems()); - return true; - case R.id.menu_context_resend: - handleResendMessage(getSelectedConversationMessage().getMessageRecord()); - actionMode.finish(); - return true; - case R.id.menu_context_save_attachment: - handleSaveAttachment((MediaMmsMessageRecord) getSelectedConversationMessage().getMessageRecord()); - actionMode.finish(); - return true; - case R.id.menu_context_reply: - maybeShowSwipeToReplyTooltip(); - handleReplyMessage(getSelectedConversationMessage()); - actionMode.finish(); - return true; - } - return false; } } diff --git a/app/src/main/res/drawable-ldrtl/ic_forward_24_tinted.xml b/app/src/main/res/drawable-ldrtl/ic_forward_24_tinted.xml new file mode 100644 index 0000000000..a4074dded0 --- /dev/null +++ b/app/src/main/res/drawable-ldrtl/ic_forward_24_tinted.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable-ldrtl/ic_reply_24_tinted.xml b/app/src/main/res/drawable-ldrtl/ic_reply_24_tinted.xml new file mode 100644 index 0000000000..0e77677c20 --- /dev/null +++ b/app/src/main/res/drawable-ldrtl/ic_reply_24_tinted.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable-night/ic_delete_tinted_24.xml b/app/src/main/res/drawable-night/ic_delete_tinted_24.xml new file mode 100644 index 0000000000..c255646ce7 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_delete_tinted_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_tinted_24.xml b/app/src/main/res/drawable/ic_delete_tinted_24.xml new file mode 100644 index 0000000000..a87c94dadd --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_tinted_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_forward_24_tinted.xml b/app/src/main/res/drawable/ic_forward_24_tinted.xml new file mode 100644 index 0000000000..bf809d6b8d --- /dev/null +++ b/app/src/main/res/drawable/ic_forward_24_tinted.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_reply_24_tinted.xml b/app/src/main/res/drawable/ic_reply_24_tinted.xml new file mode 100644 index 0000000000..5494c4ce9d --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_24_tinted.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/conversation_activity.xml b/app/src/main/res/layout/conversation_activity.xml index 5295155fb5..03e200ead2 100644 --- a/app/src/main/res/layout/conversation_activity.xml +++ b/app/src/main/res/layout/conversation_activity.xml @@ -49,7 +49,7 @@ android:layout_height="0dp" android:layout_weight="1"> - diff --git a/app/src/main/res/layout/conversation_fragment.xml b/app/src/main/res/layout/conversation_fragment.xml index 46698a5e30..8f211ff613 100644 --- a/app/src/main/res/layout/conversation_fragment.xml +++ b/app/src/main/res/layout/conversation_fragment.xml @@ -22,7 +22,7 @@ android:cacheColorHint="@color/signal_background_primary" android:clipChildren="false" android:clipToPadding="false" - android:paddingBottom="2dp" + android:paddingBottom="@dimen/conversation_bottom_padding" android:scrollbars="vertical" android:overScrollMode="ifContentScrolls" app:layout_constraintTop_toTopOf="parent" /> @@ -88,4 +88,15 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> + + diff --git a/app/src/main/res/menu/conversation_context.xml b/app/src/main/res/menu/conversation_context.xml deleted file mode 100644 index 584cf78b42..0000000000 --- a/app/src/main/res/menu/conversation_context.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index dbd5ed969c..f94046d31a 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -69,6 +69,7 @@ 4dp + 2dp 40dp 16dp 16dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 445328345a..4300aa44ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2775,18 +2775,32 @@ Signal video call - Message details - Copy text - Delete message - Forward message + + Info + + Copy + + Delete + + Forward + Resend message - Reply to message + + Reply - + + Select multiple + + + %d selected + %d selected + + - Save attachment + + Save Disappearing messages