2021-05-31 06:06:02 +02:00
package org.thoughtcrime.securesms.conversation.v2
2021-06-28 08:28:00 +02:00
import android.Manifest
2021-06-16 07:49:39 +02:00
import android.animation.FloatEvaluator
import android.animation.ValueAnimator
2022-01-14 06:56:15 +01:00
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
2021-06-04 01:58:04 +02:00
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
2022-01-14 06:56:15 +01:00
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
2021-06-24 02:04:43 +02:00
import android.util.TypedValue
2022-01-14 06:56:15 +01:00
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
2022-01-14 06:56:15 +01:00
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
2022-01-14 06:56:15 +01:00
import androidx.lifecycle.lifecycleScope
2021-06-04 01:58:04 +02:00
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
2021-05-31 06:06:02 +02:00
import androidx.recyclerview.widget.LinearLayoutManager
2021-06-25 02:55:50 +02:00
import androidx.recyclerview.widget.RecyclerView
2021-06-28 08:28:00 +02:00
import com.annimon.stream.Stream
2021-10-04 09:51:19 +02:00
import dagger.hilt.android.AndroidEntryPoint
2021-05-31 06:06:02 +02:00
import network.loki.messenger.R
2022-01-14 06:56:15 +01:00
import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding
import network.loki.messenger.databinding.ActivityConversationV2Binding
2021-06-24 03:43:51 +02:00
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
2021-06-28 08:28:00 +02:00
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
2021-06-24 03:43:51 +02:00
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
2021-07-19 05:52:50 +02:00
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
2021-07-19 05:52:50 +02:00
import org.session.libsignal.utilities.hexEncodedPrivateKey
2021-06-24 03:22:32 +02:00
import org.thoughtcrime.securesms.ApplicationContext
2021-05-31 06:06:02 +02:00
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
2021-06-28 03:11:29 +02:00
import org.thoughtcrime.securesms.audio.AudioRecorder
2021-07-12 07:44:46 +02:00
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
2021-06-25 03:11:03 +02:00
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
2022-01-14 06:56:15 +01:00
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
2021-06-17 08:07:11 +02:00
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
2021-06-07 06:04:55 +02:00
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
2021-06-28 08:28:00 +02:00
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate
2021-06-07 06:04:55 +02:00
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
2022-01-14 06:56:15 +01:00
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
2022-01-14 06:56:15 +01:00
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
2021-07-19 05:52:50 +02:00
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
2022-01-14 06:56:15 +01:00
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
2021-06-07 06:04:55 +02:00
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
2022-01-14 06:56:15 +01:00
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
2021-06-28 08:28:00 +02:00
import org.thoughtcrime.securesms.permissions.Permissions
2022-01-14 06:56:15 +01:00
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
2022-02-07 07:06:27 +01:00
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
2021-10-04 09:51:19 +02:00
import javax.inject.Inject
2022-01-14 06:56:15 +01:00
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
2021-05-31 06:06:02 +02:00
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.
2021-10-04 09:51:19 +02:00
@AndroidEntryPoint
2021-06-23 06:48:29 +02:00
class ConversationActivityV2 : PassphraseRequiredActionBarActivity ( ) , InputBarDelegate ,
2021-06-29 08:03:10 +02:00
InputBarRecordingViewDelegate , AttachmentManager . AttachmentListener , ActivityDispatcher ,
2021-06-30 03:48:54 +02:00
ConversationActionModeCallbackDelegate , VisibleMessageContentViewDelegate , RecipientModifiedListener ,
2021-07-08 01:25:43 +02:00
SearchBottomBar . EventListener , VoiceMessageViewDelegate {
2021-10-04 09:51:19 +02:00
2022-02-28 07:23:58 +01:00
private var binding : ActivityConversationV2Binding ? = null
private var actionBarBinding : ActivityConversationV2ActionBarBinding ? = null
2022-01-14 06:56:15 +01:00
@Inject lateinit var textSecurePreferences : TextSecurePreferences
2021-10-04 09:51:19 +02:00
@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
2022-01-14 06:56:15 +01:00
@Inject lateinit var viewModelFactory : ConversationViewModel . AssistedFactory
2021-10-04 09:51:19 +02:00
2021-06-18 07:54:24 +02:00
private val screenWidth = Resources . getSystem ( ) . displayMetrics . widthPixels
2022-01-14 06:56:15 +01:00
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 )
}
2021-06-07 06:04:55 +02:00
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 ( ) )
2021-06-28 03:26:13 +02:00
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
2022-01-14 06:56:15 +01:00
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
2021-06-30 03:02:46 +02:00
private val isScrolledToBottom : Boolean
get ( ) {
2022-02-28 07:23:58 +01:00
val position = layoutManager ?. findFirstCompletelyVisibleItemPosition ( ) ?: 0
2021-06-30 03:02:46 +02:00
return position == 0
}
2021-06-25 02:02:59 +02:00
2022-02-28 07:23:58 +01:00
private val layoutManager : LinearLayoutManager ?
get ( ) { return binding ?. conversationRecyclerView ?. layoutManager as LinearLayoutManager ? }
2021-05-31 06:06:02 +02:00
2021-07-19 05:52:50 +02:00
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 )
}
2021-06-04 01:58:04 +02:00
private val adapter by lazy {
2022-01-14 06:56:15 +01:00
val cursor = mmsSmsDb . getConversation ( viewModel . threadId )
2021-06-07 06:04:55 +02:00
val adapter = ConversationAdapter (
this ,
cursor ,
2021-06-30 06:29:32 +02:00
onItemPress = { message , position , view , event ->
handlePress ( message , position , view , event )
2021-06-07 06:04:55 +02:00
} ,
2021-06-09 03:37:50 +02:00
onItemSwipeToReply = { message , position ->
handleSwipeToReply ( message , position )
} ,
2021-06-07 06:04:55 +02:00
onItemLongPress = { message , position ->
handleLongPress ( message , position )
2021-06-21 07:26:09 +02:00
} ,
2022-02-06 02:41:35 +01:00
glide ,
onDeselect = { message , position ->
actionMode ?. let {
onDeselect ( message , position , it )
}
}
2021-06-07 06:04:55 +02:00
)
2021-06-30 02:30:10 +02:00
adapter . visibleMessageContentViewDelegate = this
2021-06-04 01:58:04 +02:00
adapter
}
2021-06-01 08:17:14 +02:00
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 ) }
2022-02-07 07:06:27 +01:00
private val messageToScrollTimestamp = AtomicLong ( - 1 )
private val messageToScrollAuthor = AtomicReference < Address ? > ( null )
2021-06-17 02:53:56 +02:00
2021-05-31 06:06:02 +02:00
// region Settings
companion object {
2021-07-01 01:54:09 +02:00
// Extras
2021-05-31 06:06:02 +02:00
const val THREAD _ID = " thread_id "
2021-07-01 01:31:30 +02:00
const val ADDRESS = " address "
2022-02-07 07:06:27 +01:00
const val SCROLL _MESSAGE _ID = " scroll_message_id "
const val SCROLL _MESSAGE _AUTHOR = " scroll_message_author "
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
2021-05-31 06:06:02 +02:00
}
// endregion
// region Lifecycle
override fun onCreate ( savedInstanceState : Bundle ? , isReady : Boolean ) {
super . onCreate ( savedInstanceState , isReady )
2022-01-14 06:56:15 +01:00
binding = ActivityConversationV2Binding . inflate ( layoutInflater )
2022-02-28 07:23:58 +01:00
setContentView ( binding !! . root )
2022-02-07 07:06:27 +01:00
// messageIdToScroll
messageToScrollTimestamp . set ( intent . getLongExtra ( SCROLL _MESSAGE _ID , - 1 ) )
messageToScrollAuthor . set ( intent . getParcelableExtra ( SCROLL _MESSAGE _AUTHOR ) )
2022-01-14 06:56:15 +01:00
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 ( )
}
2021-05-31 06:06:02 +02:00
setUpRecyclerView ( )
2021-06-15 06:55:57 +02:00
setUpToolBar ( )
2021-06-17 06:34:50 +02:00
setUpInputBar ( )
2021-07-30 02:05:07 +02:00
setUpLinkPreviewObserver ( )
2021-06-22 08:23:47 +02:00
restoreDraftIfNeeded ( )
2022-01-14 06:56:15 +01:00
setUpUiStateObserver ( )
2022-02-28 07:23:58 +01:00
binding !! . scrollToBottomButton . setOnClickListener {
val layoutManager = binding ?. conversationRecyclerView ?. layoutManager ?: return @setOnClickListener
2021-09-21 06:50:25 +02:00
if ( layoutManager . isSmoothScrolling ) {
2022-02-28 07:23:58 +01:00
binding ?. conversationRecyclerView ?. scrollToPosition ( 0 )
2021-09-21 06:50:25 +02:00
} else {
2022-02-28 07:23:58 +01:00
binding ?. conversationRecyclerView ?. smoothScrollToPosition ( 0 )
2021-09-21 06:50:25 +02:00
}
}
2022-01-14 06:56:15 +01:00
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 ( )
2021-06-24 03:43:51 +02:00
getLatestOpenGroupInfoIfNeeded ( )
2021-06-24 06:21:05 +02:00
setUpBlockedBanner ( )
2022-02-28 07:23:58 +01:00
binding !! . searchBottomBar . setEventListener ( this )
2021-06-29 06:00:47 +02:00
setUpSearchResultObserver ( )
2021-06-25 02:18:04 +02:00
scrollToFirstUnreadMessageIfNeeded ( )
2021-06-30 06:05:30 +02:00
showOrHideInputIfNeeded ( )
2022-01-14 06:56:15 +01:00
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 ( )
}
}
2021-05-31 06:06:02 +02:00
}
2021-06-23 03:54:17 +02:00
override fun onResume ( ) {
super . onResume ( )
2022-01-14 06:56:15 +01:00
ApplicationContext . getInstance ( this ) . messageNotifier . setVisibleThread ( viewModel . threadId )
2022-01-16 18:02:39 +01:00
threadDb . markAllAsRead ( viewModel . threadId , viewModel . recipient . isOpenGroupRecipient )
2021-06-23 03:54:17 +02:00
}
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 )
}
2021-06-28 01:59:33 +02:00
override fun dispatchIntent ( body : ( Context ) -> Intent ? ) {
val intent = body ( this ) ?: return
2021-06-25 08:30:23 +02:00
push ( intent , false )
}
2021-07-09 07:13:43 +02:00
override fun showDialog ( baseDialog : BaseDialog , tag : String ? ) {
baseDialog . show ( supportFragmentManager , tag )
}
2021-06-25 08:30:23 +02:00
2022-02-28 07:23:58 +01:00
// called from onCreate
2021-05-31 06:06:02 +02:00
private fun setUpRecyclerView ( ) {
2022-02-28 07:23:58 +01:00
binding !! . conversationRecyclerView . adapter = adapter
2021-06-07 08:36:05 +02:00
val layoutManager = LinearLayoutManager ( this , LinearLayoutManager . VERTICAL , true )
2022-02-28 07:23:58 +01:00
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)
2021-06-04 01:58:04 +02:00
LoaderManager . getInstance ( this ) . restartLoader ( 0 , null , object : LoaderManager . LoaderCallbacks < Cursor > {
override fun onCreateLoader ( id : Int , bundle : Bundle ? ) : Loader < Cursor > {
2022-01-14 06:56:15 +01:00
return ConversationLoader ( viewModel . threadId , this @ConversationActivityV2 )
2021-06-04 01:58:04 +02:00
}
override fun onLoadFinished ( loader : Loader < Cursor > , cursor : Cursor ? ) {
adapter . changeCursor ( cursor )
2022-02-07 07:06:27 +01:00
if ( cursor != null ) {
val messageTimestamp = messageToScrollTimestamp . getAndSet ( - 1 )
val author = messageToScrollAuthor . getAndSet ( null )
if ( author != null && messageTimestamp >= 0 ) {
jumpToMessage ( author , messageTimestamp , null )
}
}
2021-06-04 01:58:04 +02:00
}
override fun onLoaderReset ( cursor : Loader < Cursor > ) {
adapter . changeCursor ( null )
}
} )
2022-02-28 07:23:58 +01:00
binding !! . conversationRecyclerView . addOnScrollListener ( object : RecyclerView . OnScrollListener ( ) {
2021-06-25 02:55:50 +02:00
override fun onScrolled ( recyclerView : RecyclerView , dx : Int , dy : Int ) {
handleRecyclerViewScrolled ( )
}
} )
2021-05-31 06:06:02 +02:00
}
2021-06-01 08:17:14 +02:00
2022-02-28 07:23:58 +01:00
// called from onCreate
2021-06-15 06:55:57 +02:00
private fun setUpToolBar ( ) {
2021-06-04 06:55:53 +02:00
val actionBar = supportActionBar !!
2022-01-14 06:56:15 +01:00
actionBarBinding = ActivityConversationV2ActionBarBinding . inflate ( layoutInflater )
actionBar . title = " "
2022-02-28 07:23:58 +01:00
actionBar . customView = actionBarBinding !! . root
2021-06-04 06:55:53 +02:00
actionBar . setDisplayShowCustomEnabled ( true )
2022-02-28 07:23:58 +01:00
actionBarBinding !! . conversationTitleView . text = viewModel . recipient . toShortString ( )
2022-01-14 06:56:15 +01:00
@DimenRes val sizeID : Int = if ( viewModel . recipient . isClosedGroupRecipient ) {
R . dimen . medium _profile _picture _size
2021-07-01 02:02:02 +02:00
} else {
2022-01-14 06:56:15 +01:00
R . dimen . small _profile _picture _size
2021-07-01 02:02:02 +02:00
}
val size = resources . getDimension ( sizeID ) . roundToInt ( )
2022-02-28 07:23:58 +01:00
actionBarBinding !! . profilePictureView . layoutParams = LinearLayout . LayoutParams ( size , size )
actionBarBinding !! . profilePictureView . glide = glide
2022-01-14 06:56:15 +01:00
MentionManagerUtilities . populateUserPublicKeyCacheIfNeeded ( viewModel . threadId , this )
2022-02-28 07:23:58 +01:00
actionBarBinding !! . profilePictureView . update ( viewModel . recipient )
2021-06-01 08:17:14 +02:00
}
2021-06-07 01:48:01 +02:00
2022-02-28 07:23:58 +01:00
// called from onCreate
2021-06-17 06:34:50 +02:00
private fun setUpInputBar ( ) {
2022-02-28 07:23:58 +01:00
binding !! . inputBar . delegate = this
binding !! . inputBarRecordingView . delegate = this
2021-06-17 06:34:50 +02:00
// GIF button
2022-02-28 07:23:58 +01:00
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 ( ) }
2021-06-29 08:01:02 +02:00
gifButton . snIsEnabled = false
2021-06-17 06:34:50 +02:00
// Document button
2022-02-28 07:23:58 +01:00
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 ( ) }
2021-06-29 08:01:02 +02:00
documentButton . snIsEnabled = false
2021-06-17 06:34:50 +02:00
// Library button
2022-02-28 07:23:58 +01:00
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 ( ) }
2021-06-29 08:01:02 +02:00
libraryButton . snIsEnabled = false
2021-06-17 06:34:50 +02:00
// Camera button
2022-02-28 07:23:58 +01:00
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 ( ) }
2021-06-29 08:01:02 +02:00
cameraButton . snIsEnabled = false
2021-06-17 06:34:50 +02:00
}
2022-02-28 07:23:58 +01:00
// called from onCreate
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 ( ) )
2022-01-14 06:56:15 +01:00
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
}
2021-07-12 07:44:46 +02:00
} else if ( intent . hasExtra ( Intent . EXTRA _TEXT ) ) {
val dataTextExtra = intent . getCharSequenceExtra ( Intent . EXTRA _TEXT ) ?: " "
2022-02-28 07:23:58 +01:00
binding !! . inputBar . text = dataTextExtra . toString ( )
2021-07-12 07:44:46 +02:00
} else {
2022-01-14 06:56:15 +01:00
viewModel . getDraft ( ) ?. let { text ->
2022-02-28 07:23:58 +01:00
binding !! . inputBar . text = text
2022-01-14 06:56:15 +01:00
}
2021-07-01 01:54:09 +02:00
}
2021-06-22 08:23:47 +02:00
}
2022-01-14 06:56:15 +01:00
private fun addOpenGroupGuidelinesIfNeeded ( isOxenHostedOpenGroup : Boolean ) {
2021-06-23 05:11:21 +02:00
if ( !is OxenHostedOpenGroup ) { return }
2022-02-28 07:23:58 +01:00
binding ?. openGroupGuidelinesView ?. visibility = View . VISIBLE
val recyclerViewLayoutParams = binding ?. conversationRecyclerView ?. layoutParams as RelativeLayout . LayoutParams ? ?: return
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
2022-02-28 07:23:58 +01:00
binding ?. conversationRecyclerView ?. layoutParams = recyclerViewLayoutParams
2021-06-23 05:11:21 +02:00
}
2022-02-28 07:23:58 +01:00
// called from onCreate
2021-06-24 03:22:32 +02:00
private fun setUpTypingObserver ( ) {
2022-01-14 06:56:15 +01:00
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 ( )
2021-06-30 03:02:46 +02:00
// FIXME: Also checking isScrolledToBottom is a quick fix for an issue where the
// typing indicator overlays the recycler view when scrolled up
2022-02-28 07:23:58 +01:00
val viewContainer = binding ?. typingIndicatorViewContainer ?: return @observe
viewContainer . isVisible = recipients . isNotEmpty ( ) && isScrolledToBottom
viewContainer . setTypists ( recipients )
2021-06-24 03:22:32 +02:00
}
2022-01-14 06:56:15 +01:00
if ( textSecurePreferences . isTypingIndicatorsEnabled ( ) ) {
2022-02-28 07:23:58 +01:00
binding !! . inputBar . addTextChangedListener ( object : SimpleTextWatcher ( ) {
2021-06-25 03:11:03 +02:00
override fun onTextChanged ( text : String ? ) {
2022-01-14 06:56:15 +01:00
ApplicationContext . getInstance ( this @ConversationActivityV2 ) . typingStatusSender . onTypingStarted ( viewModel . threadId )
2021-06-25 03:11:03 +02:00
}
} )
}
2021-06-24 03:22:32 +02:00
}
2021-06-30 02:45:31 +02:00
private fun setUpRecipientObserver ( ) {
2022-01-14 06:56:15 +01:00
viewModel . recipient . addListener ( this )
2021-06-30 02:45:31 +02:00
}
2022-02-28 07:23:58 +01:00
private fun tearDownRecipientObserver ( ) {
viewModel . recipient . removeListener ( this )
}
2021-06-24 03:43:51 +02:00
private fun getLatestOpenGroupInfoIfNeeded ( ) {
2022-01-14 06:56:15 +01:00
val openGroup = lokiThreadDb . getOpenGroupChat ( viewModel . threadId ) ?: return
2021-06-24 03:43:51 +02:00
OpenGroupAPIV2 . getMemberCount ( openGroup . room , openGroup . server ) . successUi { updateSubtitle ( ) }
}
2022-02-28 07:23:58 +01:00
// called from onCreate
2021-06-24 06:21:05 +02:00
private fun setUpBlockedBanner ( ) {
2022-01-14 06:56:15 +01:00
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
2022-02-28 07:23:58 +01:00
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 ( ) {
2022-01-14 06:56:15 +01:00
if ( ! textSecurePreferences . isLinkPreviewsEnabled ( ) ) {
2021-06-24 07:46:36 +02:00
linkPreviewViewModel . onUserCancel ( ) ; return
}
2022-01-14 06:56:15 +01:00
linkPreviewViewModel . linkPreviewState . observe ( this ) { previewState : LinkPreviewState ? ->
2021-06-24 07:46:36 +02:00
if ( previewState == null ) return @observe
2022-01-14 06:56:15 +01:00
when {
previewState . isLoading -> {
2022-02-28 07:23:58 +01:00
binding ?. inputBar ?. draftLinkPreview ( )
2022-01-14 06:56:15 +01:00
}
previewState . linkPreview . isPresent -> {
2022-02-28 07:23:58 +01:00
binding ?. inputBar ?. updateLinkPreviewDraft ( glide , previewState . linkPreview . get ( ) )
2022-01-14 06:56:15 +01:00
}
else -> {
2022-02-28 07:23:58 +01:00
binding ?. inputBar ?. cancelLinkPreviewDraft ( )
2022-01-14 06:56:15 +01:00
}
2021-06-24 07:46:36 +02:00
}
2022-01-14 06:56:15 +01: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 ( ) {
2022-01-14 06:56:15 +01:00
val lastSeenTimestamp = threadDb . getLastSeenAndHasSent ( viewModel . threadId ) . first ( )
2021-06-25 01:38:26 +02:00
val lastSeenItemPosition = adapter . findLastSeenItemPosition ( lastSeenTimestamp ) ?: return
2021-06-25 02:18:04 +02:00
if ( lastSeenItemPosition <= 3 ) { return }
2022-02-28 07:23:58 +01:00
binding ?. conversationRecyclerView ?. scrollToPosition ( lastSeenItemPosition )
2021-06-25 01:38:26 +02:00
}
2021-06-07 01:48:01 +02:00
override fun onPrepareOptionsMenu ( menu : Menu ) : Boolean {
2022-01-14 06:56:15 +01:00
ConversationMenuHelper . onPrepareOptionsMenu ( menu , menuInflater , viewModel . recipient , viewModel . threadId , this ) { onOptionsItemSelected ( it ) }
2021-06-07 06:04:55 +02:00
super . onPrepareOptionsMenu ( menu )
return true
2021-06-07 01:48:01 +02:00
}
2021-06-22 08:23:47 +02:00
override fun onDestroy ( ) {
2022-02-28 07:23:58 +01:00
viewModel . saveDraft ( binding ?. inputBar ?. text ?. trim ( ) ?: " " )
tearDownRecipientObserver ( )
2021-06-22 08:23:47 +02:00
super . onDestroy ( )
2022-02-28 07:23:58 +01:00
binding = null
actionBarBinding = null
2021-06-22 08:23:47 +02:00
}
2021-06-01 08:17:14 +02:00
// endregion
2021-06-30 06:05:30 +02:00
// 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 {
2022-01-14 06:56:15 +01:00
if ( viewModel . recipient . isContactRecipient ) {
2022-02-28 07:23:58 +01:00
binding ?. blockedBanner ?. isVisible = viewModel . recipient . isBlocked
2021-06-30 05:15:39 +02:00
}
updateSubtitle ( )
2021-06-30 06:05:30 +02:00
showOrHideInputIfNeeded ( )
2022-02-28 07:23:58 +01:00
actionBarBinding ?. profilePictureView ?. update ( recipient )
2021-06-30 06:05:30 +02:00
}
}
private fun showOrHideInputIfNeeded ( ) {
2022-01-14 06:56:15 +01:00
if ( viewModel . recipient . isClosedGroupRecipient ) {
val group = groupDb . getGroup ( viewModel . recipient . address . toGroupString ( ) ) . orNull ( )
2021-06-30 06:05:30 +02:00
val isActive = ( group ?. isActive == true )
2022-02-28 07:23:58 +01:00
binding ?. inputBar ?. showInput = isActive
2021-06-30 06:05:30 +02:00
} else {
2022-02-28 07:23:58 +01:00
binding ?. inputBar ?. showInput = true
2021-06-30 02:45:31 +02:00
}
}
2021-06-18 03:00:52 +02:00
override fun inputBarEditTextContentChanged ( newContent : CharSequence ) {
2022-02-28 07:23:58 +01:00
val inputBarText = binding ?. inputBar ?. text ?: return // TODO check if we should be referencing newContent here instead
2022-01-14 06:56:15 +01:00
if ( textSecurePreferences . isLinkPreviewsEnabled ( ) ) {
2022-02-28 07:23:58 +01:00
linkPreviewViewModel . onTextChanged ( this , inputBarText , 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 ( )
2022-01-14 06:56:15 +01:00
&& ! textSecurePreferences . isLinkPreviewsEnabled ( ) && ! textSecurePreferences . hasSeenLinkPreviewSuggestionDialog ( ) ) {
2021-06-29 07:48:40 +02:00
LinkPreviewDialog {
setUpLinkPreviewObserver ( )
2022-01-14 06:56:15 +01:00
linkPreviewViewModel . onEnabled ( )
2022-02-28 07:23:58 +01:00
linkPreviewViewModel . onTextChanged ( this , inputBarText , 0 , 0 )
2021-06-29 07:48:40 +02:00
} . show ( supportFragmentManager , " Link Preview Dialog " )
2022-01-14 06:56:15 +01:00
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 )
}
2022-02-09 04:18:22 +01:00
} else {
currentMentionStartIndex = - 1
hideMentionCandidates ( )
2021-06-25 06:42:04 +02:00
}
previousText = text
}
private fun showOrUpdateMentionCandidatesIfNeeded ( query : String = " " ) {
2022-02-28 07:23:58 +01:00
val additionalContentContainer = binding ?. additionalContentContainer ?: return
2021-06-25 06:42:04 +02:00
if ( !is ShowingMentionCandidatesView ) {
2022-02-28 07:23:58 +01:00
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 ) }
2022-02-28 07:23:58 +01:00
additionalContentContainer . addView ( view )
2022-01-14 06:56:15 +01:00
val candidates = MentionsManager . getMentionCandidates ( query , viewModel . threadId , viewModel . recipient . isOpenGroupRecipient )
2021-06-25 06:42:04 +02:00
this . mentionCandidatesView = view
2022-01-14 06:56:15 +01:00
view . show ( candidates , viewModel . threadId )
2021-06-25 06:42:04 +02:00
} else {
2022-01-14 06:56:15 +01:00
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
2022-02-28 07:23:58 +01:00
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
2022-02-28 07:23:58 +01:00
val allButtonContainers = listOfNotNull (
binding ?. cameraButtonContainer ,
binding ?. libraryButtonContainer ,
binding ?. documentButtonContainer ,
binding ?. gifButtonContainer
)
2021-06-18 01:51:44 +02:00
val isReversed = isShowingAttachmentOptions // Run the animation in reverse
2021-06-29 08:01:02 +02:00
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 = !is ShowingAttachmentOptions
2021-06-29 08:01:02 +02:00
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 ( ) {
2022-02-28 07:23:58 +01:00
binding ?. inputBarRecordingView ?. show ( )
binding ?. inputBar ?. alpha = 0.0f
2021-06-17 08:07:11 +02:00
val animation = ValueAnimator . ofObject ( FloatEvaluator ( ) , 1.0f , 0.0f )
animation . duration = 250L
animation . addUpdateListener { animator ->
2022-02-28 07:23:58 +01:00
binding ?. inputBar ?. alpha = animator . animatedValue as Float
2021-06-17 08:07:11 +02:00
}
animation . start ( )
}
2021-06-18 07:54:24 +02:00
private fun expandVoiceMessageLockView ( ) {
2022-02-28 07:23:58 +01:00
val lockView = binding ?. inputBarRecordingView ?. lockView ?: return
val animation = ValueAnimator . ofObject ( FloatEvaluator ( ) , lockView . scaleX , 1.10f )
2021-06-18 07:54:24 +02:00
animation . duration = 250L
animation . addUpdateListener { animator ->
2022-02-28 07:23:58 +01:00
lockView . scaleX = animator . animatedValue as Float
lockView . scaleY = animator . animatedValue as Float
2021-06-18 07:54:24 +02:00
}
animation . start ( )
}
private fun collapseVoiceMessageLockView ( ) {
2022-02-28 07:23:58 +01:00
val lockView = binding ?. inputBarRecordingView ?. lockView ?: return
val animation = ValueAnimator . ofObject ( FloatEvaluator ( ) , lockView . scaleX , 1.0f )
2021-06-18 07:54:24 +02:00
animation . duration = 250L
animation . addUpdateListener { animator ->
2022-02-28 07:23:58 +01:00
lockView . scaleX = animator . animatedValue as Float
lockView . scaleY = animator . animatedValue as Float
2021-06-18 07:54:24 +02:00
}
animation . start ( )
}
private fun hideVoiceMessageUI ( ) {
2022-02-28 07:23:58 +01:00
val chevronImageView = binding ?. inputBarRecordingView ?. chevronImageView ?: return
val slideToCancelTextView = binding ?. inputBarRecordingView ?. slideToCancelTextView ?: return
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 ( )
}
2022-02-28 07:23:58 +01:00
binding ?. inputBarRecordingView ?. hide ( )
2021-06-18 07:54:24 +02:00
}
override fun handleVoiceMessageUIHidden ( ) {
2022-02-28 07:23:58 +01:00
val inputBar = binding ?. inputBar ?: return
inputBar . alpha = 1.0f
2021-06-17 08:07:11 +02:00
val animation = ValueAnimator . ofObject ( FloatEvaluator ( ) , 0.0f , 1.0f )
animation . duration = 250L
animation . addUpdateListener { animator ->
2022-02-28 07:23:58 +01:00
inputBar . alpha = animator . animatedValue as Float
2021-06-17 08:07:11 +02:00
}
animation . start ( )
2021-06-16 06:50:41 +02:00
}
2021-06-23 06:48:29 +02:00
2021-06-25 02:55:50 +02:00
private fun handleRecyclerViewScrolled ( ) {
2021-06-30 03:02:46 +02:00
// FIXME: Checking isScrolledToBottom is a quick fix for an issue where the
// typing indicator overlays the recycler view when scrolled up
2022-02-28 07:23:58 +01:00
val binding = binding ?: return
2022-01-14 06:56:15 +01:00
val wasTypingIndicatorVisibleBefore = binding . typingIndicatorViewContainer . isVisible
binding . typingIndicatorViewContainer . isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
2022-02-28 07:23:58 +01:00
binding . typingIndicatorViewContainer . isVisible
binding . scrollToBottomButton . isVisible = !is ScrolledToBottom && adapter . itemCount > 0
val firstVisiblePosition = layoutManager ?. findFirstVisibleItemPosition ( ) ?: - 1
unreadCount = min ( unreadCount , firstVisiblePosition ) . coerceAtLeast ( 0 )
2021-06-25 02:02:59 +02:00
updateUnreadCountIndicator ( )
2021-06-24 02:04:43 +02:00
}
2021-06-25 02:02:59 +02:00
private fun updateUnreadCountIndicator ( ) {
2022-02-28 07:23:58 +01:00
val binding = binding ?: return
2022-01-18 12:32:20 +01:00
val formattedUnreadCount = if ( unreadCount < 10000 ) unreadCount . toString ( ) else " 9999+ "
2022-01-14 06:56:15 +01:00
binding . unreadCountTextView . text = formattedUnreadCount
2022-01-18 12:32:20 +01:00
val textSize = if ( unreadCount < 10000 ) 12.0f else 9.0f
2022-01-14 06:56:15 +01:00
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 ( ) {
2022-02-28 07:23:58 +01:00
val actionBarBinding = actionBarBinding ?: return
2022-01-14 06:56:15 +01:00
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 ( ) ) )
2021-07-26 01:37:39 +02:00
} else {
2022-01-14 06:56:15 +01:00
actionBarBinding . conversationSubtitleView . text = getString ( R . string . ConversationActivity _muted _forever )
2021-07-26 01:37:39 +02:00
}
2022-01-14 06:56:15 +01:00
} else if ( viewModel . recipient . isGroupRecipient ) {
val openGroup = lokiThreadDb . getOpenGroupChat ( viewModel . threadId )
2021-06-24 03:38:06 +02:00
if ( openGroup != null ) {
2021-10-04 09:51:19 +02:00
val userCount = lokiApiDb . getUserCount ( openGroup . room , openGroup . server ) ?: 0
2022-01-14 06:56:15 +01:00
actionBarBinding . conversationSubtitleView . text = getString ( R . string . ConversationActivity _member _count , userCount )
2021-06-24 03:38:06 +02:00
} else {
2022-01-14 06:56:15 +01:00
actionBarBinding . conversationSubtitleView . isVisible = false
2021-06-24 03:38:06 +02:00
}
} else {
2022-01-14 06:56:15 +01:00
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
2021-06-01 08:17:14 +02:00
// region Interaction
2021-06-07 01:48:01 +02:00
override fun onOptionsItemSelected ( item : MenuItem ) : Boolean {
2021-06-29 08:05:40 +02:00
if ( item . itemId == android . R . id . home ) {
return false
}
2022-01-14 06:56:15 +01:00
return ConversationMenuHelper . onOptionItemSelected ( this , item , viewModel . recipient )
2021-06-01 08:17:14 +02:00
}
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
2021-06-30 06:29:32 +02:00
private fun handlePress ( message : MessageRecord , position : Int , view : VisibleMessageView , event : MotionEvent ) {
2021-06-07 06:04:55 +02:00
val actionMode = this . actionMode
if ( actionMode != null ) {
2022-02-06 02:41:35 +01:00
onDeselect ( message , position , actionMode )
2021-06-21 06:48:27 +02:00
} 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.
2021-06-30 06:29:32 +02:00
view . onContentClick ( event )
2021-06-07 06:04:55 +02:00
}
}
2022-02-06 02:41:35 +01:00
private fun onDeselect ( message : MessageRecord , position : Int , actionMode : ActionMode ) {
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
}
}
2021-06-09 03:37:50 +02:00
// `position` is the adapter position; not the visual position
private fun handleSwipeToReply ( message : MessageRecord , position : Int ) {
2022-02-28 07:23:58 +01:00
binding ?. inputBar ?. draftQuote ( viewModel . recipient , message , glide )
2021-06-09 03:37:50 +02:00
}
// `position` is the adapter position; not the visual position
2021-06-07 06:04:55 +02:00
private fun handleLongPress ( message : MessageRecord , position : Int ) {
val actionMode = this . actionMode
2022-01-14 06:56:15 +01:00
val actionModeCallback = ConversationActionModeCallback ( adapter , viewModel . threadId , this )
2021-06-28 08:28:00 +02:00
actionModeCallback . delegate = this
2021-06-30 03:44:26 +02:00
searchViewItem ?. collapseActionView ( )
2021-06-07 06:04:55 +02:00
if ( actionMode == null ) { // Nothing should be selected if this is the case
adapter . toggleSelection ( message , position )
2022-01-14 06:56:15 +01:00
this . actionMode = startActionMode ( actionModeCallback , ActionMode . TYPE _PRIMARY )
2021-06-04 07:10:58 +02:00
} else {
2021-06-07 06:04:55 +02:00
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
2022-02-28 07:23:58 +01:00
val chevronImageView = binding ?. inputBarRecordingView ?. chevronImageView ?: return
val slideToCancelTextView = binding ?. inputBarRecordingView ?. slideToCancelTextView ?: return
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 ( !is LockViewExpanded ) {
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 ) {
2021-06-28 03:26:13 +02:00
val x = event . rawX . roundToInt ( )
val y = event . rawY . roundToInt ( )
if ( isValidLockViewLocation ( x , y ) ) {
2022-02-28 07:23:58 +01:00
binding ?. inputBarRecordingView ?. lock ( )
2021-06-17 06:01:43 +02:00
} else {
2022-02-28 07:23:58 +01:00
val recordButtonOverlay = binding ?. inputBarRecordingView ?. recordButtonOverlay ?: return
2021-06-28 03:26:13 +02:00
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)
2022-02-28 07:23:58 +01:00
val binding = binding ?: return false
2021-06-18 07:54:24 +02:00
val lockViewLocation = IntArray ( 2 ) { 0 }
2022-01-14 06:56:15 +01:00
binding . inputBarRecordingView . lockView . getLocationOnScreen ( lockViewLocation )
2021-06-18 07:54:24 +02:00
val hitRect = Rect ( lockViewLocation [ 0 ] - lockViewHitMargin , 0 ,
2022-01-14 06:56:15 +01:00
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 ) {
2022-02-28 07:23:58 +01:00
val binding = binding ?: return
2021-06-25 07:11:38 +02:00
if ( currentMentionStartIndex == - 1 ) { return }
mentions . add ( mention )
2022-01-14 06:56:15 +01:00
val previousText = binding . inputBar . text
2021-06-25 07:11:38 +02:00
val newText = previousText . substring ( 0 , currentMentionStartIndex ) + " @ " + mention . displayName + " "
2022-01-14 06:56:15 +01:00
binding . inputBar . text = newText
binding . inputBar . setSelection ( newText . length )
2021-06-25 07:11:38 +02:00
currentMentionStartIndex = - 1
hideMentionCandidates ( )
this . previousText = newText
}
2021-06-30 02:30:10 +02:00
override fun scrollToMessageIfPossible ( timestamp : Long ) {
val lastSeenItemPosition = adapter . getItemPositionForTimestamp ( timestamp ) ?: return
2022-02-28 07:23:58 +01:00
binding ?. conversationRecyclerView ?. scrollToPosition ( lastSeenItemPosition )
2021-06-30 02:30:10 +02:00
}
2021-07-14 01:37:18 +02:00
override fun playVoiceMessageAtIndexIfPossible ( indexInAdapter : Int ) {
if ( indexInAdapter < 0 || indexInAdapter >= adapter . itemCount ) { return }
2022-02-28 07:23:58 +01:00
val viewHolder = binding ?. conversationRecyclerView ?. findViewHolderForAdapterPosition ( indexInAdapter ) as ? ConversationAdapter . VisibleMessageViewHolder ?: return
viewHolder . view . playVoiceMessage ( )
2021-07-08 01:25:43 +02:00
}
2021-06-28 02:00:18 +02:00
override fun sendMessage ( ) {
2022-01-14 06:56:15 +01:00
if ( viewModel . recipient . isContactRecipient && viewModel . recipient . isBlocked ) {
BlockedDialog ( viewModel . recipient ) . show ( supportFragmentManager , " Blocked Dialog " )
2021-06-28 05:36:15 +02:00
return
}
2022-02-28 07:23:58 +01:00
val binding = binding ?: return
2022-01-14 06:56:15 +01:00
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 ( )
}
}
2021-07-12 07:44:46 +02:00
override fun commitInputContent ( contentUri : Uri ) {
val media = Media ( contentUri , MediaUtil . getMimeType ( this , contentUri ) !! , 0 , 0 , 0 , 0 , Optional . absent ( ) , Optional . absent ( ) )
2022-01-14 06:56:15 +01:00
startActivityForResult ( MediaSendActivity . buildEditorIntent ( this , listOf ( media ) , viewModel . recipient , getMessageBody ( ) ) , PICK _FROM _LIBRARY )
2021-07-12 07:44:46 +02:00
}
2021-07-19 05:52:50 +02:00
private fun sendTextOnlyMessage ( hasPermissionToSendSeed : Boolean = false ) {
val text = getMessageBody ( )
2022-01-14 06:56:15 +01:00
val userPublicKey = textSecurePreferences . getLocalNumber ( )
val isNoteToSelf = ( viewModel . recipient . isContactRecipient && viewModel . recipient . address . toString ( ) == userPublicKey )
2021-07-19 05:52:50 +02:00
if ( text . contains ( seed ) && !is NoteToSelf && ! 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 ( )
2021-07-19 05:52:50 +02:00
message . text = text
2022-01-14 06:56:15 +01:00
val outgoingTextMessage = OutgoingTextMessage . from ( message , viewModel . recipient )
2021-06-25 07:24:34 +02:00
// Clear the input bar
2022-02-28 07:23:58 +01:00
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
2022-01-14 06:56:15 +01:00
message . id = smsDb . insertMessageOutbox ( viewModel . threadId , outgoingTextMessage , false , message . sentTimestamp !! ) { }
2021-06-25 07:24:34 +02:00
// Send it
2022-01-14 06:56:15 +01:00
MessageSender . send ( message , viewModel . recipient . address )
2021-06-25 07:24:34 +02:00
// Send a typing stopped message
2022-01-14 06:56:15 +01:00
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 ( )
2022-01-14 06:56:15 +01:00
val sender = if ( it . isOutgoing ) fromSerialized ( textSecurePreferences . getLocalNumber ( ) !! ) else it . individualRecipient . address
2021-07-05 07:48:46 +02:00
QuoteModel ( it . dateSent , sender , it . body , false , quotedAttachments )
2021-06-28 05:29:17 +02:00
}
2022-01-14 06:56:15 +01:00
val outgoingTextMessage = OutgoingMediaMessage . from ( message , viewModel . recipient , attachments , quote , linkPreview )
2021-06-28 02:00:18 +02:00
// Clear the input bar
2022-02-28 07:23:58 +01:00
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
2022-01-14 06:56:15 +01:00
message . id = mmsDb . insertMessageOutbox ( outgoingTextMessage , viewModel . threadId , false ) { }
2021-06-28 02:00:18 +02:00
// Send it
2022-01-14 06:56:15 +01:00
MessageSender . send ( message , viewModel . recipient . address , attachments , quote , linkPreview )
2021-06-28 02:00:18 +02:00
// Send a typing stopped message
2022-01-14 06:56:15 +01:00
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 ( ) {
2022-01-14 06:56:15 +01:00
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. " )
2022-01-14 06:56:15 +01:00
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 "
2022-01-14 06:56:15 +01:00
) { dialog : DialogInterface , _ : Int -> dialog . dismiss ( ) }
2021-09-02 02:19:43 +02:00
builder . create ( ) . show ( )
} else {
2022-01-14 06:56:15 +01:00
AttachmentManager . selectGif ( this , PICK _GIF )
2021-09-02 02:19:43 +02:00
}
2021-06-25 07:53:47 +02:00
}
private fun showDocumentPicker ( ) {
2022-01-14 06:56:15 +01:00
AttachmentManager . selectDocument ( this , PICK _DOCUMENT )
2021-06-25 07:53:47 +02:00
}
private fun pickFromLibrary ( ) {
2022-02-28 07:23:58 +01:00
binding ?. inputBar ?. text ?. trim ( ) ?. let { text ->
AttachmentManager . selectGallery ( this , PICK _FROM _LIBRARY , viewModel . recipient , text )
}
2021-06-25 07:53:47 +02:00
}
private fun showCamera ( ) {
2022-01-14 06:56:15 +01:00
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
2021-07-13 07:17:30 +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
}
2021-08-16 06:05:49 +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 -> {
2022-01-14 06:56:15 +01:00
if ( ! viewModel . recipient . isOpenGroupRecipient ) { return }
2021-06-29 03:14:58 +02:00
val extras = intent ?. extras ?: return
2022-01-14 06:56:15 +01:00
if ( !in tent . hasExtra ( selectedContactsKey ) ) { return }
2021-06-29 03:14:58 +02:00
val selectedContacts = extras . getStringArray ( selectedContactsKey ) !!
2022-01-14 06:56:15 +01:00
val recipients = selectedContacts . map { contact ->
Recipient . from ( this , fromSerialized ( contact ) , true )
2021-06-29 03:14:58 +02:00
}
2022-01-14 06:56:15 +01: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 ( ) {
2021-06-30 02:30:10 +02:00
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
}
2021-06-28 03:26:13 +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 )
2021-07-06 08:53:44 +02:00
future . addListener ( object : ListenableFuture . Listener < Pair < Uri , Long > > {
2021-06-28 03:11:29 +02:00
2021-07-06 08:53:44 +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 ( )
}
} )
}
2021-06-28 03:26:13 +02:00
override fun cancelVoiceMessage ( ) {
hideVoiceMessageUI ( )
window . clearFlags ( WindowManager . LayoutParams . FLAG _KEEP _SCREEN _ON )
audioRecorder . stopRecording ( )
stopAudioHandler . removeCallbacks ( stopVoiceMessageRecordingTask )
}
2021-06-28 08:28:00 +02:00
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 ) { _ , _ ->
2022-01-14 06:56:15 +01:00
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-06-28 08:28:00 +02:00
}
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 }
2021-10-04 09:51:19 +02:00
val allHasHash = messages . all { lokiMessageDb . getMessageServerHash ( it . id ) != null }
2022-01-14 06:56:15 +01:00
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 ) {
2022-01-14 06:56:15 +01:00
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 ( )
2021-08-17 07:11:53 +02:00
} else if ( allSentByCurrentUser && allHasHash ) {
2021-08-13 07:30:26 +02:00
val bottomSheet = DeleteOptionsBottomSheet ( )
2022-01-14 06:56:15 +01:00
bottomSheet . recipient = viewModel . recipient
2021-08-13 07:30:26 +02:00
bottomSheet . onDeleteForMeTapped = {
for ( message in messages ) {
2022-01-14 06:56:15 +01:00
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 ) {
2022-01-14 06:56:15 +01:00
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 ) {
2022-01-14 06:56:15 +01:00
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
}
2021-06-28 08:28:00 +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 )
2021-07-13 06:28:25 +02:00
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 ) { _ , _ ->
2022-01-14 06:56:15 +01:00
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 ( )
2021-06-28 08:28:00 +02:00
}
2021-07-13 06:28:25 +02:00
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 ) { _ , _ ->
2022-01-14 06:56:15 +01:00
viewModel . banAndDeleteAll ( messages . first ( ) . individualRecipient )
2021-07-13 06:28:25 +02:00
endActionMode ( )
2021-06-29 02:05:39 +02:00
}
builder . setNegativeButton ( android . R . string . cancel ) { dialog , _ ->
dialog . dismiss ( )
endActionMode ( )
}
builder . show ( )
2021-06-28 08:28:00 +02:00
}
2021-06-29 02:05:39 +02:00
override fun copyMessages ( messages : Set < MessageRecord > ) {
val sortedMessages = messages . sortedBy { it . dateSent }
2021-09-15 03:04:43 +02:00
val messageSize = sortedMessages . size
2021-06-29 02:05:39 +02:00
val builder = StringBuilder ( )
2021-09-15 03:04:43 +02:00
val messageIterator = sortedMessages . iterator ( )
while ( messageIterator . hasNext ( ) ) {
val message = messageIterator . next ( )
2022-01-14 06:56:15 +01:00
val body = MentionUtilities . highlightMentions ( message . body , viewModel . threadId , this )
2021-06-29 02:05:39 +02:00
if ( TextUtils . isEmpty ( body ) ) { continue }
2021-09-15 03:04:43 +02:00
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 ( )
2021-06-28 08:28:00 +02:00
}
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 ( )
2021-06-28 08:28:00 +02:00
}
override fun resendMessage ( messages : Set < MessageRecord > ) {
2022-02-07 07:06:27 +01:00
messages . iterator ( ) . forEach { messageRecord ->
2021-07-14 05:52:10 +02:00
ResendMessageUtilities . resend ( messageRecord )
2021-07-01 03:06:11 +02:00
}
endActionMode ( )
2021-06-28 08:28:00 +02:00
}
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
}
2021-06-28 08:28:00 +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 , { _ , _ ->
2021-06-28 08:28:00 +02:00
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 ( )
}
2021-06-28 08:28:00 +02:00
. onAllGranted {
2021-06-29 02:05:39 +02:00
endActionMode ( )
val attachments : List < SaveAttachmentTask . Attachment ? > = Stream . of ( message . slideDeck . slides )
2021-06-28 08:28:00 +02:00
. 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 > ) {
2022-02-28 07:23:58 +01:00
binding ?. inputBar ?. draftQuote ( viewModel . recipient , messages . first ( ) , glide )
2021-06-29 02:05:39 +02:00
endActionMode ( )
2021-06-28 08:28:00 +02:00
}
private fun sendMediaSavedNotification ( ) {
2022-01-14 06:56:15 +01:00
if ( viewModel . recipient . isGroupRecipient ) { return }
2021-06-28 08:28:00 +02:00
val timestamp = System . currentTimeMillis ( )
val kind = DataExtractionNotification . Kind . MediaSaved ( timestamp )
val message = DataExtractionNotification ( kind )
2022-01-14 06:56:15 +01:00
MessageSender . send ( message , viewModel . recipient . address )
2021-06-28 08:28:00 +02:00
}
2021-06-29 02:05:39 +02:00
private fun endActionMode ( ) {
actionMode ?. finish ( )
actionMode = null
}
2021-05-31 06:06:02 +02:00
// endregion
2021-06-22 08:23:47 +02:00
// region General
2021-06-25 07:20:54 +02:00
private fun getMessageBody ( ) : String {
2022-02-28 07:23:58 +01:00
var result = binding ?. inputBar ?. text ?. trim ( ) ?: return " "
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 " )
}
}
2022-01-14 06:56:15 +01:00
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 ( ) ) {
2021-06-29 07:35:53 +02:00
result . getResults ( ) [ result . position ] ?. let {
2022-02-28 07:23:58 +01:00
jumpToMessage ( it . messageRecipient . address , it . receivedTimestampMs ,
{ searchViewModel . onMissingResult ( ) } )
2021-06-29 07:35:53 +02:00
}
2021-06-29 06:00:47 +02:00
}
2022-02-28 07:23:58 +01:00
binding ?. searchBottomBar ?. setData ( result . position , result . getResults ( ) . size )
2021-06-29 06:00:47 +02:00
} )
}
2022-01-14 06:56:15 +01:00
fun onSearchOpened ( ) {
searchViewModel . onSearchOpened ( )
2022-02-28 07:23:58 +01:00
binding ?. searchBottomBar ?. visibility = View . VISIBLE
binding ?. searchBottomBar ?. setData ( 0 , 0 )
binding ?. inputBar ?. visibility = View . GONE
2022-01-14 06:56:15 +01:00
}
fun onSearchClosed ( ) {
searchViewModel . onSearchClosed ( )
2022-02-28 07:23:58 +01:00
binding ?. searchBottomBar ?. visibility = View . GONE
binding ?. inputBar ?. visibility = View . VISIBLE
2022-01-14 06:56:15 +01:00
adapter . onSearchQueryUpdated ( null )
invalidateOptionsMenu ( )
}
fun onSearchQueryUpdated ( query : String ) {
searchViewModel . onQueryUpdated ( query , viewModel . threadId )
2022-02-28 07:23:58 +01:00
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 ( ) {
2022-01-14 06:56:15 +01:00
this . searchViewModel . onMoveUp ( )
2021-06-29 06:00:47 +02:00
}
override fun onSearchMoveDownPressed ( ) {
2022-01-14 06:56:15 +01:00
this . searchViewModel . onMoveDown ( )
2021-06-29 06:00:47 +02:00
}
2021-06-29 07:35:53 +02:00
private fun jumpToMessage ( author : Address , timestamp : Long , onMessageNotFound : Runnable ? ) {
SimpleTask . run ( lifecycle , {
2022-01-14 06:56:15 +01:00
mmsSmsDb . getMessagePositionInConversation ( viewModel . threadId , timestamp , author )
2021-06-29 07:35:53 +02:00
} ) { p : Int -> moveToMessagePosition ( p , onMessageNotFound ) }
}
private fun moveToMessagePosition ( position : Int , onMessageNotFound : Runnable ? ) {
if ( position >= 0 ) {
2022-02-28 07:23:58 +01:00
binding ?. conversationRecyclerView ?. scrollToPosition ( position )
2021-06-29 07:35:53 +02:00
} else {
onMessageNotFound ?. run ( )
}
}
2021-06-29 03:49:10 +02:00
// endregion
2021-05-31 06:06:02 +02:00
}