From 3943e670b2dd300de64f8ff8859c3e38e5ec4533 Mon Sep 17 00:00:00 2001 From: Rashad Sookram Date: Tue, 11 Jan 2022 09:36:21 -0500 Subject: [PATCH] Implement bottom selection menu in chat. --- .../securesms/components/InputPanel.java | 8 +- .../conversation/ConversationActivity.java | 5 + .../conversation/ConversationFragment.java | 176 +++++++++++------- .../mutiselect/MultiselectItemDecoration.kt | 18 +- .../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 | 15 +- 15 files changed, 209 insertions(+), 122 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 9188b3323..696c8bcb7 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 f451b90ee..45af53757 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 b662a23c9..cec601512 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; @@ -55,6 +55,7 @@ import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityOptionsCompat; import androidx.core.text.HtmlCompat; import androidx.core.view.ViewCompat; +import androidx.core.view.ViewKt; import androidx.lifecycle.LiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProviders; @@ -65,17 +66,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.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.menu.ActionItem; +import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar; import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager; import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity; import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner; @@ -160,20 +163,23 @@ import org.thoughtcrime.securesms.util.TopToastPopup; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.WindowUtil; +import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; 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.verify.VerifyIdentityActivity; 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; import java.util.Locale; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ExecutionException; import kotlin.Unit; @@ -224,6 +230,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 +272,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( @@ -739,7 +747,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 +755,86 @@ 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()); + actionMode.finish(); + })); + } + + if (menuState.shouldShowCopyAction()) { + items.add(new ActionItem(R.drawable.ic_copy_24_tinted, getResources().getString(R.string.conversation_context__menu_copy_text), () -> { + handleCopyMessage(selectedParts); + actionMode.finish(); + })); + } + + if (menuState.shouldShowDetailsAction()) { + items.add(new ActionItem(R.drawable.ic_info_tinted_24, getResources().getString(R.string.conversation_context__menu_message_details), () -> { + handleDisplayDetails(getSelectedConversationMessage()); + actionMode.finish(); + })); + } + + if (menuState.shouldShowDeleteAction()) { + items.add(new ActionItem(R.drawable.ic_delete_tinted_24, getResources().getString(R.string.conversation_context__menu_delete_message), () -> { + handleDeleteMessages(selectedParts); + actionMode.finish(); + })); + } + + bottomActionBar.setItems(items); + } + + private void setBottomActionBarVisibility(boolean isVisible) { + boolean isCurrentlyVisible = bottomActionBar.getVisibility() == View.VISIBLE; + if (isVisible == isCurrentlyVisible) { + return; + } + + int scrollOffset = (int) DimensionUnit.DP.toPixels(34); + + if (isVisible) { + ViewUtil.animateIn(bottomActionBar, bottomActionBar.getEnterAnimation()); + listener.onBottomActionBarVisibilityChanged(View.VISIBLE); + + list.setPadding(list.getPaddingLeft(), list.getPaddingTop(), list.getPaddingRight(), (int) DimensionUnit.DP.toPixels(88)); + list.scrollBy(0, -scrollOffset); + } else { + ViewUtil.animateOut(bottomActionBar, bottomActionBar.getExitAnimation()) + .addListener(new ListenableFuture.Listener() { + @Override public void onSuccess(Boolean result) { + listener.onBottomActionBarVisibilityChanged(View.GONE); + list.setPadding(list.getPaddingLeft(), list.getPaddingTop(), list.getPaddingRight(), getResources().getDimensionPixelSize(R.dimen.conversation_bottom_padding)); + + ViewKt.doOnPreDraw(list, view -> { + list.scrollBy(0, scrollOffset); + return Unit.INSTANCE; + }); + } + + @Override public void onFailure(ExecutionException e) { + } + }); + } } private @Nullable ConversationAdapter getListAdapter() { @@ -769,9 +846,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 +1210,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 +1282,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 +1307,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 +1403,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 +1887,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 +1903,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/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt index f6ae29a29..915a356cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt @@ -108,8 +108,10 @@ class MultiselectItemDecoration( override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val currentSelection = getCurrentSelection(parent) if (selectedParts.isEmpty() && currentSelection.isNotEmpty()) { + val wasRunning = enterExitAnimation?.isRunning ?: false enterExitAnimation?.end() - enterExitAnimation = ValueAnimator.ofFloat(enterExitAnimation?.animatedFraction ?: 0f, 1f).apply { + val startValue = if (wasRunning) enterExitAnimation?.animatedFraction else 0f + enterExitAnimation = ValueAnimator.ofFloat(startValue ?: 0f, 1f).apply { duration = 150L start() } @@ -142,7 +144,10 @@ class MultiselectItemDecoration( if (adapter.selectedItems.isEmpty()) { drawFocusShadeUnderIfNecessary(canvas, parent) - return + + if (enterExitAnimation == null || !isInitialAnimation()) { + return + } } shadePaint.color = when { @@ -189,7 +194,9 @@ class MultiselectItemDecoration( canvas.restore() } - drawChecks(parent, canvas, adapter) + if (adapter.selectedItems.isNotEmpty()) { + drawChecks(parent, canvas, adapter) + } } /** @@ -312,7 +319,8 @@ class MultiselectItemDecoration( val adapter = parent.adapter as ConversationAdapter val isLtr = ViewUtil.isLtr(child) - if (adapter.selectedItems.isNotEmpty() && child is Multiselectable) { + val isAnimatingSelection = enterExitAnimation != null && isInitialAnimation() + if ((isAnimatingSelection || adapter.selectedItems.isNotEmpty()) && child is Multiselectable) { val target = child.getHorizontalTranslationTarget() if (target != null) { @@ -323,7 +331,7 @@ class MultiselectItemDecoration( } val translation: Float = if (isInitialAnimation()) { - max(0, gutter - start) * (enterExitAnimation?.animatedFraction ?: 1f) + max(0, gutter - start) * (enterExitAnimation?.animatedValue as Float? ?: 1f) } else { max(0, gutter - start).toFloat() } 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 000000000..a4074dded --- /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 000000000..0e77677c2 --- /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 000000000..c255646ce --- /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 000000000..a87c94dad --- /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 000000000..bf809d6b8 --- /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 000000000..5494c4ce9 --- /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 5295155fb..03e200ead 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 46698a5e3..8f211ff61 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 584cf78b4..000000000 --- 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 dbd5ed969..f94046d31 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 ec5a6350b..db42751a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2785,13 +2785,22 @@ Forward Resend message - Reply to message + + Reply - + + Select multiple + + + %d selected + %d selected + + - Save attachment + + Save Disappearing messages