session-android/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt

1451 lines
68 KiB
Kotlin
Raw Normal View History

package org.thoughtcrime.securesms.conversation.v2
import android.Manifest
2021-06-16 07:49:39 +02:00
import android.animation.FloatEvaluator
import android.animation.ValueAnimator
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
2021-06-17 02:53:56 +02:00
import android.content.res.Resources
import android.database.Cursor
2021-06-17 05:18:09 +02:00
import android.graphics.Rect
2021-06-24 02:18:52 +02:00
import android.graphics.Typeface
2021-06-28 02:00:18 +02:00
import android.net.Uri
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
2021-06-29 02:05:39 +02:00
import android.text.TextUtils
2021-06-25 06:42:04 +02:00
import android.util.Log
2021-06-28 03:11:29 +02:00
import android.util.Pair
import android.util.TypedValue
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
2021-07-01 02:02:02 +02:00
import android.widget.LinearLayout
2021-06-16 01:51:50 +02:00
import android.widget.RelativeLayout
2021-06-28 02:44:00 +02:00
import android.widget.Toast
import androidx.activity.viewModels
2021-07-01 02:02:02 +02:00
import androidx.annotation.DimenRes
2021-06-29 02:05:39 +02:00
import androidx.appcompat.app.AlertDialog
2021-06-24 02:18:52 +02:00
import androidx.core.view.isVisible
2021-06-29 06:00:47 +02:00
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
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 dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding
import network.loki.messenger.databinding.ActivityConversationV2Binding
import nl.komponents.kovenant.ui.successUi
2021-06-24 06:21:05 +02:00
import org.session.libsession.messaging.contacts.Contact
2021-06-25 06:42:04 +02:00
import org.session.libsession.messaging.mentions.Mention
2021-06-18 03:00:52 +02:00
import org.session.libsession.messaging.mentions.MentionsManager
import org.session.libsession.messaging.messages.control.DataExtractionNotification
2021-06-28 02:00:18 +02:00
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
2021-06-25 07:20:54 +02:00
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
2021-06-25 07:20:54 +02:00
import org.session.libsession.messaging.sending_receiving.MessageSender
2021-06-28 02:00:18 +02:00
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
2021-06-28 05:29:17 +02:00
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
2021-06-30 03:44:26 +02:00
import org.session.libsession.utilities.Address
2021-06-29 03:14:58 +02:00
import org.session.libsession.utilities.Address.Companion.fromSerialized
2021-06-28 03:11:29 +02:00
import org.session.libsession.utilities.MediaTypes
2021-06-24 07:46:36 +02:00
import org.session.libsession.utilities.TextSecurePreferences
2021-06-30 03:44:26 +02:00
import org.session.libsession.utilities.concurrent.SimpleTask
2021-06-29 03:14:58 +02:00
import org.session.libsession.utilities.recipients.Recipient
2021-06-30 02:45:31 +02:00
import org.session.libsession.utilities.recipients.RecipientModifiedListener
import org.session.libsignal.crypto.MnemonicCodec
2021-06-28 02:44:00 +02:00
import org.session.libsignal.utilities.ListenableFuture
2021-07-01 01:54:09 +02:00
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.hexEncodedPrivateKey
2021-06-24 03:22:32 +02:00
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
2021-06-28 03:11:29 +02:00
import org.thoughtcrime.securesms.audio.AudioRecorder
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog
import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog
2021-06-17 06:34:50 +02:00
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
2021-06-16 01:51:50 +02:00
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate
2021-06-18 03:00:52 +02:00
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.VisibleMessageContentViewDelegate
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VoiceMessageViewDelegate
2021-06-29 06:00:47 +02:00
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.LokiAPIDatabase
import org.thoughtcrime.securesms.database.LokiMessageDatabase
import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
2021-06-28 05:29:17 +02:00
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
2021-06-25 08:09:37 +02:00
import org.thoughtcrime.securesms.giph.ui.GiphyActivity
2021-06-24 07:46:36 +02:00
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository
2021-06-29 07:48:40 +02:00
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
2021-06-24 07:46:36 +02:00
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
2021-06-25 08:09:37 +02:00
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.MediaSendActivity
import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.mms.GifSlide
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.toPx
import java.util.Locale
2021-06-28 02:44:00 +02:00
import java.util.concurrent.ExecutionException
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
2021-06-25 01:19:21 +02:00
// Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually
// part of the conversation activity layout. This is just because it makes the layout a lot simpler. The
// price we pay is a bit of back and forth between the input bar and the conversation activity.
@AndroidEntryPoint
2021-06-23 06:48:29 +02:00
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate,
InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher,
ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener,
2021-07-08 01:25:43 +02:00
SearchBottomBar.EventListener, VoiceMessageViewDelegate {
private lateinit var binding: ActivityConversationV2Binding
private lateinit var actionBarBinding: ActivityConversationV2ActionBarBinding
@Inject lateinit var textSecurePreferences: TextSecurePreferences
@Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var mmsSmsDb: MmsSmsDatabase
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
@Inject lateinit var sessionContactDb: SessionContactDatabase
@Inject lateinit var groupDb: GroupDatabase
@Inject lateinit var lokiApiDb: LokiAPIDatabase
@Inject lateinit var smsDb: SmsDatabase
@Inject lateinit var mmsDb: MmsDatabase
@Inject lateinit var lokiMessageDb: LokiMessageDatabase
@Inject lateinit var viewModelFactory: ConversationViewModel.AssistedFactory
2021-06-18 07:54:24 +02:00
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private val linkPreviewViewModel: LinkPreviewViewModel by lazy {
ViewModelProvider(this, LinkPreviewViewModel.Factory(LinkPreviewRepository(this)))
.get(LinkPreviewViewModel::class.java)
}
private val viewModel: ConversationViewModel by viewModels {
var threadId = intent.getLongExtra(THREAD_ID, -1L)
if (threadId == -1L) {
intent.getParcelableExtra<Address>(ADDRESS)?.let { address ->
val recipient = Recipient.from(this, address, false)
threadId = threadDb.getOrCreateThreadIdFor(recipient)
} ?: finish()
}
viewModelFactory.create(threadId)
}
private var actionMode: ActionMode? = null
2021-06-25 06:42:04 +02:00
private var unreadCount = 0
// Attachments
2021-06-28 03:11:29 +02:00
private val audioRecorder = AudioRecorder(this)
private val stopAudioHandler = Handler(Looper.getMainLooper())
private val stopVoiceMessageRecordingTask = Runnable { sendVoiceMessage() }
2021-06-25 07:53:47 +02:00
private val attachmentManager by lazy { AttachmentManager(this, this) }
2021-06-17 05:18:09 +02:00
private var isLockViewExpanded = false
2021-06-17 08:29:57 +02:00
private var isShowingAttachmentOptions = false
2021-06-25 06:42:04 +02:00
// Mentions
private val mentions = mutableListOf<Mention>()
2021-06-18 03:05:14 +02:00
private var mentionCandidatesView: MentionCandidatesView? = null
2021-06-25 06:42:04 +02:00
private var previousText: CharSequence = ""
private var currentMentionStartIndex = -1
private var isShowingMentionCandidatesView = false
2021-06-30 03:44:26 +02:00
// Search
val searchViewModel: SearchViewModel by viewModels()
2021-06-30 03:44:26 +02:00
var searchViewItem: MenuItem? = null
2021-06-25 02:02:59 +02:00
private val isScrolledToBottom: Boolean
get() {
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
return position == 0
}
2021-06-25 02:02:59 +02:00
private val layoutManager: LinearLayoutManager
get() { return binding.conversationRecyclerView.layoutManager as LinearLayoutManager }
private val seed by lazy {
var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED)
if (hexEncodedSeed == null) {
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
}
val loadFileContents: (String) -> String = { fileName ->
MnemonicUtilities.loadFileContents(this, fileName)
}
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
}
private val adapter by lazy {
val cursor = mmsSmsDb.getConversation(viewModel.threadId)
val adapter = ConversationAdapter(
this,
cursor,
onItemPress = { message, position, view, event ->
handlePress(message, position, view, event)
},
2021-06-09 03:37:50 +02:00
onItemSwipeToReply = { message, position ->
handleSwipeToReply(message, position)
},
onItemLongPress = { message, position ->
handleLongPress(message, position)
2021-06-21 07:26:09 +02:00
},
glide
)
adapter.visibleMessageContentViewDelegate = this
adapter
}
private val glide by lazy { GlideApp.with(this) }
2021-06-18 07:54:24 +02:00
private val lockViewHitMargin by lazy { toPx(40, resources) }
2021-06-17 07:20:19 +02:00
private val gifButton by lazy { InputBarButton(this, R.drawable.ic_gif_white_24dp, hasOpaqueBackground = true, isGIFButton = true) }
2021-06-17 06:34:50 +02:00
private val documentButton by lazy { InputBarButton(this, R.drawable.ic_document_small_dark, hasOpaqueBackground = true) }
private val libraryButton by lazy { InputBarButton(this, R.drawable.ic_baseline_photo_library_24, hasOpaqueBackground = true) }
private val cameraButton by lazy { InputBarButton(this, R.drawable.ic_baseline_photo_camera_24, hasOpaqueBackground = true) }
2021-06-17 02:53:56 +02:00
// region Settings
companion object {
2021-07-01 01:54:09 +02:00
// Extras
const val THREAD_ID = "thread_id"
2021-07-01 01:31:30 +02:00
const val ADDRESS = "address"
2021-07-01 01:54:09 +02:00
// Request codes
2021-06-25 07:53:47 +02:00
const val PICK_DOCUMENT = 2
const val TAKE_PHOTO = 7
const val PICK_GIF = 10
const val PICK_FROM_LIBRARY = 12
2021-06-29 03:14:58 +02:00
const val INVITE_CONTACTS = 124
2021-08-16 07:09:12 +02:00
//flag
2021-10-19 01:33:15 +02:00
const val IS_UNSEND_REQUESTS_ENABLED = true
}
// endregion
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady)
binding = ActivityConversationV2Binding.inflate(layoutInflater)
setContentView(binding.root)
val thread = threadDb.getRecipientForThreadId(viewModel.threadId)
2021-07-09 01:38:45 +02:00
if (thread == null) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
return finish()
}
setUpRecyclerView()
2021-06-15 06:55:57 +02:00
setUpToolBar()
2021-06-17 06:34:50 +02:00
setUpInputBar()
setUpLinkPreviewObserver()
2021-06-22 08:23:47 +02:00
restoreDraftIfNeeded()
setUpUiStateObserver()
binding.scrollToBottomButton.setOnClickListener {
val layoutManager = binding.conversationRecyclerView.layoutManager ?: return@setOnClickListener
if (layoutManager.isSmoothScrolling) {
binding.conversationRecyclerView.scrollToPosition(0)
} else {
binding.conversationRecyclerView.smoothScrollToPosition(0)
}
}
unreadCount = mmsSmsDb.getUnreadCount(viewModel.threadId)
2021-06-25 02:02:59 +02:00
updateUnreadCountIndicator()
2021-06-24 03:22:32 +02:00
setUpTypingObserver()
2021-06-30 02:45:31 +02:00
setUpRecipientObserver()
2021-06-24 03:38:06 +02:00
updateSubtitle()
getLatestOpenGroupInfoIfNeeded()
2021-06-24 06:21:05 +02:00
setUpBlockedBanner()
binding.searchBottomBar.setEventListener(this)
2021-06-29 06:00:47 +02:00
setUpSearchResultObserver()
2021-06-25 02:18:04 +02:00
scrollToFirstUnreadMessageIfNeeded()
showOrHideInputIfNeeded()
if (viewModel.recipient.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
2021-07-09 01:38:45 +02:00
if (openGroup == null) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show()
return finish()
}
}
}
override fun onResume() {
super.onResume()
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(viewModel.threadId)
threadDb.markAllAsRead(viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
}
override fun onPause() {
super.onPause()
ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(-1)
}
2021-06-25 08:30:23 +02:00
override fun getSystemService(name: String): Any? {
if (name == ActivityDispatcher.SERVICE) {
return this
}
return super.getSystemService(name)
}
override fun dispatchIntent(body: (Context) -> Intent?) {
val intent = body(this) ?: return
2021-06-25 08:30:23 +02:00
push(intent, false)
}
override fun showDialog(baseDialog: BaseDialog, tag: String?) {
baseDialog.show(supportFragmentManager, tag)
}
2021-06-25 08:30:23 +02:00
private fun setUpRecyclerView() {
binding.conversationRecyclerView.adapter = adapter
2021-06-07 08:36:05 +02:00
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
binding.conversationRecyclerView.layoutManager = layoutManager
2021-06-07 01:48:01 +02:00
// Workaround for the fact that CursorRecyclerViewAdapter doesn't auto-update automatically (even though it says it will)
LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks<Cursor> {
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
return ConversationLoader(viewModel.threadId, this@ConversationActivityV2)
}
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
adapter.changeCursor(cursor)
}
override fun onLoaderReset(cursor: Loader<Cursor>) {
adapter.changeCursor(null)
}
})
binding.conversationRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
handleRecyclerViewScrolled()
}
})
}
2021-06-15 06:55:57 +02:00
private fun setUpToolBar() {
val actionBar = supportActionBar!!
actionBarBinding = ActivityConversationV2ActionBarBinding.inflate(layoutInflater)
actionBar.title = ""
actionBar.customView = actionBarBinding.root
actionBar.setDisplayShowCustomEnabled(true)
actionBarBinding.conversationTitleView.text = viewModel.recipient.toShortString()
@DimenRes val sizeID: Int = if (viewModel.recipient.isClosedGroupRecipient) {
R.dimen.medium_profile_picture_size
2021-07-01 02:02:02 +02:00
} else {
R.dimen.small_profile_picture_size
2021-07-01 02:02:02 +02:00
}
val size = resources.getDimension(sizeID).roundToInt()
actionBarBinding.profilePictureView.layoutParams = LinearLayout.LayoutParams(size, size)
actionBarBinding.profilePictureView.glide = glide
MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this)
actionBarBinding.profilePictureView.update(viewModel.recipient)
}
2021-06-07 01:48:01 +02:00
2021-06-17 06:34:50 +02:00
private fun setUpInputBar() {
binding.inputBar.delegate = this
binding.inputBarRecordingView.delegate = this
2021-06-17 06:34:50 +02:00
// GIF button
binding.gifButtonContainer.addView(gifButton)
2021-06-17 06:34:50 +02:00
gifButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
2021-06-25 07:53:47 +02:00
gifButton.onUp = { showGIFPicker() }
gifButton.snIsEnabled = false
2021-06-17 06:34:50 +02:00
// Document button
binding.documentButtonContainer.addView(documentButton)
2021-06-17 06:34:50 +02:00
documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
2021-06-25 07:53:47 +02:00
documentButton.onUp = { showDocumentPicker() }
documentButton.snIsEnabled = false
2021-06-17 06:34:50 +02:00
// Library button
binding.libraryButtonContainer.addView(libraryButton)
2021-06-17 06:34:50 +02:00
libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
2021-06-25 07:53:47 +02:00
libraryButton.onUp = { pickFromLibrary() }
libraryButton.snIsEnabled = false
2021-06-17 06:34:50 +02:00
// Camera button
binding.cameraButtonContainer.addView(cameraButton)
2021-06-17 06:34:50 +02:00
cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
2021-06-25 07:53:47 +02:00
cameraButton.onUp = { showCamera() }
cameraButton.snIsEnabled = false
2021-06-17 06:34:50 +02:00
}
2021-06-22 08:23:47 +02:00
private fun restoreDraftIfNeeded() {
2021-07-01 01:54:09 +02:00
val mediaURI = intent.data
val mediaType = AttachmentManager.MediaType.from(intent.type)
if (mediaURI != null && mediaType != null) {
if (AttachmentManager.MediaType.IMAGE == mediaType || AttachmentManager.MediaType.GIF == mediaType || AttachmentManager.MediaType.VIDEO == mediaType) {
val media = Media(mediaURI, MediaUtil.getMimeType(this, mediaURI)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent())
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient, ""), PICK_FROM_LIBRARY)
2021-07-01 01:54:09 +02:00
return
} else {
prepMediaForSending(mediaURI, mediaType).addListener(object : ListenableFuture.Listener<Boolean> {
override fun onSuccess(result: Boolean?) {
sendAttachments(attachmentManager.buildSlideDeck().asAttachments(), null)
}
override fun onFailure(e: ExecutionException?) {
Toast.makeText(this@ConversationActivityV2, R.string.activity_conversation_attachment_prep_failed, Toast.LENGTH_LONG).show()
}
})
return
}
} else if (intent.hasExtra(Intent.EXTRA_TEXT)) {
val dataTextExtra = intent.getCharSequenceExtra(Intent.EXTRA_TEXT) ?: ""
binding.inputBar.text = dataTextExtra.toString()
} else {
viewModel.getDraft()?.let { text ->
binding.inputBar.text = text
}
2021-07-01 01:54:09 +02:00
}
2021-06-22 08:23:47 +02:00
}
private fun addOpenGroupGuidelinesIfNeeded(isOxenHostedOpenGroup: Boolean) {
2021-06-23 05:11:21 +02:00
if (!isOxenHostedOpenGroup) { return }
binding.openGroupGuidelinesView.visibility = View.VISIBLE
val recyclerViewLayoutParams = binding.conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams
2021-06-23 05:57:13 +02:00
recyclerViewLayoutParams.topMargin = toPx(57, resources) // The height of the open group guidelines view is hardcoded to this
binding.conversationRecyclerView.layoutParams = recyclerViewLayoutParams
2021-06-23 05:11:21 +02:00
}
2021-06-24 03:22:32 +02:00
private fun setUpTypingObserver() {
ApplicationContext.getInstance(this).typingStatusRepository.getTypists(viewModel.threadId).observe(this) { state ->
2021-06-24 03:22:32 +02:00
val recipients = if (state != null) state.typists else listOf()
// FIXME: Also checking isScrolledToBottom is a quick fix for an issue where the
// typing indicator overlays the recycler view when scrolled up
binding.typingIndicatorViewContainer.isVisible = recipients.isNotEmpty() && isScrolledToBottom
binding.typingIndicatorViewContainer.setTypists(recipients)
inputBarHeightChanged(binding.inputBar.height)
2021-06-24 03:22:32 +02:00
}
if (textSecurePreferences.isTypingIndicatorsEnabled()) {
binding.inputBar.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(text: String?) {
ApplicationContext.getInstance(this@ConversationActivityV2).typingStatusSender.onTypingStarted(viewModel.threadId)
}
})
}
2021-06-24 03:22:32 +02:00
}
2021-06-30 02:45:31 +02:00
private fun setUpRecipientObserver() {
viewModel.recipient.addListener(this)
2021-06-30 02:45:31 +02:00
}
private fun getLatestOpenGroupInfoIfNeeded() {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) ?: return
OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() }
}
2021-06-24 06:21:05 +02:00
private fun setUpBlockedBanner() {
if (viewModel.recipient.isGroupRecipient) { return }
val sessionID = viewModel.recipient.address.toString()
val contact = sessionContactDb.getContactWithSessionID(sessionID)
2021-06-24 06:21:05 +02:00
val name = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionID
binding.blockedBannerTextView.text = resources.getString(R.string.activity_conversation_blocked_banner_text, name)
binding.blockedBanner.isVisible = viewModel.recipient.isBlocked
binding.blockedBanner.setOnClickListener { viewModel.unblock() }
2021-06-24 06:21:05 +02:00
}
2021-06-24 07:46:36 +02:00
private fun setUpLinkPreviewObserver() {
if (!textSecurePreferences.isLinkPreviewsEnabled()) {
2021-06-24 07:46:36 +02:00
linkPreviewViewModel.onUserCancel(); return
}
linkPreviewViewModel.linkPreviewState.observe(this) { previewState: LinkPreviewState? ->
2021-06-24 07:46:36 +02:00
if (previewState == null) return@observe
when {
previewState.isLoading -> {
binding.inputBar.draftLinkPreview()
}
previewState.linkPreview.isPresent -> {
binding.inputBar.updateLinkPreviewDraft(glide, previewState.linkPreview.get())
}
else -> {
binding.inputBar.cancelLinkPreviewDraft()
}
2021-06-24 07:46:36 +02:00
}
}
}
private fun setUpUiStateObserver() {
lifecycleScope.launchWhenStarted {
viewModel.uiState.collect { uiState ->
uiState.uiMessages.firstOrNull()?.let {
Toast.makeText(this@ConversationActivityV2, it.message, Toast.LENGTH_LONG).show()
viewModel.messageShown(it.id)
}
addOpenGroupGuidelinesIfNeeded(uiState.isOxenHostedOpenGroup)
}
}
2021-06-24 07:46:36 +02:00
}
2021-06-25 02:18:04 +02:00
private fun scrollToFirstUnreadMessageIfNeeded() {
val lastSeenTimestamp = threadDb.getLastSeenAndHasSent(viewModel.threadId).first()
val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return
2021-06-25 02:18:04 +02:00
if (lastSeenItemPosition <= 3) { return }
binding.conversationRecyclerView.scrollToPosition(lastSeenItemPosition)
}
2021-06-07 01:48:01 +02:00
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, viewModel.recipient, viewModel.threadId, this) { onOptionsItemSelected(it) }
super.onPrepareOptionsMenu(menu)
return true
2021-06-07 01:48:01 +02:00
}
2021-06-22 08:23:47 +02:00
override fun onDestroy() {
viewModel.saveDraft(binding.inputBar.text.trim())
2021-06-22 08:23:47 +02:00
super.onDestroy()
}
// endregion
// region Animation & Updating
2021-06-30 02:45:31 +02:00
override fun onModified(recipient: Recipient) {
2021-06-30 05:15:39 +02:00
runOnUiThread {
if (viewModel.recipient.isContactRecipient) {
binding.blockedBanner.isVisible = viewModel.recipient.isBlocked
2021-06-30 05:15:39 +02:00
}
updateSubtitle()
showOrHideInputIfNeeded()
actionBarBinding.profilePictureView.update(recipient)
}
}
private fun showOrHideInputIfNeeded() {
if (viewModel.recipient.isClosedGroupRecipient) {
val group = groupDb.getGroup(viewModel.recipient.address.toGroupString()).orNull()
val isActive = (group?.isActive == true)
binding.inputBar.showInput = isActive
} else {
binding.inputBar.showInput = true
2021-06-30 02:45:31 +02:00
}
}
2021-06-16 01:51:50 +02:00
override fun inputBarHeightChanged(newValue: Int) {
2021-06-24 07:20:33 +02:00
@Suppress("NAME_SHADOWING") val newValue = max(newValue, resources.getDimension(R.dimen.input_bar_height).roundToInt())
2021-06-24 03:22:32 +02:00
// 36 DP is the exact height of the typing indicator view. It's also exactly 18 * 2, and 18 is the large message
// corner radius. This makes 36 DP look "correct" in the context of other messages on the screen.
val typingIndicatorHeight = if (binding.typingIndicatorViewContainer.isVisible) toPx(36, resources) else 0
2021-06-18 03:00:52 +02:00
// Recycler view
val recyclerViewLayoutParams = binding.conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams
2021-06-25 07:11:38 +02:00
recyclerViewLayoutParams.bottomMargin = newValue + typingIndicatorHeight
binding.conversationRecyclerView.layoutParams = recyclerViewLayoutParams
2021-06-18 07:11:41 +02:00
// Additional content container
val additionalContentContainerLayoutParams = binding.additionalContentContainer.layoutParams as RelativeLayout.LayoutParams
2021-06-18 07:11:41 +02:00
additionalContentContainerLayoutParams.bottomMargin = newValue
binding.additionalContentContainer.layoutParams = additionalContentContainerLayoutParams
2021-06-18 03:00:52 +02:00
// Attachment options
val attachmentButtonHeight = binding.inputBar.attachmentButtonsContainerHeight
val bottomMargin = (newValue - binding.inputBar.additionalContentHeight - attachmentButtonHeight) / 2
val margin = toPx(8, resources)
val attachmentOptionsContainerLayoutParams = binding.attachmentOptionsContainer.layoutParams as RelativeLayout.LayoutParams
attachmentOptionsContainerLayoutParams.bottomMargin = bottomMargin + attachmentButtonHeight + margin
binding.attachmentOptionsContainer.layoutParams = attachmentOptionsContainerLayoutParams
2021-06-23 07:14:19 +02:00
// Scroll to bottom button
val scrollToBottomButtonLayoutParams = binding.scrollToBottomButton.layoutParams as RelativeLayout.LayoutParams
scrollToBottomButtonLayoutParams.bottomMargin = newValue + binding.additionalContentContainer.height + toPx(12, resources)
binding.scrollToBottomButton.layoutParams = scrollToBottomButtonLayoutParams
2021-06-16 01:51:50 +02:00
}
2021-06-16 06:50:41 +02:00
2021-06-18 03:00:52 +02:00
override fun inputBarEditTextContentChanged(newContent: CharSequence) {
if (textSecurePreferences.isLinkPreviewsEnabled()) {
linkPreviewViewModel.onTextChanged(this, binding.inputBar.text, 0, 0)
2021-06-29 07:48:40 +02:00
}
2021-06-25 06:42:04 +02:00
showOrHideMentionCandidatesIfNeeded(newContent)
2021-06-29 07:48:40 +02:00
if (LinkPreviewUtil.findWhitelistedUrls(newContent.toString()).isNotEmpty()
&& !textSecurePreferences.isLinkPreviewsEnabled() && !textSecurePreferences.hasSeenLinkPreviewSuggestionDialog()) {
2021-06-29 07:48:40 +02:00
LinkPreviewDialog {
setUpLinkPreviewObserver()
linkPreviewViewModel.onEnabled()
linkPreviewViewModel.onTextChanged(this, binding.inputBar.text, 0, 0)
2021-06-29 07:48:40 +02:00
}.show(supportFragmentManager, "Link Preview Dialog")
textSecurePreferences.setHasSeenLinkPreviewSuggestionDialog()
2021-06-29 07:48:40 +02:00
}
2021-06-18 03:00:52 +02:00
}
2021-06-25 06:42:04 +02:00
private fun showOrHideMentionCandidatesIfNeeded(text: CharSequence) {
2021-06-25 07:11:38 +02:00
if (text.length < previousText.length) {
2021-06-25 06:42:04 +02:00
currentMentionStartIndex = -1
hideMentionCandidates()
val mentionsToRemove = mentions.filter { !text.contains(it.displayName) }
mentions.removeAll(mentionsToRemove)
2021-06-18 03:05:14 +02:00
}
2021-06-25 06:42:04 +02:00
if (text.isNotEmpty()) {
val lastCharIndex = text.lastIndex
val lastChar = text[lastCharIndex]
2021-06-25 07:11:38 +02:00
// Check if there is whitespace before the '@' or the '@' is the first character
val isCharacterBeforeLastWhiteSpaceOrStartOfLine: Boolean
if (text.length == 1) {
isCharacterBeforeLastWhiteSpaceOrStartOfLine = true // Start of line
} else {
val charBeforeLast = text[lastCharIndex - 1]
isCharacterBeforeLastWhiteSpaceOrStartOfLine = Character.isWhitespace(charBeforeLast)
}
if (lastChar == '@' && isCharacterBeforeLastWhiteSpaceOrStartOfLine) {
2021-06-25 06:42:04 +02:00
currentMentionStartIndex = lastCharIndex
showOrUpdateMentionCandidatesIfNeeded()
2021-06-25 07:11:38 +02:00
} else if (Character.isWhitespace(lastChar) || lastChar == '@') { // the lastCharacter == "@" is to check for @@
2021-06-25 06:42:04 +02:00
currentMentionStartIndex = -1
hideMentionCandidates()
} else if (currentMentionStartIndex != -1) {
val query = text.substring(currentMentionStartIndex + 1) // + 1 to get rid of the "@"
showOrUpdateMentionCandidatesIfNeeded(query)
}
}
previousText = text
}
private fun showOrUpdateMentionCandidatesIfNeeded(query: String = "") {
if (!isShowingMentionCandidatesView) {
binding.additionalContentContainer.removeAllViews()
2021-06-25 06:42:04 +02:00
val view = MentionCandidatesView(this)
view.glide = glide
2021-06-25 07:11:38 +02:00
view.onCandidateSelected = { handleMentionSelected(it) }
binding.additionalContentContainer.addView(view)
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
2021-06-25 06:42:04 +02:00
this.mentionCandidatesView = view
view.show(candidates, viewModel.threadId)
2021-06-25 06:42:04 +02:00
view.alpha = 0.0f
val animation = ValueAnimator.ofObject(FloatEvaluator(), view.alpha, 1.0f)
animation.duration = 250L
animation.addUpdateListener { animator ->
view.alpha = animator.animatedValue as Float
}
animation.start()
} else {
val candidates = MentionsManager.getMentionCandidates(query, viewModel.threadId, viewModel.recipient.isOpenGroupRecipient)
2021-06-25 06:42:04 +02:00
this.mentionCandidatesView!!.setMentionCandidates(candidates)
}
isShowingMentionCandidatesView = true
2021-06-18 03:05:14 +02:00
}
private fun hideMentionCandidates() {
2021-06-25 06:42:04 +02:00
if (isShowingMentionCandidatesView) {
val mentionCandidatesView = mentionCandidatesView ?: return
val animation = ValueAnimator.ofObject(FloatEvaluator(), mentionCandidatesView.alpha, 0.0f)
animation.duration = 250L
animation.addUpdateListener { animator ->
mentionCandidatesView.alpha = animator.animatedValue as Float
if (animator.animatedFraction == 1.0f) { binding.additionalContentContainer.removeAllViews() }
2021-06-25 06:42:04 +02:00
}
animation.start()
2021-06-18 03:05:14 +02:00
}
2021-06-25 06:42:04 +02:00
isShowingMentionCandidatesView = false
2021-06-18 03:00:52 +02:00
}
2021-06-17 08:29:57 +02:00
override fun toggleAttachmentOptions() {
val targetAlpha = if (isShowingAttachmentOptions) 0.0f else 1.0f
val allButtonContainers = listOf( binding.cameraButtonContainer, binding.libraryButtonContainer, binding.documentButtonContainer, binding.gifButtonContainer)
2021-06-18 01:51:44 +02:00
val isReversed = isShowingAttachmentOptions // Run the animation in reverse
val count = allButtonContainers.size
allButtonContainers.indices.forEach { index ->
val view = allButtonContainers[index]
2021-06-17 08:29:57 +02:00
val animation = ValueAnimator.ofObject(FloatEvaluator(), view.alpha, targetAlpha)
animation.duration = 250L
2021-06-18 01:51:44 +02:00
animation.startDelay = if (isReversed) 50L * (count - index.toLong()) else 50L * index.toLong()
2021-06-17 08:29:57 +02:00
animation.addUpdateListener { animator ->
view.alpha = animator.animatedValue as Float
}
animation.start()
}
isShowingAttachmentOptions = !isShowingAttachmentOptions
val allButtons = listOf( cameraButton, libraryButton, documentButton, gifButton )
allButtons.forEach { it.snIsEnabled = isShowingAttachmentOptions }
2021-06-17 08:29:57 +02:00
}
2021-06-16 06:50:41 +02:00
override fun showVoiceMessageUI() {
binding.inputBarRecordingView.show()
binding.inputBar.alpha = 0.0f
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
animation.duration = 250L
animation.addUpdateListener { animator ->
binding.inputBar.alpha = animator.animatedValue as Float
}
animation.start()
}
2021-06-18 07:54:24 +02:00
private fun expandVoiceMessageLockView() {
val animation = ValueAnimator.ofObject(FloatEvaluator(), binding.inputBarRecordingView.lockView.scaleX, 1.10f)
2021-06-18 07:54:24 +02:00
animation.duration = 250L
animation.addUpdateListener { animator ->
binding.inputBarRecordingView.lockView.scaleX = animator.animatedValue as Float
binding.inputBarRecordingView.lockView.scaleY = animator.animatedValue as Float
2021-06-18 07:54:24 +02:00
}
animation.start()
}
private fun collapseVoiceMessageLockView() {
val animation = ValueAnimator.ofObject(FloatEvaluator(), binding.inputBarRecordingView.lockView.scaleX, 1.0f)
2021-06-18 07:54:24 +02:00
animation.duration = 250L
animation.addUpdateListener { animator ->
binding.inputBarRecordingView.lockView.scaleX = animator.animatedValue as Float
binding.inputBarRecordingView.lockView.scaleY = animator.animatedValue as Float
2021-06-18 07:54:24 +02:00
}
animation.start()
}
private fun hideVoiceMessageUI() {
val chevronImageView = binding.inputBarRecordingView.chevronImageView
val slideToCancelTextView = binding.inputBarRecordingView.slideToCancelTextView
2021-06-18 07:54:24 +02:00
listOf( chevronImageView, slideToCancelTextView ).forEach { view ->
val animation = ValueAnimator.ofObject(FloatEvaluator(), view.translationX, 0.0f)
animation.duration = 250L
animation.addUpdateListener { animator ->
view.translationX = animator.animatedValue as Float
}
animation.start()
}
binding.inputBarRecordingView.hide()
2021-06-18 07:54:24 +02:00
}
override fun handleVoiceMessageUIHidden() {
binding.inputBar.alpha = 1.0f
val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f)
animation.duration = 250L
animation.addUpdateListener { animator ->
binding.inputBar.alpha = animator.animatedValue as Float
}
animation.start()
2021-06-16 06:50:41 +02:00
}
2021-06-23 06:48:29 +02:00
private fun handleRecyclerViewScrolled() {
// FIXME: Checking isScrolledToBottom is a quick fix for an issue where the
// typing indicator overlays the recycler view when scrolled up
val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible
binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
val isTypingIndicatorVisibleAfter = binding.typingIndicatorViewContainer.isVisible
if (isTypingIndicatorVisibleAfter != wasTypingIndicatorVisibleBefore) {
inputBarHeightChanged(binding.inputBar.height)
}
binding.scrollToBottomButton.isVisible = !isScrolledToBottom
unreadCount = min(unreadCount, layoutManager.findFirstVisibleItemPosition())
2021-06-25 02:02:59 +02:00
updateUnreadCountIndicator()
}
2021-06-25 02:02:59 +02:00
private fun updateUnreadCountIndicator() {
val formattedUnreadCount = if (unreadCount < 10000) unreadCount.toString() else "9999+"
binding.unreadCountTextView.text = formattedUnreadCount
val textSize = if (unreadCount < 10000) 12.0f else 9.0f
binding.unreadCountTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
binding.unreadCountTextView.setTypeface(Typeface.DEFAULT, if (unreadCount < 100) Typeface.BOLD else Typeface.NORMAL)
binding.unreadCountIndicator.isVisible = (unreadCount != 0)
2021-06-23 06:48:29 +02:00
}
2021-06-24 03:38:06 +02:00
private fun updateSubtitle() {
actionBarBinding.muteIconImageView.isVisible = viewModel.recipient.isMuted
actionBarBinding.conversationSubtitleView.isVisible = true
if (viewModel.recipient.isMuted) {
if (viewModel.recipient.mutedUntil != Long.MAX_VALUE) {
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(viewModel.recipient.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))
} else {
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_forever)
}
} else if (viewModel.recipient.isGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId)
2021-06-24 03:38:06 +02:00
if (openGroup != null) {
val userCount = lokiApiDb.getUserCount(openGroup.room, openGroup.server) ?: 0
actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_member_count, userCount)
2021-06-24 03:38:06 +02:00
} else {
actionBarBinding.conversationSubtitleView.isVisible = false
2021-06-24 03:38:06 +02:00
}
} else {
actionBarBinding.conversationSubtitleView.isVisible = false
2021-06-24 03:38:06 +02:00
}
2021-06-23 06:48:29 +02:00
}
2021-06-16 01:51:50 +02:00
// endregion
// region Interaction
2021-06-07 01:48:01 +02:00
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
return false
}
return ConversationMenuHelper.onOptionItemSelected(this, item, viewModel.recipient)
}
2021-06-04 05:15:43 +02:00
2021-06-09 03:37:50 +02:00
// `position` is the adapter position; not the visual position
private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView, event: MotionEvent) {
val actionMode = this.actionMode
if (actionMode != null) {
adapter.toggleSelection(message, position)
val actionModeCallback = ConversationActionModeCallback(adapter, viewModel.threadId, this)
actionModeCallback.delegate = this
actionModeCallback.updateActionModeMenu(actionMode.menu)
if (adapter.selectedItems.isEmpty()) {
actionMode.finish()
this.actionMode = null
}
} else {
2021-06-24 03:24:25 +02:00
// NOTE:
// We have to use onContentClick (rather than a click listener directly on
// the view) so as to not interfere with all the other gestures. Do not add
// onClickListeners directly to message content views.
view.onContentClick(event)
}
}
2021-06-09 03:37:50 +02:00
// `position` is the adapter position; not the visual position
private fun handleSwipeToReply(message: MessageRecord, position: Int) {
binding.inputBar.draftQuote(viewModel.recipient, message, glide)
2021-06-09 03:37:50 +02:00
}
// `position` is the adapter position; not the visual position
private fun handleLongPress(message: MessageRecord, position: Int) {
val actionMode = this.actionMode
val actionModeCallback = ConversationActionModeCallback(adapter, viewModel.threadId, this)
actionModeCallback.delegate = this
2021-06-30 03:44:26 +02:00
searchViewItem?.collapseActionView()
if (actionMode == null) { // Nothing should be selected if this is the case
adapter.toggleSelection(message, position)
this.actionMode = startActionMode(actionModeCallback, ActionMode.TYPE_PRIMARY)
2021-06-04 07:10:58 +02:00
} else {
adapter.toggleSelection(message, position)
actionModeCallback.updateActionModeMenu(actionMode.menu)
if (adapter.selectedItems.isEmpty()) {
actionMode.finish()
this.actionMode = null
}
2021-06-04 07:10:58 +02:00
}
}
2021-06-17 02:53:56 +02:00
override fun onMicrophoneButtonMove(event: MotionEvent) {
val rawX = event.rawX
val chevronImageView = binding.inputBarRecordingView.chevronImageView
val slideToCancelTextView = binding.inputBarRecordingView.slideToCancelTextView
2021-06-17 02:53:56 +02:00
if (rawX < screenWidth / 2) {
val translationX = rawX - screenWidth / 2
val sign = -1.0f
val chevronDamping = 4.0f
val labelDamping = 3.0f
val chevronX = (chevronDamping * (sqrt(abs(translationX)) / sqrt(chevronDamping))) * sign
val labelX = (labelDamping * (sqrt(abs(translationX)) / sqrt(labelDamping))) * sign
chevronImageView.translationX = chevronX
slideToCancelTextView.translationX = labelX
} else {
chevronImageView.translationX = 0.0f
slideToCancelTextView.translationX = 0.0f
}
2021-06-17 05:18:09 +02:00
if (isValidLockViewLocation(event.rawX.roundToInt(), event.rawY.roundToInt())) {
if (!isLockViewExpanded) {
2021-06-18 07:54:24 +02:00
expandVoiceMessageLockView()
2021-06-17 05:18:09 +02:00
isLockViewExpanded = true
}
} else {
if (isLockViewExpanded) {
2021-06-18 07:54:24 +02:00
collapseVoiceMessageLockView()
2021-06-17 05:18:09 +02:00
isLockViewExpanded = false
}
}
}
2021-06-17 02:53:56 +02:00
override fun onMicrophoneButtonCancel(event: MotionEvent) {
2021-06-18 07:54:24 +02:00
hideVoiceMessageUI()
2021-06-17 02:53:56 +02:00
}
override fun onMicrophoneButtonUp(event: MotionEvent) {
val x = event.rawX.roundToInt()
val y = event.rawY.roundToInt()
if (isValidLockViewLocation(x, y)) {
binding.inputBarRecordingView.lock()
2021-06-17 06:01:43 +02:00
} else {
val recordButtonOverlay = binding.inputBarRecordingView.recordButtonOverlay
val location = IntArray(2) { 0 }
recordButtonOverlay.getLocationOnScreen(location)
val hitRect = Rect(location[0], location[1], location[0] + recordButtonOverlay.width, location[1] + recordButtonOverlay.height)
if (hitRect.contains(x, y)) {
sendVoiceMessage()
} else {
cancelVoiceMessage()
}
2021-06-17 06:01:43 +02:00
}
2021-06-17 02:53:56 +02:00
}
2021-06-18 07:54:24 +02:00
private fun isValidLockViewLocation(x: Int, y: Int): Boolean {
2021-06-23 05:57:13 +02:00
// We can be anywhere above the lock view and a bit to the side of it (at most `lockViewHitMargin`
// to the side)
2021-06-18 07:54:24 +02:00
val lockViewLocation = IntArray(2) { 0 }
binding.inputBarRecordingView.lockView.getLocationOnScreen(lockViewLocation)
2021-06-18 07:54:24 +02:00
val hitRect = Rect(lockViewLocation[0] - lockViewHitMargin, 0,
lockViewLocation[0] + binding.inputBarRecordingView.lockView.width + lockViewHitMargin, lockViewLocation[1] + binding.inputBarRecordingView.lockView.height)
2021-06-18 07:54:24 +02:00
return hitRect.contains(x, y)
2021-06-17 02:53:56 +02:00
}
2021-06-24 06:21:05 +02:00
2021-06-25 07:11:38 +02:00
private fun handleMentionSelected(mention: Mention) {
if (currentMentionStartIndex == -1) { return }
mentions.add(mention)
val previousText = binding.inputBar.text
2021-06-25 07:11:38 +02:00
val newText = previousText.substring(0, currentMentionStartIndex) + "@" + mention.displayName + " "
binding.inputBar.text = newText
binding.inputBar.setSelection(newText.length)
2021-06-25 07:11:38 +02:00
currentMentionStartIndex = -1
hideMentionCandidates()
this.previousText = newText
}
override fun scrollToMessageIfPossible(timestamp: Long) {
val lastSeenItemPosition = adapter.getItemPositionForTimestamp(timestamp) ?: return
binding.conversationRecyclerView.scrollToPosition(lastSeenItemPosition)
}
override fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) {
if (indexInAdapter < 0 || indexInAdapter >= adapter.itemCount) { return }
val viewHolder = binding.conversationRecyclerView.findViewHolderForAdapterPosition(indexInAdapter) as? ConversationAdapter.VisibleMessageViewHolder
viewHolder?.view?.playVoiceMessage()
2021-07-08 01:25:43 +02:00
}
2021-06-28 02:00:18 +02:00
override fun sendMessage() {
if (viewModel.recipient.isContactRecipient && viewModel.recipient.isBlocked) {
BlockedDialog(viewModel.recipient).show(supportFragmentManager, "Blocked Dialog")
2021-06-28 05:36:15 +02:00
return
}
if (binding.inputBar.linkPreview != null || binding.inputBar.quote != null) {
sendAttachments(listOf(), getMessageBody(), binding.inputBar.quote, binding.inputBar.linkPreview)
2021-06-28 05:29:17 +02:00
} else {
sendTextOnlyMessage()
}
}
override fun commitInputContent(contentUri: Uri) {
val media = Media(contentUri, MediaUtil.getMimeType(this, contentUri)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent())
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient, getMessageBody()), PICK_FROM_LIBRARY)
}
private fun sendTextOnlyMessage(hasPermissionToSendSeed: Boolean = false) {
val text = getMessageBody()
val userPublicKey = textSecurePreferences.getLocalNumber()
val isNoteToSelf = (viewModel.recipient.isContactRecipient && viewModel.recipient.address.toString() == userPublicKey)
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
val dialog = SendSeedDialog { sendTextOnlyMessage(true) }
return dialog.show(supportFragmentManager, "Send Seed Dialog")
}
2021-06-25 07:24:34 +02:00
// Create the message
2021-06-25 07:20:54 +02:00
val message = VisibleMessage()
message.sentTimestamp = System.currentTimeMillis()
message.text = text
val outgoingTextMessage = OutgoingTextMessage.from(message, viewModel.recipient)
2021-06-25 07:24:34 +02:00
// Clear the input bar
binding.inputBar.text = ""
binding.inputBar.cancelQuoteDraft()
binding.inputBar.cancelLinkPreviewDraft()
2021-06-25 07:24:34 +02:00
// Clear mentions
previousText = ""
currentMentionStartIndex = -1
mentions.clear()
// Put the message in the database
message.id = smsDb.insertMessageOutbox(viewModel.threadId, outgoingTextMessage, false, message.sentTimestamp!!) { }
2021-06-25 07:24:34 +02:00
// Send it
MessageSender.send(message, viewModel.recipient.address)
2021-06-25 07:24:34 +02:00
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
2021-06-25 06:42:04 +02:00
}
2021-06-25 07:53:47 +02:00
2021-06-28 05:29:17 +02:00
private fun sendAttachments(attachments: List<Attachment>, body: String?, quotedMessage: MessageRecord? = null, linkPreview: LinkPreview? = null) {
2021-06-28 02:00:18 +02:00
// Create the message
val message = VisibleMessage()
message.sentTimestamp = System.currentTimeMillis()
message.text = body
2021-06-28 05:29:17 +02:00
val quote = quotedMessage?.let {
val quotedAttachments = (it as? MmsMessageRecord)?.slideDeck?.asAttachments() ?: listOf()
val sender = if (it.isOutgoing) fromSerialized(textSecurePreferences.getLocalNumber()!!) else it.individualRecipient.address
QuoteModel(it.dateSent, sender, it.body, false, quotedAttachments)
2021-06-28 05:29:17 +02:00
}
val outgoingTextMessage = OutgoingMediaMessage.from(message, viewModel.recipient, attachments, quote, linkPreview)
2021-06-28 02:00:18 +02:00
// Clear the input bar
binding.inputBar.text = ""
binding.inputBar.cancelQuoteDraft()
binding.inputBar.cancelLinkPreviewDraft()
2021-06-28 02:00:18 +02:00
// Clear mentions
previousText = ""
currentMentionStartIndex = -1
mentions.clear()
// Reset the attachment manager
2021-06-28 02:44:00 +02:00
attachmentManager.clear()
// Reset attachments button if needed
if (isShowingAttachmentOptions) { toggleAttachmentOptions() }
2021-06-28 02:00:18 +02:00
// Put the message in the database
message.id = mmsDb.insertMessageOutbox(outgoingTextMessage, viewModel.threadId, false) { }
2021-06-28 02:00:18 +02:00
// Send it
MessageSender.send(message, viewModel.recipient.address, attachments, quote, linkPreview)
2021-06-28 02:00:18 +02:00
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
2021-06-28 02:00:18 +02:00
}
2021-06-25 07:53:47 +02:00
private fun showGIFPicker() {
val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning()
2021-09-02 02:19:43 +02:00
if (!hasSeenGIFMetaDataWarning) {
val builder = AlertDialog.Builder(this)
builder.setTitle("Search GIFs?")
builder.setMessage("You will not have full metadata protection when sending GIFs.")
builder.setPositiveButton("OK") { dialog: DialogInterface, _: Int ->
textSecurePreferences.setHasSeenGIFMetaDataWarning()
AttachmentManager.selectGif(this, PICK_GIF)
2021-09-02 02:19:43 +02:00
dialog.dismiss()
}
builder.setNegativeButton(
"Cancel"
) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
2021-09-02 02:19:43 +02:00
builder.create().show()
} else {
AttachmentManager.selectGif(this, PICK_GIF)
2021-09-02 02:19:43 +02:00
}
2021-06-25 07:53:47 +02:00
}
private fun showDocumentPicker() {
AttachmentManager.selectDocument(this, PICK_DOCUMENT)
2021-06-25 07:53:47 +02:00
}
private fun pickFromLibrary() {
AttachmentManager.selectGallery(this, PICK_FROM_LIBRARY, viewModel.recipient, binding.inputBar.text.trim())
2021-06-25 07:53:47 +02:00
}
private fun showCamera() {
attachmentManager.capturePhoto(this, TAKE_PHOTO, viewModel.recipient);
2021-06-25 07:53:47 +02:00
}
override fun onAttachmentChanged() {
2021-06-28 03:11:29 +02:00
// Do nothing
2021-06-25 07:53:47 +02:00
}
2021-06-25 08:09:37 +02:00
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
2021-06-25 08:09:37 +02:00
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
2021-06-28 02:44:00 +02:00
val mediaPreppedListener = object : ListenableFuture.Listener<Boolean> {
override fun onSuccess(result: Boolean?) {
sendAttachments(attachmentManager.buildSlideDeck().asAttachments(), null)
}
override fun onFailure(e: ExecutionException?) {
Toast.makeText(this@ConversationActivityV2, R.string.activity_conversation_attachment_prep_failed, Toast.LENGTH_LONG).show()
}
}
2021-06-25 08:09:37 +02:00
when (requestCode) {
PICK_DOCUMENT -> {
2021-06-28 02:50:35 +02:00
val uri = intent?.data ?: return
2021-06-28 02:44:00 +02:00
prepMediaForSending(uri, AttachmentManager.MediaType.DOCUMENT).addListener(mediaPreppedListener)
2021-06-25 08:09:37 +02:00
}
PICK_GIF -> {
2021-06-28 02:50:35 +02:00
intent ?: return
2021-06-28 02:00:18 +02:00
val uri = intent.data ?: return
2021-06-25 08:09:37 +02:00
val type = AttachmentManager.MediaType.GIF
val width = intent.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0)
val height = intent.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0)
2021-06-28 02:44:00 +02:00
prepMediaForSending(uri, type, width, height).addListener(mediaPreppedListener)
2021-06-25 08:09:37 +02:00
}
PICK_FROM_LIBRARY,
TAKE_PHOTO -> {
2021-06-28 02:50:35 +02:00
intent ?: return
2021-06-28 02:00:18 +02:00
val body = intent.getStringExtra(MediaSendActivity.EXTRA_MESSAGE)
2021-06-25 08:09:37 +02:00
val media = intent.getParcelableArrayListExtra<Media>(MediaSendActivity.EXTRA_MEDIA) ?: return
val slideDeck = SlideDeck()
for (item in media) {
when {
MediaUtil.isVideoType(item.mimeType) -> {
slideDeck.addSlide(VideoSlide(this, item.uri, 0, item.caption.orNull()))
}
MediaUtil.isGif(item.mimeType) -> {
slideDeck.addSlide(GifSlide(this, item.uri, 0, item.width, item.height, item.caption.orNull()))
}
MediaUtil.isImageType(item.mimeType) -> {
slideDeck.addSlide(ImageSlide(this, item.uri, 0, item.width, item.height, item.caption.orNull()))
}
else -> {
Log.d("Loki", "Asked to send an unexpected media type: '" + item.mimeType + "'. Skipping.")
}
}
}
2021-06-28 02:00:18 +02:00
sendAttachments(slideDeck.asAttachments(), body)
2021-06-25 08:09:37 +02:00
}
2021-06-29 03:14:58 +02:00
INVITE_CONTACTS -> {
if (!viewModel.recipient.isOpenGroupRecipient) { return }
2021-06-29 03:14:58 +02:00
val extras = intent?.extras ?: return
if (!intent.hasExtra(selectedContactsKey)) { return }
2021-06-29 03:14:58 +02:00
val selectedContacts = extras.getStringArray(selectedContactsKey)!!
val recipients = selectedContacts.map { contact ->
Recipient.from(this, fromSerialized(contact), true)
2021-06-29 03:14:58 +02:00
}
viewModel.inviteContacts(recipients)
2021-06-29 03:14:58 +02:00
}
2021-06-25 08:09:37 +02:00
}
}
2021-06-28 02:00:18 +02:00
2021-06-28 02:44:00 +02:00
private fun prepMediaForSending(uri: Uri, type: AttachmentManager.MediaType): ListenableFuture<Boolean> {
return prepMediaForSending(uri, type, null, null)
2021-06-28 02:00:18 +02:00
}
2021-06-28 02:44:00 +02:00
private fun prepMediaForSending(uri: Uri, type: AttachmentManager.MediaType, width: Int?, height: Int?): ListenableFuture<Boolean> {
return attachmentManager.setMedia(glide, uri, type, MediaConstraints.getPushMediaConstraints(), width ?: 0, height ?: 0)
2021-06-28 02:00:18 +02:00
}
2021-06-28 03:11:29 +02:00
override fun startRecordingVoiceMessage() {
if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) {
showVoiceMessageUI()
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
audioRecorder.startRecording()
stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 60000) // Limit voice messages to 1 minute each
} else {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
.withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_baseline_mic_48)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages))
.execute()
}
2021-06-28 03:11:29 +02:00
}
override fun sendVoiceMessage() {
hideVoiceMessageUI()
2021-06-28 03:11:29 +02:00
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
val future = audioRecorder.stopRecording()
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)
future.addListener(object : ListenableFuture.Listener<Pair<Uri, Long>> {
2021-06-28 03:11:29 +02:00
override fun onSuccess(result: Pair<Uri, Long>) {
val audioSlide = AudioSlide(this@ConversationActivityV2, result.first, result.second, MediaTypes.AUDIO_AAC, true)
2021-06-28 03:11:29 +02:00
val slideDeck = SlideDeck()
slideDeck.addSlide(audioSlide)
sendAttachments(slideDeck.asAttachments(), null)
}
override fun onFailure(e: ExecutionException) {
Toast.makeText(this@ConversationActivityV2, R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show()
}
})
}
override fun cancelVoiceMessage() {
hideVoiceMessageUI()
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
audioRecorder.stopRecording()
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)
}
2021-08-16 07:09:12 +02:00
// Remove this after the unsend request is enabled
fun deleteMessagesWithoutUnsendRequest(messages: Set<MessageRecord>) {
2021-06-29 02:05:39 +02:00
val messageCount = messages.size
val builder = AlertDialog.Builder(this)
builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
builder.setCancelable(true)
builder.setPositiveButton(R.string.delete) { _, _ ->
viewModel.deleteMessagesWithoutUnsendRequest(messages)
2021-06-29 02:05:39 +02:00
endActionMode()
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
endActionMode()
}
builder.show()
}
2021-08-11 07:17:53 +02:00
override fun deleteMessages(messages: Set<MessageRecord>) {
2021-08-17 08:16:17 +02:00
if (!IS_UNSEND_REQUESTS_ENABLED) {
2021-08-16 07:09:12 +02:00
deleteMessagesWithoutUnsendRequest(messages)
return
}
2021-08-16 04:08:35 +02:00
val allSentByCurrentUser = messages.all { it.isOutgoing }
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id) != null }
if (viewModel.recipient.isOpenGroupRecipient) {
2021-08-13 07:30:26 +02:00
val messageCount = messages.size
val builder = AlertDialog.Builder(this)
builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
builder.setCancelable(true)
builder.setPositiveButton(R.string.delete) { _, _ ->
for (message in messages) {
viewModel.deleteForEveryone(message)
2021-08-13 07:30:26 +02:00
}
endActionMode()
2021-06-29 02:05:39 +02:00
}
2021-08-13 07:30:26 +02:00
builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
endActionMode()
}
builder.show()
} else if (allSentByCurrentUser && allHasHash) {
2021-08-13 07:30:26 +02:00
val bottomSheet = DeleteOptionsBottomSheet()
bottomSheet.recipient = viewModel.recipient
2021-08-13 07:30:26 +02:00
bottomSheet.onDeleteForMeTapped = {
for (message in messages) {
viewModel.deleteLocally(message)
2021-08-13 07:30:26 +02:00
}
bottomSheet.dismiss()
2021-08-13 07:49:05 +02:00
endActionMode()
2021-08-13 07:30:26 +02:00
}
bottomSheet.onDeleteForEveryoneTapped = {
for (message in messages) {
viewModel.deleteForEveryone(message)
2021-08-13 07:30:26 +02:00
}
bottomSheet.dismiss()
2021-08-13 07:49:05 +02:00
endActionMode()
}
bottomSheet.onCancelTapped = {
bottomSheet.dismiss()
endActionMode()
2021-08-13 07:30:26 +02:00
}
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
2021-08-16 04:08:35 +02:00
} else {
val messageCount = messages.size
val builder = AlertDialog.Builder(this)
builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
builder.setCancelable(true)
builder.setPositiveButton(R.string.delete) { _, _ ->
for (message in messages) {
viewModel.deleteLocally(message)
2021-08-16 04:08:35 +02:00
}
endActionMode()
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
endActionMode()
}
builder.show()
2021-06-29 02:05:39 +02:00
}
}
override fun banUser(messages: Set<MessageRecord>) {
2021-06-29 02:05:39 +02:00
val builder = AlertDialog.Builder(this)
builder.setTitle(R.string.ConversationFragment_ban_selected_user)
builder.setMessage("This will ban the selected user from this room. It won't ban them from other rooms.")
2021-06-29 02:05:39 +02:00
builder.setCancelable(true)
builder.setPositiveButton(R.string.ban) { _, _ ->
viewModel.banUser(messages.first().individualRecipient)
2021-06-29 02:05:39 +02:00
endActionMode()
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
endActionMode()
}
builder.show()
}
override fun banAndDeleteAll(messages: Set<MessageRecord>) {
val builder = AlertDialog.Builder(this)
builder.setTitle(R.string.ConversationFragment_ban_selected_user)
builder.setMessage("This will ban the selected user from this room and delete all messages sent by them. It won't ban them from other rooms or delete the messages they sent there.")
builder.setCancelable(true)
builder.setPositiveButton(R.string.ban) { _, _ ->
viewModel.banAndDeleteAll(messages.first().individualRecipient)
endActionMode()
2021-06-29 02:05:39 +02:00
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
endActionMode()
}
builder.show()
}
2021-06-29 02:05:39 +02:00
override fun copyMessages(messages: Set<MessageRecord>) {
val sortedMessages = messages.sortedBy { it.dateSent }
val messageSize = sortedMessages.size
2021-06-29 02:05:39 +02:00
val builder = StringBuilder()
val messageIterator = sortedMessages.iterator()
while (messageIterator.hasNext()) {
val message = messageIterator.next()
val body = MentionUtilities.highlightMentions(message.body, viewModel.threadId, this)
2021-06-29 02:05:39 +02:00
if (TextUtils.isEmpty(body)) { continue }
if (messageSize > 1) {
val formattedTimestamp = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), message.timestamp)
builder.append("$formattedTimestamp: ")
}
builder.append(body)
if (messageIterator.hasNext()) {
builder.append('\n')
}
2021-06-29 02:05:39 +02:00
}
if (builder.isNotEmpty() && builder[builder.length - 1] == '\n') {
builder.deleteCharAt(builder.length - 1)
}
val result = builder.toString()
if (TextUtils.isEmpty(result)) { return }
val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(ClipData.newPlainText("Message Content", result))
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
endActionMode()
}
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()
2021-06-29 02:05:39 +02:00
endActionMode()
}
override fun resendMessage(messages: Set<MessageRecord>) {
messages.forEach { messageRecord ->
2021-07-14 05:52:10 +02:00
ResendMessageUtilities.resend(messageRecord)
}
endActionMode()
}
2021-07-13 06:42:16 +02:00
override fun showMessageDetail(messages: Set<MessageRecord>) {
2021-07-13 08:22:10 +02:00
val message = messages.first()
val intent = Intent(this, MessageDetailActivity::class.java)
intent.putExtra(MessageDetailActivity.MESSAGE_TIMESTAMP, message.timestamp)
push(intent)
endActionMode()
2021-07-13 06:42:16 +02:00
}
override fun saveAttachment(messages: Set<MessageRecord>) {
val message = messages.first() as MmsMessageRecord
2021-06-29 02:05:39 +02:00
SaveAttachmentTask.showWarningDialog(this, { _, _ ->
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))
2021-06-29 02:05:39 +02:00
.onAnyDenied {
endActionMode()
Toast.makeText(this@ConversationActivityV2, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show()
}
.onAllGranted {
2021-06-29 02:05:39 +02:00
endActionMode()
val attachments: List<SaveAttachmentTask.Attachment?> = 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<MessageRecord>) {
binding.inputBar.draftQuote(viewModel.recipient, messages.first(), glide)
2021-06-29 02:05:39 +02:00
endActionMode()
}
private fun sendMediaSavedNotification() {
if (viewModel.recipient.isGroupRecipient) { return }
val timestamp = System.currentTimeMillis()
val kind = DataExtractionNotification.Kind.MediaSaved(timestamp)
val message = DataExtractionNotification(kind)
MessageSender.send(message, viewModel.recipient.address)
}
2021-06-29 02:05:39 +02:00
private fun endActionMode() {
actionMode?.finish()
actionMode = null
}
// endregion
2021-06-22 08:23:47 +02:00
// region General
2021-06-25 07:20:54 +02:00
private fun getMessageBody(): String {
var result = binding.inputBar.text.trim()
2021-06-25 06:42:04 +02:00
for (mention in mentions) {
try {
val startIndex = result.indexOf("@" + mention.displayName)
val endIndex = startIndex + mention.displayName.count() + 1 // + 1 to include the "@"
result = result.substring(0, startIndex) + "@" + mention.publicKey + result.substring(endIndex)
} catch (exception: Exception) {
Log.d("Loki", "Failed to process mention due to error: $exception")
}
}
return result
2021-06-22 08:23:47 +02:00
}
// endregion
2021-06-29 03:49:10 +02:00
// region Search
2021-06-29 06:00:47 +02:00
private fun setUpSearchResultObserver() {
searchViewModel.searchResults.observe(this, Observer { result: SearchViewModel.SearchResult? ->
if (result == null) return@Observer
if (result.getResults().isNotEmpty()) {
result.getResults()[result.position]?.let {
jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs, Runnable { searchViewModel.onMissingResult() })
}
2021-06-29 06:00:47 +02:00
}
binding.searchBottomBar.setData(result.position, result.getResults().size)
2021-06-29 06:00:47 +02:00
})
}
fun onSearchOpened() {
searchViewModel.onSearchOpened()
binding.searchBottomBar.visibility = View.VISIBLE
binding.searchBottomBar.setData(0, 0)
binding.inputBar.visibility = View.GONE
}
fun onSearchClosed() {
searchViewModel.onSearchClosed()
binding.searchBottomBar.visibility = View.GONE
binding.inputBar.visibility = View.VISIBLE
adapter.onSearchQueryUpdated(null)
invalidateOptionsMenu()
}
fun onSearchQueryUpdated(query: String) {
searchViewModel.onQueryUpdated(query, viewModel.threadId)
binding.searchBottomBar.showLoading()
2021-06-29 03:49:10 +02:00
adapter.onSearchQueryUpdated(query)
}
2021-06-29 06:00:47 +02:00
override fun onSearchMoveUpPressed() {
this.searchViewModel.onMoveUp()
2021-06-29 06:00:47 +02:00
}
override fun onSearchMoveDownPressed() {
this.searchViewModel.onMoveDown()
2021-06-29 06:00:47 +02:00
}
private fun jumpToMessage(author: Address, timestamp: Long, onMessageNotFound: Runnable?) {
SimpleTask.run(lifecycle, {
mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, timestamp, author)
}) { p: Int -> moveToMessagePosition(p, onMessageNotFound) }
}
private fun moveToMessagePosition(position: Int, onMessageNotFound: Runnable?) {
if (position >= 0) {
binding.conversationRecyclerView.scrollToPosition(position)
} else {
onMessageNotFound?.run()
}
}
2021-06-29 03:49:10 +02:00
// endregion
}