Merge remote-tracking branch 'upstream/ui' into ui

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
This commit is contained in:
jubb 2021-06-29 10:05:34 +10:00
commit 6775e0afd7
14 changed files with 250 additions and 76 deletions

View File

@ -23,6 +23,7 @@ import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioAttributes;
@ -103,9 +104,9 @@ public class AudioSlidePlayer implements SensorEventListener {
} }
private void play(final double progress, boolean earpiece) throws IOException { 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(); 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.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl);
this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment()); this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment());
this.startTime = System.currentTimeMillis(); this.startTime = System.currentTimeMillis();
@ -184,8 +185,6 @@ public class AudioSlidePlayer implements SensorEventListener {
public void onPlayerError(ExoPlaybackException error) { public void onPlayerError(ExoPlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + error); Log.w(TAG, "MediaPlayer Error: " + error);
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
synchronized (AudioSlidePlayer.this) { synchronized (AudioSlidePlayer.this) {
mediaPlayer = null; mediaPlayer = null;
@ -267,8 +266,17 @@ public class AudioSlidePlayer implements SensorEventListener {
return slide; return slide;
} }
public Long getDuration() {
if (mediaPlayer == null) { return 0L; }
return mediaPlayer.getDuration();
}
private Pair<Double, Integer> getProgress() { public Double getProgress() {
if (mediaPlayer == null) { return 0.0; }
return (double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration();
}
private Pair<Double, Integer> getProgressTuple() {
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) { if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
return new Pair<>(0D, 0); return new Pair<>(0D, 0);
} else { } else {
@ -277,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() { private void notifyOnStart() {
Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this)); Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this));
} }
@ -383,7 +401,7 @@ public class AudioSlidePlayer implements SensorEventListener {
return; return;
} }
Pair<Double, Integer> progress = player.getProgress(); Pair<Double, Integer> progress = player.getProgressTuple();
player.notifyOnProgress(progress.first, progress.second); player.notifyOnProgress(progress.first, progress.second);
sendEmptyMessageDelayed(0, 50); sendEmptyMessageDelayed(0, 50);
} }

View File

@ -1,8 +1,12 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import android.Manifest
import android.animation.FloatEvaluator import android.animation.FloatEvaluator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.content.ClipData
import android.content.ClipboardManager
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.database.Cursor import android.database.Cursor
@ -23,6 +27,7 @@ import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.*
import kotlinx.android.synthetic.main.activity_conversation_v2.view.* import kotlinx.android.synthetic.main.activity_conversation_v2.view.*
import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.* import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.*
@ -36,12 +41,17 @@ import nl.komponents.kovenant.ui.successUi
import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.Mention
import org.session.libsession.messaging.mentions.MentionsManager 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.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.sending_receiving.MessageSender 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.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.MediaTypes
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.ListenableFuture import org.session.libsignal.utilities.ListenableFuture
@ -55,6 +65,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.InputBarRecordingViewDelegate
import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCandidatesView 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.ConversationActionModeCallback
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
@ -62,6 +73,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.DraftDatabase import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts import org.thoughtcrime.securesms.database.DraftDatabase.Drafts
import org.thoughtcrime.securesms.database.model.MessageRecord 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.giph.ui.GiphyActivity
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
@ -73,8 +85,10 @@ import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity import org.thoughtcrime.securesms.mediasend.MediaSendActivity
import org.thoughtcrime.securesms.mms.* import org.thoughtcrime.securesms.mms.*
import org.thoughtcrime.securesms.notifications.MarkReadReceiver import org.thoughtcrime.securesms.notifications.MarkReadReceiver
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import kotlin.math.* import kotlin.math.*
@ -84,7 +98,8 @@ import kotlin.math.*
// price we pay is a bit of back and forth between the input bar and the conversation activity. // price we pay is a bit of back and forth between the input bar and the conversation activity.
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate, class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher { InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
ConversationActionModeCallbackDelegate {
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private var linkPreviewViewModel: LinkPreviewViewModel? = null private var linkPreviewViewModel: LinkPreviewViewModel? = null
private var threadID: Long = -1 private var threadID: Long = -1
@ -575,6 +590,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (actionMode != null) { if (actionMode != null) {
adapter.toggleSelection(message, position) adapter.toggleSelection(message, position)
val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this)
actionModeCallback.delegate = this
actionModeCallback.updateActionModeMenu(actionMode.menu) actionModeCallback.updateActionModeMenu(actionMode.menu)
if (adapter.selectedItems.isEmpty()) { if (adapter.selectedItems.isEmpty()) {
actionMode.finish() actionMode.finish()
@ -598,6 +614,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun handleLongPress(message: MessageRecord, position: Int) { private fun handleLongPress(message: MessageRecord, position: Int) {
val actionMode = this.actionMode val actionMode = this.actionMode
val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this) val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this)
actionModeCallback.delegate = this
if (actionMode == null) { // Nothing should be selected if this is the case if (actionMode == null) { // Nothing should be selected if this is the case
adapter.toggleSelection(message, position) adapter.toggleSelection(message, position)
this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
@ -678,7 +695,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
private fun unblock() { private fun unblock() {
// TODO: Implement if (!thread.isContactRecipient) { return }
DatabaseFactory.getRecipientDatabase(this).setBlocked(thread, false)
} }
private fun handleMentionSelected(mention: Mention) { private fun handleMentionSelected(mention: Mention) {
@ -694,6 +712,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun sendMessage() { 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 {
sendTextOnlyMessage()
}
}
private fun sendTextOnlyMessage() {
// Create the message // Create the message
val message = VisibleMessage() val message = VisibleMessage()
message.sentTimestamp = System.currentTimeMillis() message.sentTimestamp = System.currentTimeMillis()
@ -713,13 +743,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID) ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID)
} }
private fun sendAttachments(attachments: List<Attachment>, body: String?) { private fun sendAttachments(attachments: List<Attachment>, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) {
// TODO: Quotes & link previews
// Create the message // Create the message
val message = VisibleMessage() val message = VisibleMessage()
message.sentTimestamp = System.currentTimeMillis() message.sentTimestamp = System.currentTimeMillis()
message.text = body 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 // Clear the input bar
inputBar.text = "" inputBar.text = ""
// Clear mentions // Clear mentions
@ -733,7 +766,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// Put the message in the database // Put the message in the database
message.id = DatabaseFactory.getMmsDatabase(this).insertMessageOutbox(outgoingTextMessage, threadID, false) { } message.id = DatabaseFactory.getMmsDatabase(this).insertMessageOutbox(outgoingTextMessage, threadID, false) { }
// Send it // Send it
MessageSender.send(message, thread.address, attachments, null, null) MessageSender.send(message, thread.address, attachments, quote, linkPreview)
// Send a typing stopped message // Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID) ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID)
} }
@ -854,6 +887,75 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
audioRecorder.stopRecording() audioRecorder.stopRecording()
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)
} }
override fun deleteMessage(messages: Set<MessageRecord>) {
// TODO: Implement
}
override fun banUser(messages: Set<MessageRecord>) {
// TODO: Implement
}
override fun copyMessage(messages: Set<MessageRecord>) {
// TODO: Implement
}
override fun copySessionID(messages: Set<MessageRecord>) {
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<MessageRecord>) {
// TODO: Implement
}
override fun saveAttachment(messages: Set<MessageRecord>) {
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<SaveAttachmentTask.Attachment?> = Stream.of<Slide>(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<MessageRecord>) {
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 // endregion
// region General // region General

View File

@ -36,6 +36,7 @@ class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
} }
private fun unblock() { private fun unblock() {
// TODO: Implement DatabaseFactory.getRecipientDatabase(requireContext()).setBlocked(recipient, false)
dismiss()
} }
} }

View File

@ -29,6 +29,8 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
private var linkPreviewDraftView: LinkPreviewDraftView? = null private var linkPreviewDraftView: LinkPreviewDraftView? = null
var delegate: InputBarDelegate? = null var delegate: InputBarDelegate? = null
var additionalContentHeight = 0 var additionalContentHeight = 0
var quote: MessageRecord? = null
var linkPreview: LinkPreview? = null
var text: String var text: String
get() { return inputBarEditText.text.toString() } 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. // a quote and a link preview at the same time.
fun draftQuote(message: MessageRecord) { fun draftQuote(message: MessageRecord) {
quote = message
linkPreview = null
linkPreviewDraftView = null linkPreviewDraftView = null
inputBarAdditionalContentContainer.removeAllViews() inputBarAdditionalContentContainer.removeAllViews()
val quoteView = QuoteView(context, QuoteView.Mode.Draft) val quoteView = QuoteView(context, QuoteView.Mode.Draft)
@ -121,6 +125,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
} }
override fun cancelQuoteDraft() { override fun cancelQuoteDraft() {
quote = null
inputBarAdditionalContentContainer.removeAllViews() inputBarAdditionalContentContainer.removeAllViews()
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
additionalContentHeight = 0 additionalContentHeight = 0
@ -128,6 +133,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
} }
fun draftLinkPreview() { fun draftLinkPreview() {
quote = null
val linkPreviewDraftHeight = toPx(88, resources) val linkPreviewDraftHeight = toPx(88, resources)
inputBarAdditionalContentContainer.removeAllViews() inputBarAdditionalContentContainer.removeAllViews()
val linkPreviewDraftView = LinkPreviewDraftView(context) val linkPreviewDraftView = LinkPreviewDraftView(context)
@ -140,11 +146,14 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate, Li
} }
fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) { fun updateLinkPreviewDraft(glide: GlideRequests, linkPreview: LinkPreview) {
this.linkPreview = linkPreview
val linkPreviewDraftView = this.linkPreviewDraftView ?: return val linkPreviewDraftView = this.linkPreviewDraftView ?: return
linkPreviewDraftView.update(glide, linkPreview) linkPreviewDraftView.update(glide, linkPreview)
} }
override fun cancelLinkPreviewDraft() { override fun cancelLinkPreviewDraft() {
if (quote != null) { return }
linkPreview = null
inputBarAdditionalContentContainer.removeAllViews() inputBarAdditionalContentContainer.removeAllViews()
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
additionalContentHeight = 0 additionalContentHeight = 0

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.v2.menus package org.thoughtcrime.securesms.conversation.v2.menus
import android.content.Context import android.content.Context
import android.util.Log
import android.view.ActionMode import android.view.ActionMode
import android.view.Menu import android.view.Menu
import android.view.MenuItem 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.conversation.v2.ConversationAdapter
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
class ConversationActionModeCallback(private val adapter: ConversationAdapter, private val threadID: Long, class ConversationActionModeCallback(private val adapter: ConversationAdapter, private val threadID: Long,
private val context: Context) : ActionMode.Callback { private val context: Context) : ActionMode.Callback {
var delegate: ConversationActionModeCallbackDelegate? = null
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val inflater = mode.menuInflater val inflater = mode.menuInflater
@ -44,8 +47,6 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (selectedUsers.size > 1) { return false } if (selectedUsers.size > 1) { return false }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server) return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
} }
// Message info
menu.findItem(R.id.menu_context_details).isVisible = (selectedItems.size == 1)
// Delete message // Delete message
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems() menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()
// Ban user // Ban user
@ -70,6 +71,16 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
} }
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { 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 return true
} }
@ -77,4 +88,15 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
adapter.selectedItems.clear() adapter.selectedItems.clear()
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
} }
}
interface ConversationActionModeCallbackDelegate {
fun deleteMessage(messages: Set<MessageRecord>)
fun banUser(messages: Set<MessageRecord>)
fun copyMessage(messages: Set<MessageRecord>)
fun copySessionID(messages: Set<MessageRecord>)
fun resendMessage(messages: Set<MessageRecord>)
fun saveAttachment(messages: Set<MessageRecord>)
fun reply(messages: Set<MessageRecord>)
} }

View File

@ -37,7 +37,7 @@ class ControlMessageView : LinearLayout {
} }
fun recycle() { fun recycle() {
// TODO: Implement
} }
// endregion // endregion
} }

View File

@ -30,9 +30,5 @@ class DocumentView : LinearLayout {
documentTitleTextView.setTextColor(textColor) documentTitleTextView.setTextColor(textColor)
documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
} }
fun recycle() {
// TODO: Implement
}
// endregion // endregion
} }

View File

@ -64,9 +64,5 @@ class LinkPreviewView : LinearLayout {
super.dispatchDraw(canvas) super.dispatchDraw(canvas)
cornerMask.mask(canvas) cornerMask.mask(canvas)
} }
fun recycle() {
// TODO: Implement
}
// endregion // endregion
} }

View File

@ -7,7 +7,6 @@ import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.util.Linkify import android.text.util.Linkify
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
@ -28,12 +27,12 @@ import org.thoughtcrime.securesms.components.emoji.EmojiTextView
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.loki.utilities.* import org.thoughtcrime.securesms.loki.utilities.*
import org.thoughtcrime.securesms.loki.utilities.MentionUtilities.highlightMentions
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import kotlin.math.roundToInt import kotlin.math.roundToInt
class VisibleMessageContentView : LinearLayout { class VisibleMessageContentView : LinearLayout {
var onContentClick: ((rawRect: Rect) -> Unit)? = null var onContentClick: ((rawRect: Rect) -> Unit)? = null
var onContentDoubleTap: (() -> Unit)? = null
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
@ -58,6 +57,7 @@ class VisibleMessageContentView : LinearLayout {
// Body // Body
mainContainer.removeAllViews() mainContainer.removeAllViews()
onContentClick = null onContentClick = null
onContentDoubleTap = null
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) { if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
val linkPreviewView = LinkPreviewView(context) val linkPreviewView = LinkPreviewView(context)
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster) linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
@ -83,6 +83,7 @@ class VisibleMessageContentView : LinearLayout {
// We have to use onContentClick (rather than a click listener directly on the voice // 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. // message view) so as to not interfere with all the other gestures.
onContentClick = { voiceMessageView.togglePlayback() } onContentClick = { voiceMessageView.togglePlayback() }
onContentDoubleTap = { voiceMessageView.handleDoubleTap() }
} else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) { } else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) {
val documentView = DocumentView(context) val documentView = DocumentView(context)
documentView.bind(message, VisibleMessageContentView.getTextColor(context, message)) documentView.bind(message, VisibleMessageContentView.getTextColor(context, message))

View File

@ -4,21 +4,15 @@ import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.Region
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.* import android.view.*
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.withClip
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_conversation.view.*
import kotlinx.android.synthetic.main.view_visible_message.view.* import kotlinx.android.synthetic.main.view_visible_message.view.*
import kotlinx.android.synthetic.main.view_visible_message.view.profilePictureView import kotlinx.android.synthetic.main.view_visible_message.view.profilePictureView
import network.loki.messenger.R import network.loki.messenger.R
@ -27,7 +21,6 @@ import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.ViewUtil
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MessageRecord 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.getColorWithID
import org.thoughtcrime.securesms.loki.utilities.toDp import org.thoughtcrime.securesms.loki.utilities.toDp
import org.thoughtcrime.securesms.loki.utilities.toPx import org.thoughtcrime.securesms.loki.utilities.toPx
@ -46,8 +39,10 @@ class VisibleMessageView : LinearLayout {
private var dx = 0.0f private var dx = 0.0f
private var previousTranslationX = 0.0f private var previousTranslationX = 0.0f
private val gestureHandler = Handler(Looper.getMainLooper()) private val gestureHandler = Handler(Looper.getMainLooper())
private var pressCallback: Runnable? = null
private var longPressCallback: Runnable? = null private var longPressCallback: Runnable? = null
private var onDownTimestamp = 0L private var onDownTimestamp = 0L
private var onDoubleTap: (() -> Unit)? = null
var snIsSelected = false var snIsSelected = false
set(value) { field = value; handleIsSelectedChanged()} set(value) { field = value; handleIsSelectedChanged()}
var onPress: ((rawX: Int, rawY: Int) -> Unit)? = null var onPress: ((rawX: Int, rawY: Int) -> Unit)? = null
@ -58,6 +53,7 @@ class VisibleMessageView : LinearLayout {
const val swipeToReplyThreshold = 80.0f // dp const val swipeToReplyThreshold = 80.0f // dp
const val longPressMovementTreshold = 10.0f // dp const val longPressMovementTreshold = 10.0f // dp
const val longPressDurationThreshold = 250L // ms const val longPressDurationThreshold = 250L // ms
const val maxDoubleTapInterval = 200L
} }
// region Lifecycle // region Lifecycle
@ -143,6 +139,7 @@ class VisibleMessageView : LinearLayout {
if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width } if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width }
// Populate content view // Populate content view
messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread) messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread)
onDoubleTap = { messageContentView.onContentDoubleTap?.invoke() }
} }
private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { private fun setMessageSpacing(isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
@ -195,7 +192,7 @@ class VisibleMessageView : LinearLayout {
val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing) val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing)
val threshold = VisibleMessageView.swipeToReplyThreshold val threshold = VisibleMessageView.swipeToReplyThreshold
val iconSize = toPx(24, context.resources) 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.left = messageContentContainer.right + spacing
swipeToReplyIconRect.top = height - bottomVOffset - iconSize swipeToReplyIconRect.top = height - bottomVOffset - iconSize
swipeToReplyIconRect.right = messageContentContainer.right + iconSize + spacing swipeToReplyIconRect.right = messageContentContainer.right + iconSize + spacing
@ -272,7 +269,18 @@ class VisibleMessageView : LinearLayout {
onSwipeToReply?.invoke() onSwipeToReply?.invoke()
} else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) { } else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) {
longPressCallback?.let { gestureHandler.removeCallbacks(it) } longPressCallback?.let { gestureHandler.removeCallbacks(it) }
onPress?.invoke(event.rawX.toInt(), event.rawY.toInt()) 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(event.rawX.toInt(), event.rawY.toInt()) }
this.pressCallback = newPressCallback
gestureHandler.postDelayed(newPressCallback, VisibleMessageView.maxDoubleTapInterval)
}
} }
resetPosition() resetPosition()
} }
@ -300,5 +308,14 @@ class VisibleMessageView : LinearLayout {
fun onContentClick(rawRect: Rect) { fun onContentClick(rawRect: Rect) {
messageContentView.onContentClick?.invoke(rawRect) messageContentView.onContentClick?.invoke(rawRect)
} }
private fun onPress(rawX: Int, rawY: Int) {
onPress?.invoke(rawX, rawY)
pressCallback = null
}
fun onContentClick() {
messageContentView.onContentClick?.invoke()
}
// endregion // endregion
} }

View File

@ -2,31 +2,29 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.util.Log
import android.view.ViewOutlineProvider import android.view.*
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlinx.android.synthetic.main.view_voice_message.view.* import kotlinx.android.synthetic.main.view_voice_message.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
import org.thoughtcrime.securesms.components.CornerMask import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.roundToLong
class VoiceMessageView : LinearLayout { class VoiceMessageView : LinearLayout, AudioSlidePlayer.Listener {
private val snHandler = Handler(Looper.getMainLooper())
private val cornerMask by lazy { CornerMask(this) } private val cornerMask by lazy { CornerMask(this) }
private var runnable: Runnable? = null private var isPlaying = false
private var mockIsPlaying = false private var progress = 0.0
private var mockProgress = 0L private var duration = 0L
set(value) { field = value; handleProgressChanged() } private var player: AudioSlidePlayer? = null
private var mockDuration = 12000L private var isPreparing = false
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize() } constructor(context: Context) : super(context) { initialize() }
@ -36,14 +34,18 @@ class VoiceMessageView : LinearLayout {
private fun initialize() { private fun initialize() {
LayoutInflater.from(context).inflate(R.layout.view_voice_message, this) LayoutInflater.from(context).inflate(R.layout.view_voice_message, this)
voiceMessageViewDurationTextView.text = String.format("%01d:%02d", voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(mockDuration), TimeUnit.MILLISECONDS.toMinutes(0),
TimeUnit.MILLISECONDS.toSeconds(mockDuration)) TimeUnit.MILLISECONDS.toSeconds(0))
} }
// endregion // endregion
// region Updating // region Updating
fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) { fun bind(message: MmsMessageRecord, isStartOfMessageCluster: Boolean, isEndOfMessageCluster: Boolean) {
val audio = message.slideDeck.audioSlide!! val audio = message.slideDeck.audioSlide!!
val player = AudioSlidePlayer.createFor(context, audio, this)
this.player = player
isPreparing = true
player.play(0.0)
voiceMessageViewLoader.isVisible = audio.isPendingDownload voiceMessageViewLoader.isVisible = audio.isPendingDownload
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing) val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
cornerMask.setTopLeftRadius(cornerRadii[0]) cornerMask.setTopLeftRadius(cornerRadii[0])
@ -52,43 +54,59 @@ class VoiceMessageView : LinearLayout {
cornerMask.setBottomLeftRadius(cornerRadii[3]) cornerMask.setBottomLeftRadius(cornerRadii[3])
} }
private fun handleProgressChanged() { override fun onPlayerStart(player: AudioSlidePlayer) {
if (!isPreparing) { return }
isPreparing = false
duration = player.duration
voiceMessageViewDurationTextView.text = String.format("%01d:%02d", voiceMessageViewDurationTextView.text = String.format("%01d:%02d",
TimeUnit.MILLISECONDS.toMinutes(mockDuration - mockProgress), TimeUnit.MILLISECONDS.toMinutes(duration),
TimeUnit.MILLISECONDS.toSeconds(mockDuration - mockProgress)) TimeUnit.MILLISECONDS.toSeconds(duration))
player.stop()
}
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 val layoutParams = progressView.layoutParams as RelativeLayout.LayoutParams
val fraction = mockProgress.toFloat() / mockDuration.toFloat() layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt()
layoutParams.width = (width.toFloat() * fraction).roundToInt()
progressView.layoutParams = layoutParams progressView.layoutParams = layoutParams
} }
override fun onPlayerStop(player: AudioSlidePlayer) { }
override fun dispatchDraw(canvas: Canvas) { override fun dispatchDraw(canvas: Canvas) {
super.dispatchDraw(canvas) super.dispatchDraw(canvas)
cornerMask.mask(canvas) cornerMask.mask(canvas)
} }
fun recycle() {
// TODO: Implement
}
// endregion // endregion
// region Interaction // region Interaction
fun togglePlayback() { fun togglePlayback() {
mockIsPlaying = !mockIsPlaying val player = this.player ?: return
val iconID = if (mockIsPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play isPlaying = !isPlaying
val iconID = if (isPlaying) R.drawable.exo_icon_pause else R.drawable.exo_icon_play
voiceMessagePlaybackImageView.setImageResource(iconID) voiceMessagePlaybackImageView.setImageResource(iconID)
if (mockIsPlaying) { if (isPlaying) {
updateProgress() player.play(progress)
} else { } else {
runnable?.let { snHandler.removeCallbacks(it) } player.stop()
} }
} }
private fun updateProgress() { fun handleDoubleTap() {
mockProgress += 20L val player = this.player ?: return
val runnable = Runnable { updateProgress() } player.playbackSpeed = if (player.playbackSpeed == 1.0f) 1.5f else 1.0f
this.runnable = runnable
snHandler.postDelayed(runnable, 20L)
} }
// endregion // endregion
} }

View File

@ -31,7 +31,6 @@ import org.session.libsession.utilities.MediaTypes;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.util.ResUtil; import org.thoughtcrime.securesms.util.ResUtil;
public class AudioSlide extends Slide { public class AudioSlide extends Slide {
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) { public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) {

View File

@ -32,11 +32,6 @@
android:id="@+id/menu_context_resend" android:id="@+id/menu_context_resend"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:title="@string/conversation_context__menu_message_details"
android:id="@+id/menu_context_details"
app:showAsAction="never" />
<item <item
android:title="@string/conversation_context__menu_ban_user" android:title="@string/conversation_context__menu_ban_user"
android:id="@+id/menu_context_ban_user" android:id="@+id/menu_context_ban_user"

View File

@ -34,7 +34,7 @@ class DataExtractionNotification() : ControlMessage() {
} }
} }
internal constructor(kind: Kind) : this() { constructor(kind: Kind) : this() {
this.kind = kind this.kind = kind
} }