diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemSwipeCallback.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemSwipeCallback.java index 2126219b31..842a5426f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemSwipeCallback.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItemSwipeCallback.java @@ -29,8 +29,8 @@ public class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallbac private final ConversationItemTouchListener itemTouchListener; private final OnSwipeListener onSwipeListener; - ConversationItemSwipeCallback(@NonNull SwipeAvailabilityProvider swipeAvailabilityProvider, - @NonNull OnSwipeListener onSwipeListener) + public ConversationItemSwipeCallback(@NonNull SwipeAvailabilityProvider swipeAvailabilityProvider, + @NonNull OnSwipeListener onSwipeListener) { super(0, ItemTouchHelper.END); this.itemTouchListener = new ConversationItemTouchListener(this::updateLatestDownCoordinate); @@ -40,7 +40,7 @@ public class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallbac this.canTriggerSwipe = true; } - void attachToRecyclerView(@NonNull RecyclerView recyclerView) { + public void attachToRecyclerView(@NonNull RecyclerView recyclerView) { recyclerView.addOnItemTouchListener(itemTouchListener); new ItemTouchHelper(this).attachToRecyclerView(recyclerView); } @@ -193,11 +193,11 @@ public class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallbac if (vibrator != null) vibrator.vibrate(SWIPE_SUCCESS_VIBE_TIME_MS); } - interface SwipeAvailabilityProvider { + public interface SwipeAvailabilityProvider { boolean isSwipeAvailable(ConversationMessage conversationMessage); } - interface OnSwipeListener { + public interface OnSwipeListener { void onSwipe(ConversationMessage conversationMessage); } } 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 0cad9e1cbe..f28b839189 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java @@ -197,11 +197,11 @@ public final class MenuState { .allMatch(collection -> multiselectParts.containsAll(collection.toSet())); } - static boolean canReplyToMessage(@NonNull Recipient conversationRecipient, - boolean actionMessage, - @NonNull MessageRecord messageRecord, - boolean isDisplayingMessageRequest, - boolean isNonAdminInAnnouncementGroup) + public static boolean canReplyToMessage(@NonNull Recipient conversationRecipient, + boolean actionMessage, + @NonNull MessageRecord messageRecord, + boolean isDisplayingMessageRequest, + boolean isNonAdminInAnnouncementGroup) { return !actionMessage && !isNonAdminInAnnouncementGroup && @@ -215,7 +215,7 @@ public final class MenuState { !conversationRecipient.isReleaseNotes(); } - static boolean isActionMessage(@NonNull MessageRecord messageRecord) { + public static boolean isActionMessage(@NonNull MessageRecord messageRecord) { return messageRecord.isInMemoryMessageRecord() || messageRecord.isUpdate(); } 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 db77de4d56..01c09cbd02 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 @@ -116,6 +116,7 @@ import org.thoughtcrime.securesms.conversation.ConversationIntents import org.thoughtcrime.securesms.conversation.ConversationIntents.ConversationScreenType import org.thoughtcrime.securesms.conversation.ConversationItem import org.thoughtcrime.securesms.conversation.ConversationItemSelection +import org.thoughtcrime.securesms.conversation.ConversationItemSwipeCallback import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.ConversationOptionsMenu import org.thoughtcrime.securesms.conversation.ConversationReactionDelegate @@ -221,6 +222,7 @@ import org.thoughtcrime.securesms.util.WindowUtil import org.thoughtcrime.securesms.util.concurrent.ListenableFuture import org.thoughtcrime.securesms.util.doAfterNextLayout import org.thoughtcrime.securesms.util.fragments.requireListener +import org.thoughtcrime.securesms.util.getRecordQuoteType import org.thoughtcrime.securesms.util.hasAudio import org.thoughtcrime.securesms.util.hasGiftBadge import org.thoughtcrime.securesms.util.viewModel @@ -546,6 +548,11 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment) .getRequestReviewState() .subscribeBy { presentRequestReviewState(it) } .addTo(disposables) + + ConversationItemSwipeCallback( + SwipeAvailabilityProvider(), + this::handleReplyToMessage + ).attachToRecyclerView(binding.conversationItemRecycler) } private fun presentInputReadyState(inputReadyState: InputReadyState) { @@ -893,6 +900,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment) disposables += send .doOnSubscribe { composeText.setText("") + inputPanel.clearQuote() } .subscribeBy( onError = { t -> @@ -1139,7 +1147,30 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment) //region Message action handling private fun handleReplyToMessage(conversationMessage: ConversationMessage) { - // TODO [cfv2] -- Not implemented yet. + /* + TODO [cfv2] + if (isSearchRequested) { + searchViewItem.collapseActionView(); + } + */ + + if (inputPanel.inEditMessageMode()) { + inputPanel.exitEditMessageMode() + } + + val (slideDeck, body) = viewModel.getSlideDeckAndBodyForReply(requireContext(), conversationMessage) + val author = conversationMessage.messageRecord.fromRecipient + + inputPanel.setQuote( + GlideApp.with(this), + conversationMessage.messageRecord.dateSent, + author, + body, + slideDeck, + conversationMessage.messageRecord.getRecordQuoteType() + ) + + inputPanel.clickOnComposeInput() } private fun handleEditMessage(conversationMessage: ConversationMessage) { @@ -1207,6 +1238,20 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment) ).subscribe() } + private inner class SwipeAvailabilityProvider : ConversationItemSwipeCallback.SwipeAvailabilityProvider { + override fun isSwipeAvailable(conversationMessage: ConversationMessage): Boolean { + val recipient = viewModel.recipientSnapshot ?: return false + + return actionMode == null && MenuState.canReplyToMessage( + recipient, + MenuState.isActionMessage(conversationMessage.messageRecord), + conversationMessage.messageRecord, + viewModel.hasMessageRequestState, + conversationGroupViewModel.isNonAdminInAnnouncementGroup() + ) + } + } + //endregion //region Scroll Handling diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt index 497e89f629..4a489ccd6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt @@ -32,6 +32,8 @@ import org.signal.paging.PagedData import org.signal.paging.PagingConfig import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.ShortcutLauncherActivity +import org.thoughtcrime.securesms.attachments.TombstoneAttachment +import org.thoughtcrime.securesms.components.emoji.EmojiStrings import org.thoughtcrime.securesms.components.reminder.BubbleOptOutReminder import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationSuggestionsReminder @@ -39,6 +41,8 @@ import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsRe import org.thoughtcrime.securesms.components.reminder.Reminder import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder +import org.thoughtcrime.securesms.contactshare.Contact +import org.thoughtcrime.securesms.contactshare.ContactUtil import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper import org.thoughtcrime.securesms.conversation.colors.NameColor @@ -80,9 +84,13 @@ import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.DrawableUtil +import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.SignalLocalMetrics import org.thoughtcrime.securesms.util.Util +import org.thoughtcrime.securesms.util.hasLinkPreview +import org.thoughtcrime.securesms.util.hasSharedContact import org.thoughtcrime.securesms.util.hasTextSlide +import org.thoughtcrime.securesms.util.isViewOnceMessage import org.thoughtcrime.securesms.util.requireTextSlide import java.io.IOException import java.util.Optional @@ -432,6 +440,46 @@ class ConversationRepository( .subscribeOn(Schedulers.computation()) } + fun getSlideDeckAndBodyForReply(context: Context, conversationMessage: ConversationMessage): Pair { + val messageRecord = conversationMessage.messageRecord + + return if (messageRecord.isMms && messageRecord.hasSharedContact()) { + val contact: Contact = (messageRecord as MmsMessageRecord).sharedContacts.first() + val displayName: String = ContactUtil.getDisplayName(contact) + val body: String = context.getString(R.string.ConversationActivity_quoted_contact_message, EmojiStrings.BUST_IN_SILHOUETTE, displayName) + val slideDeck = SlideDeck() + + if (contact.avatarAttachment != null) { + slideDeck.addSlide(MediaUtil.getSlideForAttachment(context, contact.avatarAttachment)) + } + + slideDeck to body + } else if (messageRecord.isMms && messageRecord.hasLinkPreview()) { + val linkPreview = (messageRecord as MmsMessageRecord).linkPreviews.first() + val slideDeck = SlideDeck() + + linkPreview.thumbnail.ifPresent { + slideDeck.addSlide(MediaUtil.getSlideForAttachment(context, it)) + } + + slideDeck to conversationMessage.getDisplayBody(context) + } else { + var slideDeck = if (messageRecord.isMms) { + (messageRecord as MmsMessageRecord).slideDeck + } else { + SlideDeck() + } + + if (messageRecord.isViewOnceMessage()) { + val attachment = TombstoneAttachment(MediaUtil.VIEW_ONCE, true) + slideDeck = SlideDeck() + slideDeck.addSlide(MediaUtil.getSlideForAttachment(context, attachment)) + } + + slideDeck to conversationMessage.getDisplayBody(context) + } + } + /** * Glide target for a contact photo which expects an error drawable, and publishes * the result to the given emitter. diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 93accefbf7..373e8111c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -28,6 +28,7 @@ import org.signal.core.util.concurrent.subscribeWithSubject import org.signal.core.util.orNull import org.signal.paging.ProxyPagingController import org.thoughtcrime.securesms.components.reminder.Reminder +import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper import org.thoughtcrime.securesms.conversation.colors.NameColor import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart @@ -290,4 +291,8 @@ class ConversationViewModel( .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) } + + fun getSlideDeckAndBodyForReply(context: Context, conversationMessage: ConversationMessage): Pair { + return repository.getSlideDeckAndBodyForReply(context, conversationMessage) + } }