From 12b008c61c3f87320b8e99b5a9d31bd2dd56ffdb Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 28 Jun 2021 13:29:17 +1000 Subject: [PATCH 1/9] Send quotes & link previews --- .../conversation/v2/ConversationActivityV2.kt | 23 +++++++++++++++---- .../conversation/v2/input_bar/InputBar.kt | 8 +++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 5153d2309a..e53ab9727e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -40,8 +40,9 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.attachments.Attachment +import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview +import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.utilities.MediaTypes -import org.session.libsession.utilities.ServiceUtil import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.ListenableFuture import org.thoughtcrime.securesms.ApplicationContext @@ -61,6 +62,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.DraftDatabase.Drafts import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.giph.ui.GiphyActivity import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel @@ -670,6 +672,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun sendMessage() { + if (inputBar.linkPreview != null || inputBar.quote != null) { + sendAttachments(listOf(), getMessageBody(), inputBar.quote, inputBar.linkPreview) + } else { + sendTextOnlyMessage() + } + } + + private fun sendTextOnlyMessage() { // Create the message val message = VisibleMessage() message.sentTimestamp = System.currentTimeMillis() @@ -689,13 +699,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID) } - private fun sendAttachments(attachments: List, body: String?) { - // TODO: Quotes & link previews + private fun sendAttachments(attachments: List, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) { // Create the message val message = VisibleMessage() message.sentTimestamp = System.currentTimeMillis() message.text = body - val outgoingTextMessage = OutgoingMediaMessage.from(message, thread, attachments, null, null) + val quote = quotedMessage?.let { + val quotedAttachments = (it as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: listOf() + QuoteModel(it.dateSent, it.individualRecipient.address, it.body, false, quotedAttachments) + } + val outgoingTextMessage = OutgoingMediaMessage.from(message, thread, attachments, quote, linkPreview) // Clear the input bar inputBar.text = "" // Clear mentions @@ -709,7 +722,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // Put the message in the database message.id = DatabaseFactory.getMmsDatabase(this).insertMessageOutbox(outgoingTextMessage, threadID, false) { } // Send it - MessageSender.send(message, thread.address, attachments, null, null) + MessageSender.send(message, thread.address, attachments, quote, linkPreview) // Send a typing stopped message ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index d00dc10f1e..1cb12f418d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -29,6 +29,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li private var linkPreviewDraftView: LinkPreviewDraftView? = null var delegate: InputBarDelegate? = null var additionalContentHeight = 0 + var quote: MessageRecord? = null + var linkPreview: LinkPreview? = null var text: String get() { return inputBarEditText.text.toString() } @@ -100,6 +102,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li // a quote and a link preview at the same time. fun draftQuote(message: MessageRecord) { + quote = message + linkPreview = null linkPreviewDraftView = null inputBarAdditionalContentContainer.removeAllViews() val quoteView = QuoteView(context, QuoteView.Mode.Draft) @@ -121,6 +125,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li } override fun cancelQuoteDraft() { + quote = null inputBarAdditionalContentContainer.removeAllViews() val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) additionalContentHeight = 0 @@ -128,6 +133,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li } fun draftLinkPreview() { + quote = null val linkPreviewDraftHeight = toPx(88, resources) inputBarAdditionalContentContainer.removeAllViews() val linkPreviewDraftView = LinkPreviewDraftView(context) @@ -140,11 +146,13 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li } fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) { + this.linkPreview = linkPreview val linkPreviewDraftView = this.linkPreviewDraftView ?: return linkPreviewDraftView.update(glide, linkPreview) } override fun cancelLinkPreviewDraft() { + linkPreview = null inputBarAdditionalContentContainer.removeAllViews() val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) additionalContentHeight = 0 From 8cb4c267c5c8119cfc867eb0a6b3300578e09feb Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 28 Jun 2021 13:36:15 +1000 Subject: [PATCH 2/9] Show blocked dialog if needed --- .../securesms/conversation/v2/ConversationActivityV2.kt | 4 ++++ .../securesms/conversation/v2/dialogs/BlockedDialog.kt | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index e53ab9727e..3a5f36d555 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -672,6 +672,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun sendMessage() { + if (thread.isContactRecipient && thread.isBlocked) { + BlockedDialog(thread).show(supportFragmentManager, "Blocked Dialog") + return + } if (inputBar.linkPreview != null || inputBar.quote != null) { sendAttachments(listOf(), getMessageBody(), inputBar.quote, inputBar.linkPreview) } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt index d42e39d1fa..3748add489 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt @@ -13,6 +13,7 @@ import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.RecipientDatabase /** Shown upon sending a message to a user that's blocked. */ class BlockedDialog(private val recipient: Recipient) : BaseDialog() { @@ -36,6 +37,7 @@ class BlockedDialog(private val recipient: Recipient) : BaseDialog() { } private fun unblock() { - // TODO: Implement + DatabaseFactory.getRecipientDatabase(requireContext()).setBlocked(recipient, false) + dismiss() } } \ No newline at end of file From c17eb5e404bf41b24cd6293a65beb390bafee819 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 28 Jun 2021 14:51:49 +1000 Subject: [PATCH 3/9] WIP --- .../securesms/audio/AudioSlidePlayer.java | 15 ++++- .../conversation/v2/ConversationActivityV2.kt | 1 - .../conversation/v2/dialogs/BlockedDialog.kt | 1 - .../v2/messages/ControlMessageView.kt | 2 +- .../conversation/v2/messages/DocumentView.kt | 4 -- .../v2/messages/LinkPreviewView.kt | 4 -- .../v2/messages/VisibleMessageContentView.kt | 6 -- .../v2/messages/VisibleMessageView.kt | 6 -- .../v2/messages/VoiceMessageView.kt | 66 ++++++++++--------- .../securesms/mms/AudioSlide.java | 1 - 10 files changed, 48 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java index a7622306ba..6158fe7f32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java @@ -105,7 +105,7 @@ public class AudioSlidePlayer implements SensorEventListener { private void play(final double progress, boolean earpiece) throws IOException { if (this.mediaPlayer != null) return; - LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl(); + LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl(); this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl); this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment()); this.startTime = System.currentTimeMillis(); @@ -267,8 +267,17 @@ public class AudioSlidePlayer implements SensorEventListener { return slide; } + public Long getDuration() { + if (mediaPlayer == null) { return 0L; } + return mediaPlayer.getDuration(); + } - private Pair getProgress() { + public Double getProgress() { + if (mediaPlayer == null) { return 0.0; } + return (double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(); + } + + private Pair getProgressTuple() { if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) { return new Pair<>(0D, 0); } else { @@ -383,7 +392,7 @@ public class AudioSlidePlayer implements SensorEventListener { return; } - Pair progress = player.getProgress(); + Pair progress = player.getProgressTuple(); player.notifyOnProgress(progress.first, progress.second); sendEmptyMessageDelayed(0, 50); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 3a5f36d555..fc44d99324 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -72,7 +72,6 @@ import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivity import org.thoughtcrime.securesms.mms.* import org.thoughtcrime.securesms.notifications.MarkReadReceiver -import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.MediaUtil import java.util.* diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt index 3748add489..3013ab8901 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/BlockedDialog.kt @@ -13,7 +13,6 @@ import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.database.RecipientDatabase /** Shown upon sending a message to a user that's blocked. */ class BlockedDialog(private val recipient: Recipient) : BaseDialog() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt index ab6c11dcb5..b4f3810e16 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt @@ -37,7 +37,7 @@ class ControlMessageView : LinearLayout { } fun recycle() { - // TODO: Implement + } // endregion } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt index 4a9c189962..c9daca6c7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DocumentView.kt @@ -30,9 +30,5 @@ class DocumentView : LinearLayout { documentTitleTextView.setTextColor(textColor) documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) } - - fun recycle() { - // TODO: Implement - } // endregion } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt index 00a98d1d33..aae48a622e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/LinkPreviewView.kt @@ -64,9 +64,5 @@ class LinkPreviewView : LinearLayout { super.dispatchDraw(canvas) cornerMask.mask(canvas) } - - fun recycle() { - // TODO: Implement - } // endregion } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index e242d6e6b5..9427f9e38b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -1,12 +1,9 @@ package org.thoughtcrime.securesms.conversation.v2.messages import android.content.Context -import android.content.res.ColorStateList -import android.graphics.Color import android.graphics.drawable.Drawable import android.text.util.Linkify import android.util.AttributeSet -import android.util.Log import android.util.TypedValue import android.view.LayoutInflater import android.widget.LinearLayout @@ -19,8 +16,6 @@ import androidx.core.graphics.BlendModeCompat import androidx.core.text.toSpannable import kotlinx.android.synthetic.main.view_visible_message_content.view.* import network.loki.messenger.R -import org.session.libsession.messaging.utilities.UpdateMessageData -import org.session.libsession.messaging.utilities.UpdateMessageData.Companion.fromJSON import org.session.libsession.utilities.ThemeUtil import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.recipients.Recipient @@ -28,7 +23,6 @@ import org.thoughtcrime.securesms.components.emoji.EmojiTextView import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.loki.utilities.* -import org.thoughtcrime.securesms.loki.utilities.MentionUtilities.highlightMentions import org.thoughtcrime.securesms.mms.GlideRequests import kotlin.math.roundToInt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 3d665c5cbd..e3d7482b64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -4,19 +4,14 @@ import android.content.Context import android.content.res.Resources import android.graphics.Canvas import android.graphics.Rect -import android.graphics.Region import android.graphics.drawable.ColorDrawable import android.os.Build import android.os.Handler import android.os.Looper import android.util.AttributeSet -import android.util.Log import android.view.* import android.widget.LinearLayout -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat -import androidx.core.graphics.withClip import androidx.core.view.isVisible import kotlinx.android.synthetic.main.view_conversation.view.* import kotlinx.android.synthetic.main.view_visible_message.view.* @@ -27,7 +22,6 @@ import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.utilities.ViewUtil import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.MessageRecord -import org.thoughtcrime.securesms.loki.utilities.disableClipping import org.thoughtcrime.securesms.loki.utilities.getColorWithID import org.thoughtcrime.securesms.loki.utilities.toDp import org.thoughtcrime.securesms.loki.utilities.toPx diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt index 111f251805..9d2620ecd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt @@ -13,20 +13,22 @@ import android.widget.RelativeLayout import androidx.core.view.isVisible import kotlinx.android.synthetic.main.view_voice_message.view.* import network.loki.messenger.R +import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment +import org.thoughtcrime.securesms.audio.AudioSlidePlayer import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities +import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.mms.AudioSlide import java.util.concurrent.TimeUnit import kotlin.math.roundToInt +import kotlin.math.roundToLong -class VoiceMessageView : LinearLayout { - private val snHandler = Handler(Looper.getMainLooper()) +class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { private val cornerMask by lazy { CornerMask(this) } - private var runnable: Runnable? = null - private var mockIsPlaying = false - private var mockProgress = 0L - set(value) { field = value; handleProgressChanged() } - private var mockDuration = 12000L + private var isPlaying = false + private var progress = 0.0 + private var player: AudioSlidePlayer? = null // region Lifecycle constructor(context: Context) : super(context) { initialize() } @@ -36,14 +38,22 @@ class VoiceMessageView : LinearLayout { private fun initialize() { LayoutInflater.from(context).inflate(R.layout.view_voice_message, this) voiceMessageViewDurationTextView.text = String.format("%01d:%02d", - TimeUnit.MILLISECONDS.toMinutes(mockDuration), - TimeUnit.MILLISECONDS.toSeconds(mockDuration)) + TimeUnit.MILLISECONDS.toMinutes(0), + TimeUnit.MILLISECONDS.toSeconds(0)) } // endregion // region Updating fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { val audio = message.slideDeck.audioSlide!! + val player = AudioSlidePlayer.createFor(context, audio, this) + this.player = player + player.play(0.0) + val duration = player.duration + player.stop() + voiceMessageViewDurationTextView.text = String.format("%01d:%02d", + TimeUnit.MILLISECONDS.toMinutes(duration), + TimeUnit.MILLISECONDS.toSeconds(duration)) voiceMessageViewLoader.isVisible = audio.isPendingDownload val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) cornerMask.setTopLeftRadius(cornerRadii[0]) @@ -52,43 +62,37 @@ class VoiceMessageView : LinearLayout { cornerMask.setBottomLeftRadius(cornerRadii[3]) } - private fun handleProgressChanged() { + override fun onPlayerStart(player: AudioSlidePlayer) { } + + override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, duration: Long) { voiceMessageViewDurationTextView.text = String.format("%01d:%02d", - TimeUnit.MILLISECONDS.toMinutes(mockDuration - mockProgress), - TimeUnit.MILLISECONDS.toSeconds(mockDuration - mockProgress)) + TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()), + TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong())) val layoutParams = progressView.layoutParams as RelativeLayout.LayoutParams - val fraction = mockProgress.toFloat() / mockDuration.toFloat() - layoutParams.width = (width.toFloat() * fraction).roundToInt() + layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt() progressView.layoutParams = layoutParams + this.progress = progress } + override fun onPlayerStop(player: AudioSlidePlayer) { } + override fun dispatchDraw(canvas: Canvas) { super.dispatchDraw(canvas) cornerMask.mask(canvas) } - - fun recycle() { - // TODO: Implement - } // endregion // region Interaction fun togglePlayback() { - mockIsPlaying = !mockIsPlaying - val iconID = if (mockIsPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play + val player = this.player ?: return + isPlaying = !isPlaying + val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play voiceMessagePlaybackImageView.setImageResource(iconID) - if (mockIsPlaying) { - updateProgress() + if (isPlaying) { + player.play(player.progress) } else { - runnable?.let { snHandler.removeCallbacks(it) } + player.stop() } } - - private fun updateProgress() { - mockProgress += 20L - val runnable = Runnable { updateProgress() } - this.runnable = runnable - snHandler.postDelayed(runnable, 20L) - } // endregion -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java index 9a1885ab90..3d45e6a6e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -31,7 +31,6 @@ import org.session.libsession.utilities.MediaTypes; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.util.ResUtil; - public class AudioSlide extends Slide { public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) { From a53ce18404a1bcce5a4705b443c673eb0c937fc4 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 28 Jun 2021 15:06:51 +1000 Subject: [PATCH 4/9] Fix duration text view --- .../securesms/audio/AudioSlidePlayer.java | 2 +- .../v2/messages/VoiceMessageView.kt | 33 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java index 6158fe7f32..8877994fbf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java @@ -103,7 +103,7 @@ public class AudioSlidePlayer implements SensorEventListener { } private void play(final double progress, boolean earpiece) throws IOException { - if (this.mediaPlayer != null) return; + if (this.mediaPlayer != null) { stop(); } LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl(); this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt index 9d2620ecd0..d63c4a9701 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt @@ -28,7 +28,9 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { private val cornerMask by lazy { CornerMask(this) } private var isPlaying = false private var progress = 0.0 + private var duration = 0L private var player: AudioSlidePlayer? = null + private var isPreparing = false // region Lifecycle constructor(context: Context) : super(context) { initialize() } @@ -48,12 +50,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { val audio = message.slideDeck.audioSlide!! val player = AudioSlidePlayer.createFor(context, audio, this) this.player = player + isPreparing = true player.play(0.0) - val duration = player.duration - player.stop() - voiceMessageViewDurationTextView.text = String.format("%01d:%02d", - TimeUnit.MILLISECONDS.toMinutes(duration), - TimeUnit.MILLISECONDS.toSeconds(duration)) voiceMessageViewLoader.isVisible = audio.isPendingDownload val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) cornerMask.setTopLeftRadius(cornerRadii[0]) @@ -62,16 +60,33 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { cornerMask.setBottomLeftRadius(cornerRadii[3]) } - override fun onPlayerStart(player: AudioSlidePlayer) { } + override fun onPlayerStart(player: AudioSlidePlayer) { + if (!isPreparing) { return } + isPreparing = false + duration = player.duration + voiceMessageViewDurationTextView.text = String.format("%01d:%02d", + TimeUnit.MILLISECONDS.toMinutes(duration), + TimeUnit.MILLISECONDS.toSeconds(duration)) + player.stop() + } - override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, duration: Long) { + override fun onPlayerProgress(player: AudioSlidePlayer, progress: Double, unused: Long) { + if (progress == 1.0) { + togglePlayback() + handleProgressChanged(0.0) + } else { + handleProgressChanged(progress) + } + } + + private fun handleProgressChanged(progress: Double) { + this.progress = progress voiceMessageViewDurationTextView.text = String.format("%01d:%02d", TimeUnit.MILLISECONDS.toMinutes(duration - (progress * duration.toDouble()).roundToLong()), TimeUnit.MILLISECONDS.toSeconds(duration - (progress * duration.toDouble()).roundToLong())) val layoutParams = progressView.layoutParams as RelativeLayout.LayoutParams layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt() progressView.layoutParams = layoutParams - this.progress = progress } override fun onPlayerStop(player: AudioSlidePlayer) { } @@ -89,7 +104,7 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play voiceMessagePlaybackImageView.setImageResource(iconID) if (isPlaying) { - player.play(player.progress) + player.play(progress) } else { player.stop() } From 6d452e19ef4e9a0fe2d7b3eebd21ebe58b52cf28 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 28 Jun 2021 15:41:23 +1000 Subject: [PATCH 5/9] Detect double taps --- .../v2/messages/VisibleMessageContentView.kt | 3 +++ .../v2/messages/VisibleMessageView.kt | 23 ++++++++++++++++++- .../v2/messages/VoiceMessageView.kt | 14 +++++------ 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 9427f9e38b..548ebb31f4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -28,6 +28,7 @@ import kotlin.math.roundToInt class VisibleMessageContentView : LinearLayout { var onContentClick: (() -> Unit)? = null + var onContentDoubleTap: (() -> Unit)? = null // region Lifecycle constructor(context: Context) : super(context) { initialize() } @@ -52,6 +53,7 @@ class VisibleMessageContentView : LinearLayout { // Body mainContainer.removeAllViews() onContentClick = null + onContentDoubleTap = null if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) { val linkPreviewView = LinkPreviewView(context) linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster) @@ -77,6 +79,7 @@ class VisibleMessageContentView : LinearLayout { // We have to use onContentClick (rather than a click listener directly on the voice // message view) so as to not interfere with all the other gestures. onContentClick = { voiceMessageView.togglePlayback() } + onContentDoubleTap = { voiceMessageView.handleDoubleTap() } } else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) { val documentView = DocumentView(context) documentView.bind(message, VisibleMessageContentView.getTextColor(context, message)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index e3d7482b64..6a56fb83ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -9,6 +9,7 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.util.AttributeSet +import android.util.Log import android.view.* import android.widget.LinearLayout import androidx.core.content.ContextCompat @@ -40,8 +41,10 @@ class VisibleMessageView : LinearLayout { private var dx = 0.0f private var previousTranslationX = 0.0f private val gestureHandler = Handler(Looper.getMainLooper()) + private var pressCallback: Runnable? = null private var longPressCallback: Runnable? = null private var onDownTimestamp = 0L + private var onDoubleTap: (() -> Unit)? = null var snIsSelected = false set(value) { field = value; handleIsSelectedChanged()} var onPress: (() -> Unit)? = null @@ -52,6 +55,7 @@ class VisibleMessageView : LinearLayout { const val swipeToReplyThreshold = 80.0f // dp const val longPressMovementTreshold = 10.0f // dp const val longPressDurationThreshold = 250L // ms + const val maxDoubleTapInterval = 200L } // region Lifecycle @@ -137,6 +141,7 @@ class VisibleMessageView : LinearLayout { if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width } // Populate content view messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread) + onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() } } private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { @@ -266,7 +271,18 @@ class VisibleMessageView : LinearLayout { onSwipeToReply?.invoke() } else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) { longPressCallback?.let { gestureHandler.removeCallbacks(it) } - onPress?.invoke() + val pressCallback = this.pressCallback + if (pressCallback != null) { + // If we're here and pressCallback isn't null, it means that we tapped again within + // maxDoubleTapInterval ms and we should count this as a double tap + gestureHandler.removeCallbacks(pressCallback) + this.pressCallback = null + onDoubleTap?.invoke() + } else { + val newPressCallback = Runnable { onPress() } + this.pressCallback = newPressCallback + gestureHandler.postDelayed(newPressCallback, VisibleMessageView.maxDoubleTapInterval) + } } resetPosition() } @@ -291,6 +307,11 @@ class VisibleMessageView : LinearLayout { onLongPress?.invoke() } + private fun onPress() { + onPress?.invoke() + pressCallback = null + } + fun onContentClick() { messageContentView.onContentClick?.invoke() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt index d63c4a9701..0cd0e8c05e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt @@ -2,24 +2,18 @@ package org.thoughtcrime.securesms.conversation.v2.messages import android.content.Context import android.graphics.Canvas -import android.graphics.drawable.Drawable -import android.os.Handler -import android.os.Looper import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.ViewOutlineProvider +import android.util.Log +import android.view.* import android.widget.LinearLayout import android.widget.RelativeLayout import androidx.core.view.isVisible import kotlinx.android.synthetic.main.view_voice_message.view.* import network.loki.messenger.R -import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.thoughtcrime.securesms.audio.AudioSlidePlayer import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities -import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.MmsMessageRecord -import org.thoughtcrime.securesms.mms.AudioSlide import java.util.concurrent.TimeUnit import kotlin.math.roundToInt import kotlin.math.roundToLong @@ -109,5 +103,9 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { player.stop() } } + + fun handleDoubleTap() { + Log.d("Test", "handleDoubleTap()") + } // endregion } From f44e65536101d2deb00aeb15669ac928f0f58440 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 28 Jun 2021 15:44:11 +1000 Subject: [PATCH 6/9] Fix reply icon offset bug --- .../java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java | 2 -- .../securesms/conversation/v2/messages/VisibleMessageView.kt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java index 8877994fbf..56a06d0ccd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java @@ -184,8 +184,6 @@ public class AudioSlidePlayer implements SensorEventListener { public void onPlayerError(ExoPlaybackException error) { Log.w(TAG, "MediaPlayer Error: " + error); - Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show(); - synchronized (AudioSlidePlayer.this) { mediaPlayer = null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 6a56fb83ab..c8d91496d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -194,7 +194,7 @@ class VisibleMessageView : LinearLayout { val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing) val threshold = VisibleMessageView.swipeToReplyThreshold val iconSize = toPx(24, context.resources) - val bottomVOffset = paddingBottom + (messageContentView.height - iconSize) / 2 + val bottomVOffset = paddingBottom + messageStatusImageView.height + (messageContentView.height - iconSize) / 2 swipeToReplyIconRect.left = messageContentContainer.right + spacing swipeToReplyIconRect.top = height - bottomVOffset - iconSize swipeToReplyIconRect.right = messageContentContainer.right + iconSize + spacing From fa71c81ea95a98959805d518a7de06e6a135dead Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 28 Jun 2021 15:49:29 +1000 Subject: [PATCH 7/9] Speed up audio on double tap --- .../securesms/audio/AudioSlidePlayer.java | 11 +++++++++++ .../conversation/v2/messages/VoiceMessageView.kt | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java index 56a06d0ccd..61a92105aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/audio/AudioSlidePlayer.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.LoadControl; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.audio.AudioAttributes; @@ -284,6 +285,16 @@ public class AudioSlidePlayer implements SensorEventListener { } } + public float getPlaybackSpeed() { + if (mediaPlayer == null) { return 1.0f; } + return mediaPlayer.getPlaybackParameters().speed; + } + + public void setPlaybackSpeed(float speed) { + if (mediaPlayer == null) { return; } + mediaPlayer.setPlaybackParameters(new PlaybackParameters(speed)); + } + private void notifyOnStart() { Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt index 0cd0e8c05e..b5fea727c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt @@ -105,7 +105,8 @@ class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener { } fun handleDoubleTap() { - Log.d("Test", "handleDoubleTap()") + val player = this.player ?: return + player.playbackSpeed = if (player.playbackSpeed == 1.0f) 1.5f else 1.0f } // endregion } From fcdd3d91499e0ac507eea6059ffd18722e4edfd8 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 28 Jun 2021 16:03:41 +1000 Subject: [PATCH 8/9] Fix quote clearing bug --- .../thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index 1cb12f418d..b1ba60bc96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -152,6 +152,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li } override fun cancelLinkPreviewDraft() { + if (quote != null) { return } linkPreview = null inputBarAdditionalContentContainer.removeAllViews() val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) From 18e345733e243f08d0ac2b0cf8dcca978a3825b5 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 28 Jun 2021 16:28:00 +1000 Subject: [PATCH 9/9] Implement a few basic contextual actions --- .../conversation/v2/ConversationActivityV2.kt | 87 ++++++++++++++++++- .../menus/ConversationActionModeCallback.kt | 26 +++++- .../menu/menu_conversation_item_action.xml | 5 -- .../control/DataExtractionNotification.kt | 2 +- 4 files changed, 110 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index fc44d99324..bca90a4f46 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -1,7 +1,11 @@ package org.thoughtcrime.securesms.conversation.v2 +import android.Manifest import android.animation.FloatEvaluator import android.animation.ValueAnimator +import android.content.ClipData +import android.content.ClipboardManager +import android.content.DialogInterface import android.content.Intent import android.content.res.Resources import android.database.Cursor @@ -21,6 +25,7 @@ import androidx.loader.app.LoaderManager import androidx.loader.content.Loader import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.annimon.stream.Stream import kotlinx.android.synthetic.main.activity_conversation_v2.* import kotlinx.android.synthetic.main.activity_conversation_v2.view.* import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.* @@ -34,11 +39,14 @@ import nl.komponents.kovenant.ui.successUi import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.MentionsManager +import org.session.libsession.messaging.messages.control.DataExtractionNotification +import org.session.libsession.messaging.messages.control.DataExtractionNotification.Kind.MediaSaved import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.messaging.sending_receiving.MessageSender.send import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel @@ -55,6 +63,7 @@ import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCandidatesView import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback +import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager @@ -72,8 +81,10 @@ import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivity import org.thoughtcrime.securesms.mms.* import org.thoughtcrime.securesms.notifications.MarkReadReceiver +import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.MediaUtil +import org.thoughtcrime.securesms.util.SaveAttachmentTask import java.util.* import java.util.concurrent.ExecutionException import kotlin.math.* @@ -83,7 +94,7 @@ import kotlin.math.* // price we pay is a bit of back and forth between the input bar and the conversation activity. class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate, - InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener { + InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ConversationActionModeCallbackDelegate { private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private var linkPreviewViewModel: LinkPreviewViewModel? = null private var threadID: Long = -1 @@ -552,6 +563,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe if (actionMode != null) { adapter.toggleSelection(message, position) val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) + actionModeCallback.delegate = this actionModeCallback.updateActionModeMenu(actionMode.menu) if (adapter.selectedItems.isEmpty()) { actionMode.finish() @@ -575,6 +587,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private fun handleLongPress(message: MessageRecord, position: Int) { val actionMode = this.actionMode val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) + actionModeCallback.delegate = this if (actionMode == null) { // Nothing should be selected if this is the case adapter.toggleSelection(message, position) this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { @@ -655,7 +668,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } private fun unblock() { - // TODO: Implement + if (!thread.isContactRecipient) { return } + DatabaseFactory.getRecipientDatabase(this).setBlocked(thread, false) } private fun handleMentionSelected(mention: Mention) { @@ -846,6 +860,75 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe audioRecorder.stopRecording() stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) } + + override fun deleteMessage(messages: Set) { + // TODO: Implement + } + + override fun banUser(messages: Set) { + // TODO: Implement + } + + override fun copyMessage(messages: Set) { + // TODO: Implement + } + + override fun copySessionID(messages: Set) { + val sessionID = messages.first().individualRecipient.address.toString() + val clip = ClipData.newPlainText("Session ID", sessionID) + val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + manager.setPrimaryClip(clip) + Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + actionMode?.finish() + actionMode = null + } + + override fun resendMessage(messages: Set) { + // TODO: Implement + } + + override fun saveAttachment(messages: Set) { + val message = messages.first() as MmsMessageRecord + SaveAttachmentTask.showWarningDialog(this, { dialog: DialogInterface?, which: Int -> + Permissions.with(this) + .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) + .maxSdkVersion(Build.VERSION_CODES.P) + .withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied)) + .onAnyDenied { Toast.makeText(this@ConversationActivityV2, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show() } + .onAllGranted { + val attachments: List = Stream.of(message.slideDeck.slides) + .filter { s: Slide -> s.uri != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()) } + .map { s: Slide -> SaveAttachmentTask.Attachment(s.uri!!, s.contentType, message.dateReceived, s.fileName.orNull()) } + .toList() + if (attachments.isNotEmpty()) { + val saveTask = SaveAttachmentTask(this) + saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, *attachments.toTypedArray()) + if (!message.isOutgoing) { + sendMediaSavedNotification() + } + return@onAllGranted + } + Toast.makeText(this, + resources.getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1), + Toast.LENGTH_LONG).show() + } + .execute() + }) + } + + override fun reply(messages: Set) { + inputBar.draftQuote(messages.first()) + actionMode?.finish() + actionMode = null + } + + private fun sendMediaSavedNotification() { + if (thread.isGroupRecipient) { return } + val timestamp = System.currentTimeMillis() + val kind = DataExtractionNotification.Kind.MediaSaved(timestamp) + val message = DataExtractionNotification(kind) + MessageSender.send(message, thread.address) + } // endregion // region General diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt index 00d62efcbb..750dc3495f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.conversation.v2.menus import android.content.Context +import android.util.Log import android.view.ActionMode import android.view.Menu import android.view.MenuItem @@ -10,9 +11,11 @@ import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord +import org.thoughtcrime.securesms.database.model.MessageRecord class ConversationActionModeCallback(private val adapter: ConversationAdapter, private val threadID: Long, private val context: Context) : ActionMode.Callback { + var delegate: ConversationActionModeCallbackDelegate? = null override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { val inflater = mode.menuInflater @@ -44,8 +47,6 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p if (selectedUsers.size > 1) { return false } return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server) } - // Message info - menu.findItem(R.id.menu_context_details).isVisible = (selectedItems.size == 1) // Delete message menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems() // Ban user @@ -70,6 +71,16 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p } override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + val selectedItems = adapter.selectedItems + when (item.itemId) { + R.id.menu_context_delete_message -> delegate?.deleteMessage(selectedItems) + R.id.menu_context_ban_user -> delegate?.banUser(selectedItems) + R.id.menu_context_copy -> delegate?.copyMessage(selectedItems) + R.id.menu_context_copy_public_key -> delegate?.copySessionID(selectedItems) + R.id.menu_context_resend -> delegate?.resendMessage(selectedItems) + R.id.menu_context_save_attachment -> delegate?.saveAttachment(selectedItems) + R.id.menu_context_reply -> delegate?.reply(selectedItems) + } return true } @@ -77,4 +88,15 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p adapter.selectedItems.clear() adapter.notifyDataSetChanged() } +} + +interface ConversationActionModeCallbackDelegate { + + fun deleteMessage(messages: Set) + fun banUser(messages: Set) + fun copyMessage(messages: Set) + fun copySessionID(messages: Set) + fun resendMessage(messages: Set) + fun saveAttachment(messages: Set) + fun reply(messages: Set) } \ No newline at end of file diff --git a/app/src/main/res/menu/menu_conversation_item_action.xml b/app/src/main/res/menu/menu_conversation_item_action.xml index b2006d81e4..ffa6fc7ed3 100644 --- a/app/src/main/res/menu/menu_conversation_item_action.xml +++ b/app/src/main/res/menu/menu_conversation_item_action.xml @@ -32,11 +32,6 @@ android:id="@+id/menu_context_resend" app:showAsAction="never" /> - -