diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java index 08f8a6d5f9..cdd864e77d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java @@ -197,6 +197,14 @@ public class ConversationMessage { return threadRecipient; } + public boolean isActiveCollapsibleHead() { + return collapsedSize > 1 && CollapsedState.isHead(messageRecord.getCollapsedState()); + } + + public boolean isActiveCollapsedHead() { + return collapsedSize > 1 && messageRecord.getCollapsedState() == CollapsedState.HEAD_COLLAPSED; + } + public static @NonNull FormattedDate getFormattedDate(@NonNull Context context, @NonNull MessageRecord messageRecord) { if (MessageRecordUtil.isScheduled(messageRecord)) { String time = DateUtils.getOnlyTimeString(context, ((MmsMessageRecord) messageRecord).getScheduledDate()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index a308df094d..884ab620a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -10,6 +10,7 @@ import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.method.LinkMovementMethod; import android.util.AttributeSet; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Button; @@ -36,6 +37,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.BindableConversationItem; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.conversation.colors.Colorizer; +import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection; import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart; import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog; import org.thoughtcrime.securesms.database.CollapsibleEvents; @@ -99,6 +101,7 @@ public final class ConversationUpdateItem extends FrameLayout private boolean isMessageRequestAccepted; private EventListener eventListener; private Button collapsedButton; + private float lastYDownRelativeToThis; private final UpdateObserver updateObserver = new UpdateObserver(); @@ -407,14 +410,35 @@ public final class ConversationUpdateItem extends FrameLayout } } + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == android.view.MotionEvent.ACTION_DOWN) { + lastYDownRelativeToThis = ev.getY(); + } + return super.onInterceptTouchEvent(ev); + } + @Override public @NonNull MultiselectPart getMultiselectPartForLatestTouch() { - return conversationMessage.getMultiselectCollection().asSingle().getSinglePart(); + MultiselectCollection parts = conversationMessage.getMultiselectCollection(); + if (parts.isSingle()) { + return parts.asSingle().getSinglePart(); + } else if (isTouchAboveCollapsedButton()) { + return parts.asDouble().getTopPart(); + } else { + return parts.asDouble().getBottomPart(); + } } @Override public int getTopBoundaryOfMultiselectPart(@NonNull MultiselectPart multiselectPart) { - return getTop(); + if (multiselectPart instanceof MultiselectPart.CollapsedHead) { + return getTop(); + } else if (multiselectPart instanceof MultiselectPart.Update && conversationMessage.isActiveCollapsibleHead()) { + return getCollapsedButtonBottom(); + } else { + return getTop(); + } } @Override @@ -427,6 +451,17 @@ public final class ConversationUpdateItem extends FrameLayout return false; } + private boolean isTouchAboveCollapsedButton() { + return conversationMessage.isActiveCollapsibleHead() && lastYDownRelativeToThis <= collapsedButton.getBottom(); + } + + private int getCollapsedButtonBottom() { + Projection projection = Projection.relativeToViewRoot(collapsedButton, null); + int bottom = (int) projection.getY() + projection.getHeight(); + projection.release(); + return bottom; + } + private void observeDisplayBody(@NonNull LifecycleOwner lifecycleOwner, @Nullable LiveData message) { if (message != null) { message.observe(lifecycleOwner, it -> { @@ -840,7 +875,7 @@ public final class ConversationUpdateItem extends FrameLayout if (eventListener != null) { if (CollapsedState.isCollapsed(collapsedState)) { eventListener.onExpandEvents(conversationMessage.getMessageRecord().getId()); - } else { + } else if (!anyCollapsibleChildrenSelected()) { eventListener.onCollapseEvents(conversationMessage.getMessageRecord().getId()); } } else { @@ -906,6 +941,16 @@ public final class ConversationUpdateItem extends FrameLayout }); } + private boolean anyCollapsibleChildrenSelected() { + long messageId = conversationMessage.getMessageRecord().getId(); + for (MultiselectPart part : batchSelected) { + if (part.getMessageRecord().getCollapsedHeadId() == messageId) { + return true; + } + } + return false; + } + @Override public void setOnClickListener(View.OnClickListener l) { super.setOnClickListener(new InternalClickListener(l)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java index ab4da02ad7..01b34eebe1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation; import androidx.annotation.NonNull; +import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection; import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -9,7 +10,6 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.MessageRecordUtil; import org.thoughtcrime.securesms.util.MessageConstraintsUtil; -import org.thoughtcrime.securesms.util.RemoteConfig; import java.util.Set; import java.util.stream.Collectors; @@ -270,9 +270,10 @@ public final class MenuState { private static boolean onlyContainsCompleteMessages(@NonNull Set multiselectParts) { return multiselectParts.stream() - .map(MultiselectPart::getConversationMessage) - .map(ConversationMessage::getMultiselectCollection) - .allMatch(collection -> multiselectParts.containsAll(collection.toSet())); + .allMatch(part -> { + MultiselectCollection collection = part.getConversationMessage().getMultiselectCollection(); + return part instanceof MultiselectPart.Update || multiselectParts.containsAll(collection.toSet()); + }); } public static boolean canReplyToMessage(@NonNull Recipient conversationRecipient, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/Multiselect.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/Multiselect.kt index 975aed107e..1af4ca29d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/Multiselect.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/Multiselect.kt @@ -17,8 +17,11 @@ object Multiselect { @JvmStatic fun getParts(conversationMessage: ConversationMessage): MultiselectCollection { val messageRecord = conversationMessage.messageRecord - - if (messageRecord.isUpdate) { + if (conversationMessage.isActiveCollapsedHead) { + return MultiselectCollection.Single(MultiselectPart.CollapsedHead(conversationMessage)) + } else if (conversationMessage.isActiveCollapsibleHead) { + return MultiselectCollection.Double(MultiselectPart.CollapsedHead(conversationMessage), MultiselectPart.Update(conversationMessage)) + } else if (messageRecord.isUpdate) { return MultiselectCollection.Single(MultiselectPart.Update(conversationMessage)) } 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 cc2d824b67..9c4f2fe597 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 @@ -273,6 +273,9 @@ class MultiselectItemDecoration( val parts: MultiselectCollection = child.conversationMessage.multiselectCollection parts.toSet().forEach { + if (it is MultiselectPart.CollapsedHead) { + return@forEach + } val topBoundary = child.getTopBoundaryOfMultiselectPart(it) val bottomBoundary = child.getBottomBoundaryOfMultiselectPart(it) if (drawCircleBehindSelector) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectPart.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectPart.kt index 07a16bea29..0ca773f56b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectPart.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectPart.kt @@ -35,4 +35,9 @@ sealed class MultiselectPart(open val conversationMessage: ConversationMessage) * Represents the entire message, for use when we've not yet enabled multiforward. */ data class Message(override val conversationMessage: ConversationMessage) : MultiselectPart(conversationMessage) + + /** + * Represents the collapsed update head button. While it is not selectable, selecting it triggers the selection of other [MultiselectPart] + */ + data class CollapsedHead(override val conversationMessage: ConversationMessage) : MultiselectPart(conversationMessage) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt index 3990f9200b..90b0010b88 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt @@ -84,6 +84,7 @@ class ConversationAdapterV2( } private val _selected = hashSetOf() + private var adapterPosition = RecyclerView.NO_POSITION override val selectedItems: Set get() = _selected.toSet() @@ -308,7 +309,23 @@ class ConversationAdapterV2( fun toggleSelection(multiselectPart: MultiselectPart) { if (multiselectPart.getMessageRecord().isInMemoryMessageRecord) { return } - if (multiselectPart in _selected) { + if (multiselectPart is MultiselectPart.CollapsedHead) { + val collapsedChildren: List = mutableListOf().apply { + add(getConversationMessage(adapterPosition)!!.multiselectCollection.asDouble().bottomPart) + addAll( + (1 until multiselectPart.conversationMessage.collapsedSize).mapNotNull { i -> + getConversationMessage(adapterPosition - i)?.multiselectCollection?.asSingle()?.singlePart + } + ) + } + + val isSelecting = collapsedChildren.any { it !in _selected } + if (isSelecting) { + _selected.addAll(collapsedChildren) + } else { + _selected.removeAll(collapsedChildren.toSet()) + } + } else if (multiselectPart in _selected) { _selected.remove(multiselectPart) } else { _selected.add(multiselectPart) @@ -451,10 +468,12 @@ class ConversationAdapterV2( init { itemView.setOnClickListener { + this@ConversationAdapterV2.adapterPosition = bindingAdapterPosition clickListener.onItemClick(bindable.getMultiselectPartForLatestTouch()) } itemView.setOnLongClickListener { + this@ConversationAdapterV2.adapterPosition = bindingAdapterPosition clickListener.onItemLongClick( it, bindable.getMultiselectPartForLatestTouch() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 6ecb600428..a92cf52244 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -3785,7 +3785,11 @@ class ConversationFragment : override fun onItemClick(item: MultiselectPart) { if (isActionModeStarted()) { - adapter.toggleSelection(item) + if (item.conversationMessage.isActiveCollapsedHead) { + viewModel.onExpandEvents(item.conversationMessage.messageRecord.id) + } else { + adapter.toggleSelection(item) + } binding.conversationItemRecycler.invalidateItemDecorations() if (adapter.selectedItems.isEmpty()) { @@ -3931,6 +3935,8 @@ class ConversationFragment : } ) } + } else if (item.conversationMessage.isActiveCollapsedHead) { + viewModel.onExpandEvents(item.conversationMessage.messageRecord.id) } else { clearFocusedItem() adapter.toggleSelection(item)