From f533a898f5e36429cfa59d2440e30b175c225b87 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 21 Oct 2021 10:55:33 -0400 Subject: [PATCH] Add new bottom actionbar to chat list. --- .../securesms/components/menu/ActionItem.kt | 13 ++ .../components/menu/SignalBottomActionBar.kt | 92 ++++++++++ .../{ => menu}/SignalContextMenu.kt | 75 ++++++--- .../conversation/ConversationFragment.java | 18 -- .../ConversationListArchiveFragment.java | 5 - .../ConversationListFragment.java | 159 +++++++++--------- .../main/res/drawable/ic_more_horiz_24.xml | 9 + app/src/main/res/drawable/ic_x_24.xml | 9 + .../signal_bottom_action_bar_background.xml | 9 + .../res/layout/action_mode_close_layout.xml | 11 ++ .../res/layout/conversation_list_fragment.xml | 13 ++ .../layout/conversation_list_item_view.xml | 2 +- .../layout/signal_bottom_action_bar_item.xml | 29 ++++ .../main/res/menu/conversation_list_batch.xml | 28 --- .../menu/conversation_list_batch_archive.xml | 11 -- .../res/menu/conversation_list_batch_pin.xml | 16 -- .../conversation_list_batch_unarchive.xml | 11 -- app/src/main/res/values-night/dark_colors.xml | 1 + app/src/main/res/values/light_colors.xml | 1 + app/src/main/res/values/strings.xml | 8 + app/src/main/res/values/styles.xml | 4 +- app/src/main/res/values/text_styles.xml | 6 + app/src/main/res/values/themes.xml | 4 +- 23 files changed, 335 insertions(+), 199 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/menu/ActionItem.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/menu/SignalBottomActionBar.kt rename app/src/main/java/org/thoughtcrime/securesms/components/{ => menu}/SignalContextMenu.kt (74%) create mode 100644 app/src/main/res/drawable/ic_more_horiz_24.xml create mode 100644 app/src/main/res/drawable/ic_x_24.xml create mode 100644 app/src/main/res/drawable/signal_bottom_action_bar_background.xml create mode 100644 app/src/main/res/layout/action_mode_close_layout.xml create mode 100644 app/src/main/res/layout/signal_bottom_action_bar_item.xml delete mode 100644 app/src/main/res/menu/conversation_list_batch.xml delete mode 100644 app/src/main/res/menu/conversation_list_batch_archive.xml delete mode 100644 app/src/main/res/menu/conversation_list_batch_pin.xml delete mode 100644 app/src/main/res/menu/conversation_list_batch_unarchive.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/menu/ActionItem.kt b/app/src/main/java/org/thoughtcrime/securesms/components/menu/ActionItem.kt new file mode 100644 index 000000000..e71e75295 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/menu/ActionItem.kt @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.components.menu + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes + +/** + * Represents an action to be rendered via [SignalContextMenu] or [SignalBottomActionBar] + */ +data class ActionItem( + @DrawableRes val iconRes: Int, + @StringRes val titleRes: Int, + val action: Runnable +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/menu/SignalBottomActionBar.kt b/app/src/main/java/org/thoughtcrime/securesms/components/menu/SignalBottomActionBar.kt new file mode 100644 index 000000000..955224639 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/menu/SignalBottomActionBar.kt @@ -0,0 +1,92 @@ +package org.thoughtcrime.securesms.components.menu + +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.util.ViewUtil + +/** + * A bar that displays a set of action buttons. Intended as a replacement for ActionModes, this gives you a simple interface to add a bunch of actions, and + * the bar itself will handle putting things in the overflow and whatnot. + * + * Overflow items are rendered in a [SignalContextMenu]. + */ +class SignalBottomActionBar(context: Context, attributeSet: AttributeSet) : LinearLayout(context, attributeSet) { + + val items: MutableList = mutableListOf() + + init { + orientation = HORIZONTAL + setBackgroundResource(R.drawable.signal_bottom_action_bar_background) + + if (Build.VERSION.SDK_INT >= 21) { + elevation = 20f + } + } + + fun setItems(items: List) { + this.items.clear() + this.items.addAll(items) + present(this.items) + } + + private fun present(items: List) { + if (width == 0) { + post { present(items) } + return + } + + val widthDp: Float = ViewUtil.pxToDp(width.toFloat()) + val minButtonWidthDp = 70 + val maxButtons: Int = (widthDp / minButtonWidthDp).toInt() + val usableButtonCount = when { + items.size <= maxButtons -> items.size + else -> maxButtons - 1 + } + + val renderableItems: List = items.subList(0, usableButtonCount) + val overflowItems: List = if (renderableItems.size < items.size) items.subList(usableButtonCount, items.size) else emptyList() + + removeAllViews() + + renderableItems.forEach { item -> + val view: View = LayoutInflater.from(context).inflate(R.layout.signal_bottom_action_bar_item, this, false) + addView(view) + bindItem(view, item) + } + + if (overflowItems.isNotEmpty()) { + val view: View = LayoutInflater.from(context).inflate(R.layout.signal_bottom_action_bar_item, this, false) + addView(view) + bindItem( + view, + ActionItem( + iconRes = R.drawable.ic_more_horiz_24, + titleRes = R.string.SignalBottomActionBar_more, + action = { + SignalContextMenu.Builder(view, parent as ViewGroup) + .preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.END) + .offsetY(ViewUtil.dpToPx(8)) + .show(overflowItems) + } + ) + ) + } + } + + private fun bindItem(view: View, item: ActionItem) { + val icon: ImageView = view.findViewById(R.id.signal_bottom_action_bar_item_icon) + val title: TextView = view.findViewById(R.id.signal_bottom_action_bar_item_title) + + icon.setImageResource(item.iconRes) + title.setText(item.titleRes) + view.setOnClickListener { item.action.run() } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/SignalContextMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/components/menu/SignalContextMenu.kt similarity index 74% rename from app/src/main/java/org/thoughtcrime/securesms/components/SignalContextMenu.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/menu/SignalContextMenu.kt index dcfec0476..62d53a8e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/SignalContextMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/menu/SignalContextMenu.kt @@ -1,6 +1,7 @@ -package org.thoughtcrime.securesms.components +package org.thoughtcrime.securesms.components.menu import android.content.Context +import android.graphics.Rect import android.os.Build import android.view.LayoutInflater import android.view.View @@ -8,8 +9,6 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.PopupWindow import android.widget.TextView -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -17,6 +16,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.util.MappingAdapter import org.thoughtcrime.securesms.util.MappingModel import org.thoughtcrime.securesms.util.MappingViewHolder +import org.thoughtcrime.securesms.util.ViewUtil /** * A custom context menu that will show next to an anchor view and display several options. Basically a PopupMenu with custom UI and positioning rules. @@ -27,10 +27,11 @@ import org.thoughtcrime.securesms.util.MappingViewHolder */ class SignalContextMenu private constructor( val anchor: View, - val container: View, - val items: List, + val container: ViewGroup, + val items: List, val baseOffsetX: Int = 0, val baseOffsetY: Int = 0, + val horizontalPosition: HorizontalPosition = HorizontalPosition.START, val onDismiss: Runnable? = null ) : PopupWindow( LayoutInflater.from(anchor.context).inflate(R.layout.signal_context_menu, null), @@ -77,8 +78,14 @@ class SignalContextMenu private constructor( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) ) - val menuBottomBound = anchor.y + anchor.height + contentView.measuredHeight + baseOffsetY - val menuTopBound = anchor.y - contentView.measuredHeight - baseOffsetY + val anchorRect = Rect(anchor.left, anchor.top, anchor.right, anchor.bottom).also { + if (anchor.parent != container) { + container.offsetDescendantRectToMyCoords(anchor, it) + } + } + + val menuBottomBound = anchorRect.bottom + contentView.measuredHeight + baseOffsetY + val menuTopBound = anchorRect.top - contentView.measuredHeight - baseOffsetY val screenBottomBound = container.height val screenTopBound = container.y @@ -88,16 +95,33 @@ class SignalContextMenu private constructor( if (menuBottomBound < screenBottomBound) { offsetY = baseOffsetY } else if (menuTopBound > screenTopBound) { - offsetY = -(anchor.height + contentView.measuredHeight + baseOffsetY) + offsetY = -(anchorRect.height() + contentView.measuredHeight + baseOffsetY) mappingAdapter.submitList(items.reversed().toAdapterItems()) } else { - offsetY = -((anchor.height / 2) + (contentView.measuredHeight / 2) + baseOffsetY) + offsetY = -((anchorRect.height() / 2) + (contentView.measuredHeight / 2) + baseOffsetY) } - showAsDropDown(anchor, baseOffsetX, offsetY) + val offsetX: Int = when (horizontalPosition) { + HorizontalPosition.START -> { + if (ViewUtil.isLtr(context)) { + baseOffsetX + } else { + -(baseOffsetX + contentView.measuredWidth) + } + } + HorizontalPosition.END -> { + if (ViewUtil.isLtr(context)) { + -(baseOffsetX + contentView.measuredWidth - anchorRect.width()) + } else { + baseOffsetX - anchorRect.width() + } + } + } + + showAsDropDown(anchor, offsetX, offsetY) } - private fun List.toAdapterItems(): List { + private fun List.toAdapterItems(): List { return this.mapIndexed { index, item -> val displayType: DisplayType = when { this.size == 1 -> DisplayType.ONLY @@ -110,14 +134,8 @@ class SignalContextMenu private constructor( } } - data class Item( - @DrawableRes val iconRes: Int, - @StringRes val titleRes: Int, - val action: Runnable - ) - private data class DisplayItem( - val item: Item, + val item: ActionItem, val displayType: DisplayType ) : MappingModel { override fun areItemsTheSame(newItem: DisplayItem): Boolean { @@ -129,7 +147,7 @@ class SignalContextMenu private constructor( } } - enum class DisplayType { + private enum class DisplayType { TOP, BOTTOM, MIDDLE, ONLY } @@ -162,18 +180,23 @@ class SignalContextMenu private constructor( } } + enum class HorizontalPosition { + START, END + } + /** * @param anchor The view to put the pop-up on * @param container A parent of [anchor] that represents the acceptable boundaries of the popup */ class Builder( val anchor: View, - val container: View + val container: ViewGroup ) { var onDismiss: Runnable? = null - var offsetX: Int = 0 - var offsetY: Int = 0 + var offsetX = 0 + var offsetY = 0 + var horizontalPosition = HorizontalPosition.START fun onDismiss(onDismiss: Runnable): Builder { this.onDismiss = onDismiss @@ -190,13 +213,19 @@ class SignalContextMenu private constructor( return this } - fun show(items: List) { + fun preferredHorizontalPosition(horizontalPosition: HorizontalPosition): Builder { + this.horizontalPosition = horizontalPosition + return this + } + + fun show(items: List) { SignalContextMenu( anchor = anchor, container = container, items = items, baseOffsetX = offsetX, baseOffsetY = offsetY, + horizontalPosition = horizontalPosition, onDismiss = onDismiss ).show() } 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 084d827f1..218738621 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -1855,8 +1855,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect private class ActionModeCallback implements ActionMode.Callback { - private int statusBarColor; - @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = mode.getMenuInflater(); @@ -1864,16 +1862,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect mode.setTitle(calculateSelectedItemCount()); - if (Build.VERSION.SDK_INT >= 21) { - Window window = getActivity().getWindow(); - statusBarColor = window.getStatusBarColor(); - WindowUtil.setStatusBarColor(window, getResources().getColor(R.color.action_mode_status_bar)); - } - - if (!ThemeUtil.isDarkTheme(getContext())) { - WindowUtil.setLightStatusBar(getActivity().getWindow()); - } - setCorrectActionModeMenuVisibility(menu); listener.onMessageActionToolbarOpened(); return true; @@ -1888,12 +1876,6 @@ public class ConversationFragment extends LoggingFragment implements Multiselect public void onDestroyActionMode(ActionMode mode) { ((ConversationAdapter)list.getAdapter()).clearSelection(); list.invalidateItemDecorations(); - - if (Build.VERSION.SDK_INT >= 21) { - WindowUtil.setStatusBarColor(requireActivity().getWindow(), statusBarColor); - } - - WindowUtil.setLightStatusBarFromTheme(requireActivity()); actionMode = null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java index 63b5b4087..d92bb0313 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java @@ -104,11 +104,6 @@ public class ConversationListArchiveFragment extends ConversationListFragment im return R.plurals.ConversationListFragment_moved_conversations_to_inbox; } - @Override - protected @MenuRes int getActionModeMenuRes() { - return R.menu.conversation_list_batch_unarchive; - } - @Override protected @DrawableRes int getArchiveIconRes() { return R.drawable.ic_unarchive_white_36dp; 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 9884b34fe..d269e9dbf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -45,12 +45,10 @@ import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.annotation.IdRes; -import androidx.annotation.MenuRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.PluralsRes; import androidx.annotation.WorkerThread; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.view.ActionMode; import androidx.appcompat.widget.Toolbar; @@ -80,7 +78,9 @@ import org.thoughtcrime.securesms.NewConversationActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.RatingManager; import org.thoughtcrime.securesms.components.SearchToolbar; -import org.thoughtcrime.securesms.components.SignalContextMenu; +import org.thoughtcrime.securesms.components.menu.ActionItem; +import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar; +import org.thoughtcrime.securesms.components.menu.SignalContextMenu; import org.thoughtcrime.securesms.components.UnreadPaymentsView; import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator; import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton; @@ -148,6 +148,7 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -200,6 +201,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode private VoiceNoteMediaControllerOwner mediaControllerOwner; private Stub voiceNotePlayerViewStub; private VoiceNotePlayerView voiceNotePlayerView; + private SignalBottomActionBar bottomActionBar; private Stopwatch startupStopwatch; @@ -242,6 +244,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow); proxyStatus = view.findViewById(R.id.conversation_list_proxy_status); unreadPaymentsDot = view.findViewById(R.id.unread_payments_indicator); + 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)); searchToolbar = new Stub<>(view.findViewById(R.id.search_toolbar)); @@ -777,10 +780,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode return null; }, none -> { - if (actionMode != null) { - actionMode.finish(); - actionMode = null; - } + endActionModeIfActive(); }); } @@ -792,10 +792,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode StorageSyncHelper.scheduleSyncForDataChange(); return null; }, none -> { - if (actionMode != null) { - actionMode.finish(); - actionMode = null; - } + endActionModeIfActive(); }); } @@ -825,11 +822,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode @Override protected void onPostExecute(Void result) { super.onPostExecute(result); - - if (actionMode != null) { - actionMode.finish(); - actionMode = null; - } + endActionModeIfActive(); } @Override @@ -881,10 +874,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode @Override protected void onPostExecute(Void result) { dialog.dismiss(); - if (actionMode != null) { - actionMode.finish(); - actionMode = null; - } + endActionModeIfActive(); } }.executeOnExecutor(SignalExecutors.BOUNDED); } @@ -906,9 +896,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode Snackbar.LENGTH_LONG) .setTextColor(Color.WHITE) .show(); - if (actionMode != null) { - actionMode.finish(); - } + endActionModeIfActive(); return; } @@ -919,9 +907,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode return null; }, unused -> { - if (actionMode != null) { - actionMode.finish(); - } + endActionModeIfActive(); }); } @@ -933,21 +919,40 @@ public class ConversationListFragment extends MainFragment implements ActionMode return null; }, unused -> { - if (actionMode != null) { - actionMode.finish(); - } + endActionModeIfActive(); }); } private void handleSelectAllThreads() { defaultAdapter.selectAllThreads(); - actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelectionIds().size())); + updateMultiSelectState(); } private void handleCreateConversation(long threadId, Recipient recipient, int distributionType) { getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1); } + private void startActionMode() { + actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this); + ViewUtil.fadeIn(bottomActionBar, 250); + ViewUtil.fadeOut(fab, 250); + ViewUtil.fadeOut(cameraFab, 250); + } + + private void endActionModeIfActive() { + if (actionMode != null) { + endActionMode(); + } + } + + private void endActionMode() { + actionMode.finish(); + actionMode = null; + ViewUtil.fadeOut(bottomActionBar, 250); + ViewUtil.fadeIn(fab, 250); + ViewUtil.fadeIn(cameraFab, 250); + } + private void onSubmitList(@NonNull List conversationList) { defaultAdapter.submitList(conversationList); onPostSubmitList(conversationList.size()); @@ -1016,10 +1021,9 @@ public class ConversationListFragment extends MainFragment implements ActionMode defaultAdapter.toggleConversationInBatchSet(conversation); if (defaultAdapter.getBatchSelectionIds().size() == 0) { - actionMode.finish(); + endActionModeIfActive(); } else { - actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelectionIds().size())); - setCorrectMenuVisibility(actionMode.getMenu()); + updateMultiSelectState(); } } } @@ -1035,35 +1039,35 @@ public class ConversationListFragment extends MainFragment implements ActionMode Collection id = Collections.singleton(conversation.getThreadRecord().getThreadId()); - List items = new ArrayList<>(); + List items = new ArrayList<>(); if (!conversation.getThreadRecord().isArchived()) { if (conversation.getThreadRecord().isRead()) { - items.add(new SignalContextMenu.Item(R.drawable.ic_unread_24, R.string.ConversationListFragment_unread, () -> handleMarkAsUnread(id))); + items.add(new ActionItem(R.drawable.ic_unread_24, R.string.ConversationListFragment_unread, () -> handleMarkAsUnread(id))); } else { - items.add(new SignalContextMenu.Item(R.drawable.ic_read_24, R.string.ConversationListFragment_read, () -> handleMarkAsRead(id))); + items.add(new ActionItem(R.drawable.ic_read_24, R.string.ConversationListFragment_read, () -> handleMarkAsRead(id))); } if (conversation.getThreadRecord().isPinned()) { - items.add(new SignalContextMenu.Item(R.drawable.ic_unpin_24, R.string.ConversationListFragment_unpin, () -> handleUnpin(id))); + items.add(new ActionItem(R.drawable.ic_unpin_24, R.string.ConversationListFragment_unpin, () -> handleUnpin(id))); } else { - items.add(new SignalContextMenu.Item(R.drawable.ic_pin_24, R.string.ConversationListFragment_pin, () -> handlePin(Collections.singleton(conversation)))); + items.add(new ActionItem(R.drawable.ic_pin_24, R.string.ConversationListFragment_pin, () -> handlePin(Collections.singleton(conversation)))); } } - items.add(new SignalContextMenu.Item(R.drawable.ic_select_24, R.string.ConversationListFragment_select, () -> { + items.add(new ActionItem(R.drawable.ic_select_24, R.string.ConversationListFragment_select, () -> { defaultAdapter.initializeBatchMode(true); defaultAdapter.toggleConversationInBatchSet(conversation); - actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(ConversationListFragment.this); + startActionMode(); })); if (conversation.getThreadRecord().isArchived()) { - items.add(new SignalContextMenu.Item(R.drawable.ic_unarchive_24, R.string.ConversationListFragment_unarchive, () -> handleArchive(id, false))); + items.add(new ActionItem(R.drawable.ic_unarchive_24, R.string.ConversationListFragment_unarchive, () -> handleArchive(id, false))); } else { - items.add(new SignalContextMenu.Item(R.drawable.ic_archive_24, R.string.ConversationListFragment_archive, () -> handleArchive(id, false))); + items.add(new ActionItem(R.drawable.ic_archive_24, R.string.ConversationListFragment_archive, () -> handleArchive(id, false))); } - items.add(new SignalContextMenu.Item(R.drawable.ic_delete_24, R.string.ConversationListFragment_delete, () -> handleDelete(id))); + items.add(new ActionItem(R.drawable.ic_delete_24, R.string.ConversationListFragment_delete, () -> handleDelete(id))); new SignalContextMenu.Builder(view, list) .offsetX(ViewUtil.dpToPx(12)) @@ -1076,45 +1080,26 @@ public class ConversationListFragment extends MainFragment implements ActionMode @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { - MenuInflater inflater = getActivity().getMenuInflater(); - - inflater.inflate(R.menu.conversation_list_batch_pin, menu); - inflater.inflate(getActionModeMenuRes(), menu); - inflater.inflate(R.menu.conversation_list_batch, menu); - - mode.setTitle("1"); - - WindowUtil.setStatusBarColor(requireActivity().getWindow(), getResources().getColor(R.color.action_mode_status_bar)); - + mode.setTitle(requireContext().getResources().getQuantityString(R.plurals.ConversationListFragment_s_selected, 1, 1)); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - setCorrectMenuVisibility(menu); + updateMultiSelectState(); return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_select_all: handleSelectAllThreads(); return true; - case R.id.menu_delete_selected: handleDelete(defaultAdapter.getBatchSelectionIds()); return true; - case R.id.menu_pin_selected: handlePin(defaultAdapter.getBatchSelection()); return true; - case R.id.menu_unpin_selected: handleUnpin(defaultAdapter.getBatchSelectionIds()); return true; - case R.id.menu_archive_selected: handleArchive(defaultAdapter.getBatchSelectionIds(), true); return true; - case R.id.menu_mark_as_read: handleMarkAsRead(defaultAdapter.getBatchSelectionIds()); return true; - case R.id.menu_mark_as_unread: handleMarkAsUnread(defaultAdapter.getBatchSelectionIds()); return true; - } - - return false; + return true; } @Override public void onDestroyActionMode(ActionMode mode) { defaultAdapter.initializeBatchMode(false); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT >= 21) { TypedArray color = getActivity().getTheme().obtainStyledAttributes(new int[] {android.R.attr.statusBarColor}); WindowUtil.setStatusBarColor(getActivity().getWindow(), color.getColor(0, Color.BLACK)); color.recycle(); @@ -1131,7 +1116,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode lightStatusBarAttr.recycle(); } - actionMode = null; + endActionModeIfActive(); } @Subscribe(threadMode = ThreadMode.MAIN) @@ -1145,29 +1130,41 @@ public class ConversationListFragment extends MainFragment implements ActionMode closeSearchIfOpen(); } - private void setCorrectMenuVisibility(@NonNull Menu menu) { + private void updateMultiSelectState() { + int count = defaultAdapter.getBatchSelectionIds().size(); boolean hasUnread = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isRead()); boolean hasUnpinned = Stream.of(defaultAdapter.getBatchSelection()).anyMatch(conversation -> !conversation.getThreadRecord().isPinned()); boolean canPin = viewModel.getPinnedCount() < MAXIMUM_PINNED_CONVERSATIONS; + if (actionMode != null) { + actionMode.setTitle(requireContext().getResources().getQuantityString(R.plurals.ConversationListFragment_s_selected, count, count)); + } + + List items = new ArrayList<>(); + if (hasUnread) { - menu.findItem(R.id.menu_mark_as_unread).setVisible(false); - menu.findItem(R.id.menu_mark_as_read).setVisible(true); + items.add(new ActionItem(R.drawable.ic_read_24, R.string.ConversationListFragment_read, () -> handleMarkAsRead(defaultAdapter.getBatchSelectionIds()))); } else { - menu.findItem(R.id.menu_mark_as_unread).setVisible(true); - menu.findItem(R.id.menu_mark_as_read).setVisible(false); + items.add(new ActionItem(R.drawable.ic_unread_24, R.string.ConversationListFragment_unread, () -> handleMarkAsUnread(defaultAdapter.getBatchSelectionIds()))); } if (!isArchived() && hasUnpinned && canPin) { - menu.findItem(R.id.menu_pin_selected).setVisible(true); - menu.findItem(R.id.menu_unpin_selected).setVisible(false); + items.add(new ActionItem(R.drawable.ic_pin_24, R.string.ConversationListFragment_pin, () -> handlePin(defaultAdapter.getBatchSelection()))); } else if (!isArchived() && !hasUnpinned) { - menu.findItem(R.id.menu_pin_selected).setVisible(false); - menu.findItem(R.id.menu_unpin_selected).setVisible(true); - } else { - menu.findItem(R.id.menu_pin_selected).setVisible(false); - menu.findItem(R.id.menu_unpin_selected).setVisible(false); + items.add(new ActionItem(R.drawable.ic_unpin_24, R.string.ConversationListFragment_unpin, () -> handleUnpin(defaultAdapter.getBatchSelectionIds()))); } + + if (isArchived()) { + items.add(new ActionItem(R.drawable.ic_unarchive_24, R.string.ConversationListFragment_unarchive, () -> handleArchive(defaultAdapter.getBatchSelectionIds(), true))); + } else { + items.add(new ActionItem(R.drawable.ic_archive_24, R.string.ConversationListFragment_archive, () -> handleArchive(defaultAdapter.getBatchSelectionIds(), true))); + } + + items.add(new ActionItem(R.drawable.ic_delete_24, R.string.ConversationListFragment_delete, () -> handleDelete(defaultAdapter.getBatchSelectionIds()))); + + items.add(new ActionItem(R.drawable.ic_select_24, R.string.ConversationListFragment_select_all, this::handleSelectAllThreads)); + + bottomActionBar.setItems(items); } protected Toolbar getToolbar(@NonNull View rootView) { @@ -1178,10 +1175,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode return R.plurals.ConversationListFragment_conversations_archived; } - protected @MenuRes int getActionModeMenuRes() { - return R.menu.conversation_list_batch_archive; - } - protected @DrawableRes int getArchiveIconRes() { return R.drawable.ic_archive_white_36dp; } diff --git a/app/src/main/res/drawable/ic_more_horiz_24.xml b/app/src/main/res/drawable/ic_more_horiz_24.xml new file mode 100644 index 000000000..a7f6f8ff9 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horiz_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_x_24.xml b/app/src/main/res/drawable/ic_x_24.xml new file mode 100644 index 000000000..a07682ea0 --- /dev/null +++ b/app/src/main/res/drawable/ic_x_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/signal_bottom_action_bar_background.xml b/app/src/main/res/drawable/signal_bottom_action_bar_background.xml new file mode 100644 index 000000000..e66e27557 --- /dev/null +++ b/app/src/main/res/drawable/signal_bottom_action_bar_background.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/action_mode_close_layout.xml b/app/src/main/res/layout/action_mode_close_layout.xml new file mode 100644 index 000000000..71c0a7e99 --- /dev/null +++ b/app/src/main/res/layout/action_mode_close_layout.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml index dda78ebe6..b5f9b0420 100644 --- a/app/src/main/res/layout/conversation_list_fragment.xml +++ b/app/src/main/res/layout/conversation_list_fragment.xml @@ -253,4 +253,17 @@ + + + diff --git a/app/src/main/res/layout/conversation_list_item_view.xml b/app/src/main/res/layout/conversation_list_item_view.xml index b65ed3075..615aeb6f5 100644 --- a/app/src/main/res/layout/conversation_list_item_view.xml +++ b/app/src/main/res/layout/conversation_list_item_view.xml @@ -46,7 +46,7 @@ android:layout_width="@dimen/conversation_list_avatar_size" android:layout_height="@dimen/conversation_list_avatar_size" android:layout_marginTop="12dp" - android:layout_marginStart="20dp" + android:layout_marginStart="24dp" android:contentDescription="@string/conversation_list_item_view__contact_photo_image" android:foreground="@drawable/contact_photo_background" app:layout_constraintStart_toEndOf="@id/conversation_list_item_check_container" diff --git a/app/src/main/res/layout/signal_bottom_action_bar_item.xml b/app/src/main/res/layout/signal_bottom_action_bar_item.xml new file mode 100644 index 000000000..25bac6ae0 --- /dev/null +++ b/app/src/main/res/layout/signal_bottom_action_bar_item.xml @@ -0,0 +1,29 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/conversation_list_batch.xml b/app/src/main/res/menu/conversation_list_batch.xml deleted file mode 100644 index 91a7061e5..000000000 --- a/app/src/main/res/menu/conversation_list_batch.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/conversation_list_batch_archive.xml b/app/src/main/res/menu/conversation_list_batch_archive.xml deleted file mode 100644 index cd67648e6..000000000 --- a/app/src/main/res/menu/conversation_list_batch_archive.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/conversation_list_batch_pin.xml b/app/src/main/res/menu/conversation_list_batch_pin.xml deleted file mode 100644 index 9f3310d7f..000000000 --- a/app/src/main/res/menu/conversation_list_batch_pin.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/conversation_list_batch_unarchive.xml b/app/src/main/res/menu/conversation_list_batch_unarchive.xml deleted file mode 100644 index 9c5fe1255..000000000 --- a/app/src/main/res/menu/conversation_list_batch_unarchive.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night/dark_colors.xml b/app/src/main/res/values-night/dark_colors.xml index 1870d090f..6905dcbc3 100644 --- a/app/src/main/res/values-night/dark_colors.xml +++ b/app/src/main/res/values-night/dark_colors.xml @@ -28,6 +28,7 @@ @color/core_grey_15 @color/core_grey_25 + @color/signal_icon_tint_primary @color/core_white @color/core_grey_25 diff --git a/app/src/main/res/values/light_colors.xml b/app/src/main/res/values/light_colors.xml index 025f3b652..f7df4f78e 100644 --- a/app/src/main/res/values/light_colors.xml +++ b/app/src/main/res/values/light_colors.xml @@ -28,6 +28,7 @@ @color/core_grey_75 @color/core_grey_60 + @color/core_black @color/core_grey_75 @color/core_grey_45 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a5a040acd..e8828e2be 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -374,6 +374,11 @@ Archive Unarchive Delete + Select all + + %d selected + %d selected + Key exchange message @@ -1580,6 +1585,9 @@ Invite to Signal Signal Message + + More + We\'ll remind you again later. We\'ll remind you again tomorrow. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ed62d134c..a0839dbd6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -384,8 +384,10 @@ + +