mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-30 13:35:18 +00:00
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:
commit
6775e0afd7
@ -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;
|
||||
@ -103,9 +104,9 @@ 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();
|
||||
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();
|
||||
@ -184,8 +185,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;
|
||||
|
||||
@ -267,8 +266,17 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
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) {
|
||||
return new Pair<>(0D, 0);
|
||||
} 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() {
|
||||
Util.runOnMain(() -> getListener().onPlayerStart(AudioSlidePlayer.this));
|
||||
}
|
||||
@ -383,7 +401,7 @@ public class AudioSlidePlayer implements SensorEventListener {
|
||||
return;
|
||||
}
|
||||
|
||||
Pair<Double, Integer> progress = player.getProgress();
|
||||
Pair<Double, Integer> progress = player.getProgressTuple();
|
||||
player.notifyOnProgress(progress.first, progress.second);
|
||||
sendEmptyMessageDelayed(0, 50);
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.Manifest
|
||||
import android.animation.FloatEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
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
|
||||
@ -23,6 +27,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.*
|
||||
@ -36,12 +41,17 @@ 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
|
||||
import org.session.libsession.utilities.MediaTypes
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
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.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
|
||||
@ -62,6 +73,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
|
||||
@ -73,8 +85,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.*
|
||||
@ -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.
|
||||
|
||||
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
|
||||
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher {
|
||||
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
|
||||
ConversationActionModeCallbackDelegate {
|
||||
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
|
||||
private var linkPreviewViewModel: LinkPreviewViewModel? = null
|
||||
private var threadID: Long = -1
|
||||
@ -575,6 +590,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()
|
||||
@ -598,6 +614,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) {
|
||||
@ -678,7 +695,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) {
|
||||
@ -694,6 +712,18 @@ 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 {
|
||||
sendTextOnlyMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendTextOnlyMessage() {
|
||||
// Create the message
|
||||
val message = VisibleMessage()
|
||||
message.sentTimestamp = System.currentTimeMillis()
|
||||
@ -713,13 +743,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(threadID)
|
||||
}
|
||||
|
||||
private fun sendAttachments(attachments: List<Attachment>, body: String?) {
|
||||
// TODO: Quotes & link previews
|
||||
private fun sendAttachments(attachments: List<Attachment>, 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
|
||||
@ -733,7 +766,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)
|
||||
}
|
||||
@ -854,6 +887,75 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
audioRecorder.stopRecording()
|
||||
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
|
||||
|
||||
// region General
|
||||
|
@ -36,6 +36,7 @@ class BlockedDialog(private val recipient: Recipient) : BaseDialog() {
|
||||
}
|
||||
|
||||
private fun unblock() {
|
||||
// TODO: Implement
|
||||
DatabaseFactory.getRecipientDatabase(requireContext()).setBlocked(recipient, false)
|
||||
dismiss()
|
||||
}
|
||||
}
|
@ -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,14 @@ 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() {
|
||||
if (quote != null) { return }
|
||||
linkPreview = null
|
||||
inputBarAdditionalContentContainer.removeAllViews()
|
||||
val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight)
|
||||
additionalContentHeight = 0
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
@ -78,3 +89,14 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
||||
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>)
|
||||
}
|
@ -37,7 +37,7 @@ class ControlMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
// TODO: Implement
|
||||
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -30,9 +30,5 @@ class DocumentView : LinearLayout {
|
||||
documentTitleTextView.setTextColor(textColor)
|
||||
documentViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
// TODO: Implement
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -64,9 +64,5 @@ class LinkPreviewView : LinearLayout {
|
||||
super.dispatchDraw(canvas)
|
||||
cornerMask.mask(canvas)
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
// TODO: Implement
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -7,7 +7,6 @@ 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
|
||||
@ -28,12 +27,12 @@ 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
|
||||
|
||||
class VisibleMessageContentView : LinearLayout {
|
||||
var onContentClick: ((rawRect: Rect) -> Unit)? = null
|
||||
var onContentDoubleTap: (() -> Unit)? = null
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
@ -58,6 +57,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)
|
||||
@ -83,6 +83,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))
|
||||
|
@ -4,21 +4,15 @@ 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.*
|
||||
import kotlinx.android.synthetic.main.view_visible_message.view.profilePictureView
|
||||
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.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
|
||||
@ -46,8 +39,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: ((rawX: Int, rawY: Int) -> Unit)? = null
|
||||
@ -58,6 +53,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
|
||||
@ -143,6 +139,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) {
|
||||
@ -195,7 +192,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
|
||||
@ -272,7 +269,18 @@ class VisibleMessageView : LinearLayout {
|
||||
onSwipeToReply?.invoke()
|
||||
} else if ((Date().time - onDownTimestamp) < VisibleMessageView.longPressDurationThreshold) {
|
||||
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()
|
||||
}
|
||||
@ -300,5 +308,14 @@ class VisibleMessageView : LinearLayout {
|
||||
fun onContentClick(rawRect: Rect) {
|
||||
messageContentView.onContentClick?.invoke(rawRect)
|
||||
}
|
||||
|
||||
private fun onPress(rawX: Int, rawY: Int) {
|
||||
onPress?.invoke(rawX, rawY)
|
||||
pressCallback = null
|
||||
}
|
||||
|
||||
fun onContentClick() {
|
||||
messageContentView.onContentClick?.invoke()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
@ -2,31 +2,29 @@ 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.thoughtcrime.securesms.audio.AudioSlidePlayer
|
||||
import org.thoughtcrime.securesms.components.CornerMask
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.MessageBubbleUtilities
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
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 duration = 0L
|
||||
private var player: AudioSlidePlayer? = null
|
||||
private var isPreparing = false
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { initialize() }
|
||||
@ -36,14 +34,18 @@ 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
|
||||
isPreparing = true
|
||||
player.play(0.0)
|
||||
voiceMessageViewLoader.isVisible = audio.isPendingDownload
|
||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||
@ -52,43 +54,59 @@ class VoiceMessageView : LinearLayout {
|
||||
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",
|
||||
TimeUnit.MILLISECONDS.toMinutes(mockDuration - mockProgress),
|
||||
TimeUnit.MILLISECONDS.toSeconds(mockDuration - mockProgress))
|
||||
TimeUnit.MILLISECONDS.toMinutes(duration),
|
||||
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 fraction = mockProgress.toFloat() / mockDuration.toFloat()
|
||||
layoutParams.width = (width.toFloat() * fraction).roundToInt()
|
||||
layoutParams.width = (width.toFloat() * progress.toFloat()).roundToInt()
|
||||
progressView.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
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(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)
|
||||
fun handleDoubleTap() {
|
||||
val player = this.player ?: return
|
||||
player.playbackSpeed = if (player.playbackSpeed == 1.0f) 1.5f else 1.0f
|
||||
}
|
||||
// endregion
|
||||
}
|
@ -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) {
|
||||
|
@ -32,11 +32,6 @@
|
||||
android:id="@+id/menu_context_resend"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:title="@string/conversation_context__menu_message_details"
|
||||
android:id="@+id/menu_context_details"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:title="@string/conversation_context__menu_ban_user"
|
||||
android:id="@+id/menu_context_ban_user"
|
||||
|
@ -34,7 +34,7 @@ class DataExtractionNotification() : ControlMessage() {
|
||||
}
|
||||
}
|
||||
|
||||
internal constructor(kind: Kind) : this() {
|
||||
constructor(kind: Kind) : this() {
|
||||
this.kind = kind
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user