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
2021-06-28 08:28:00 +02:00
import android.content.ClipData
import android.content.ClipboardManager
2021-07-01 03:06:11 +02:00
import android.content.Context
2021-06-25 08:30:23 +02:00
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
2021-06-28 03:11:29 +02:00
import android.os.*
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
2021-06-23 05:11:21 +02:00
import android.view.*
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
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-07-08 01:25:43 +02:00
import androidx.core.view.children
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
2021-06-24 07:46:36 +02:00
import androidx.lifecycle.ViewModelProviders
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-05-31 06:29:11 +02:00
import kotlinx.android.synthetic.main.activity_conversation_v2.*
2021-06-17 02:53:56 +02:00
import kotlinx.android.synthetic.main.activity_conversation_v2.view.*
2021-06-04 06:55:53 +02:00
import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.*
2021-06-23 05:11:21 +02:00
import kotlinx.android.synthetic.main.activity_home.*
2021-06-24 02:18:52 +02:00
import kotlinx.android.synthetic.main.view_conversation.view.*
2021-06-15 06:55:57 +02:00
import kotlinx.android.synthetic.main.view_input_bar.view.*
2021-06-17 05:18:09 +02:00
import kotlinx.android.synthetic.main.view_input_bar_recording.*
2021-06-17 02:53:56 +02:00
import kotlinx.android.synthetic.main.view_input_bar_recording.view.*
2021-07-08 01:25:43 +02:00
import kotlinx.android.synthetic.main.view_visible_message.view.*
2021-05-31 06:06:02 +02:00
import network.loki.messenger.R
2021-06-29 02:05:39 +02:00
import nl.komponents.kovenant.ui.failUi
2021-06-24 03:43:51 +02:00
import nl.komponents.kovenant.ui.successUi
2021-06-29 02:05:39 +02:00
import org.session.libsession.messaging.MessagingModuleConfiguration
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
2021-06-29 03:14:58 +02:00
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
2021-06-25 07:20:54 +02:00
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
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
2021-06-25 03:11:03 +02:00
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher
2021-06-24 06:05:55 +02:00
import org.thoughtcrime.securesms.conversation.v2.dialogs.*
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
2021-07-08 01:25:43 +02:00
import org.thoughtcrime.securesms.conversation.v2.messages.*
2021-06-29 06:00:47 +02:00
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel
2021-06-28 03:11:29 +02:00
import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager
2021-07-09 07:13:43 +02:00
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
2021-07-12 07:44:46 +02:00
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
2021-07-14 05:52:10 +02:00
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
2021-05-31 06:06:02 +02:00
import org.thoughtcrime.securesms.database.DatabaseFactory
2021-06-22 08:23:47 +02:00
import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.database.DraftDatabase.Drafts
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
import org.thoughtcrime.securesms.mms.*
2021-06-25 02:18:04 +02:00
import org.thoughtcrime.securesms.notifications.MarkReadReceiver
2021-06-28 08:28:00 +02:00
import org.thoughtcrime.securesms.permissions.Permissions
2021-07-09 05:18:48 +02:00
import org.thoughtcrime.securesms.util.*
2021-06-24 03:38:06 +02:00
import java.util.*
2021-06-28 02:44:00 +02:00
import java.util.concurrent.ExecutionException
2021-07-21 05:58:07 +02:00
import kotlin.collections.List
import kotlin.collections.Set
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.filter
import kotlin.collections.find
import kotlin.collections.first
import kotlin.collections.forEach
import kotlin.collections.indices
import kotlin.collections.isNotEmpty
import kotlin.collections.iterator
import kotlin.collections.listOf
import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf
import kotlin.collections.set
import kotlin.collections.sortedBy
import kotlin.collections.toTypedArray
2021-06-23 06:48:29 +02:00
import kotlin.math.*
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-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-06-18 07:54:24 +02:00
private val screenWidth = Resources . getSystem ( ) . displayMetrics . widthPixels
2021-06-24 07:46:36 +02:00
private var linkPreviewViewModel : LinkPreviewViewModel ? = null
2021-05-31 06:06:02 +02:00
private var threadID : Long = - 1
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
var searchViewModel : SearchViewModel ? = null
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 ( ) {
val position = layoutManager . findFirstCompletelyVisibleItemPosition ( )
return position == 0
}
2021-06-25 02:02:59 +02:00
private val layoutManager : LinearLayoutManager
get ( ) { return 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 {
val cursor = DatabaseFactory . getMmsSmsDatabase ( this ) . getConversation ( 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
} ,
glide
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 thread by lazy {
DatabaseFactory . getThreadDatabase ( this ) . getRecipientForThreadId ( threadID ) !!
}
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
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 "
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-05-31 06:06:02 +02:00
}
// endregion
// region Lifecycle
override fun onCreate ( savedInstanceState : Bundle ? , isReady : Boolean ) {
super . onCreate ( savedInstanceState , isReady )
setContentView ( R . layout . activity _conversation _v2 )
2021-07-01 01:31:30 +02:00
var threadID = intent . getLongExtra ( THREAD _ID , - 1L )
if ( threadID == - 1L ) {
val address = intent . getParcelableExtra < Address > ( ADDRESS ) ?: return finish ( )
val recipient = Recipient . from ( this , address , false )
threadID = DatabaseFactory . getThreadDatabase ( this ) . getOrCreateThreadIdFor ( recipient )
}
this . threadID = threadID
2021-07-09 01:38:45 +02:00
val thread = DatabaseFactory . getThreadDatabase ( this ) . getRecipientForThreadId ( threadID )
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-06-22 08:23:47 +02:00
restoreDraftIfNeeded ( )
2021-06-23 05:11:21 +02:00
addOpenGroupGuidelinesIfNeeded ( )
2021-06-23 08:08:30 +02:00
scrollToBottomButton . setOnClickListener { conversationRecyclerView . smoothScrollToPosition ( 0 ) }
2021-06-25 02:02:59 +02:00
unreadCount = DatabaseFactory . getMmsSmsDatabase ( this ) . getUnreadCount ( threadID )
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 ( )
2021-06-24 07:46:36 +02:00
setUpLinkPreviewObserver ( )
2021-06-29 06:00:47 +02:00
searchBottomBar . setEventListener ( this )
setUpSearchResultObserver ( )
2021-06-25 02:18:04 +02:00
scrollToFirstUnreadMessageIfNeeded ( )
2021-06-25 01:44:27 +02:00
markAllAsRead ( )
2021-06-30 06:05:30 +02:00
showOrHideInputIfNeeded ( )
2021-07-09 01:38:45 +02:00
if ( this . thread . isOpenGroupRecipient ) {
val openGroup = DatabaseFactory . getLokiThreadDatabase ( this ) . getOpenGroupChat ( threadID )
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 ( )
ApplicationContext . getInstance ( this ) . messageNotifier . setVisibleThread ( threadID )
}
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
2021-05-31 06:06:02 +02:00
private fun setUpRecyclerView ( ) {
2021-05-31 06:29:11 +02:00
conversationRecyclerView . adapter = adapter
2021-06-07 08:36:05 +02:00
val layoutManager = LinearLayoutManager ( this , LinearLayoutManager . VERTICAL , true )
2021-06-02 05:03:22 +02:00
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 > {
return ConversationLoader ( threadID , this @ConversationActivityV2 )
}
override fun onLoadFinished ( loader : Loader < Cursor > , cursor : Cursor ? ) {
adapter . changeCursor ( cursor )
}
override fun onLoaderReset ( cursor : Loader < Cursor > ) {
adapter . changeCursor ( null )
}
} )
2021-06-25 02:55:50 +02:00
conversationRecyclerView . addOnScrollListener ( object : RecyclerView . OnScrollListener ( ) {
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
2021-06-15 06:55:57 +02:00
private fun setUpToolBar ( ) {
2021-06-04 06:55:53 +02:00
val actionBar = supportActionBar !!
actionBar . setCustomView ( R . layout . activity _conversation _v2 _action _bar )
actionBar . setDisplayShowCustomEnabled ( true )
2021-06-01 08:17:14 +02:00
conversationTitleView . text = thread . toShortString ( )
2021-07-01 02:02:02 +02:00
@DimenRes val sizeID : Int
if ( thread . isClosedGroupRecipient ) {
sizeID = R . dimen . medium _profile _picture _size
} else {
sizeID = R . dimen . small _profile _picture _size
}
val size = resources . getDimension ( sizeID ) . roundToInt ( )
profilePictureView . layoutParams = LinearLayout . LayoutParams ( size , size )
2021-06-02 05:28:02 +02:00
profilePictureView . glide = glide
profilePictureView . update ( thread , threadID )
2021-06-01 08:17:14 +02:00
}
2021-06-07 01:48:01 +02:00
2021-06-17 06:34:50 +02:00
private fun setUpInputBar ( ) {
inputBar . delegate = this
2021-06-17 08:07:11 +02:00
inputBarRecordingView . delegate = this
2021-06-17 06:34:50 +02:00
// GIF button
gifButtonContainer . addView ( gifButton )
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
documentButtonContainer . addView ( documentButton )
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
libraryButtonContainer . addView ( libraryButton )
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
cameraButtonContainer . addView ( cameraButton )
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
}
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 ) , thread , " " ) , ConversationActivityV2 . PICK _FROM _LIBRARY )
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 ) ?: " "
inputBar . text = dataTextExtra . toString ( )
} else {
val draftDB = DatabaseFactory . getDraftDatabase ( this )
val drafts = draftDB . getDrafts ( threadID )
draftDB . clearDrafts ( threadID )
val text = drafts . find { it . type == DraftDatabase . Draft . TEXT } ?. value ?: return
inputBar . text = text
2021-07-01 01:54:09 +02:00
}
2021-06-22 08:23:47 +02:00
}
2021-06-23 05:11:21 +02:00
private fun addOpenGroupGuidelinesIfNeeded ( ) {
val openGroup = DatabaseFactory . getLokiThreadDatabase ( this ) . getOpenGroupChat ( threadID ) ?: return
val isOxenHostedOpenGroup = openGroup . room == " session " || openGroup . room == " oxen "
|| openGroup . room == " lokinet " || openGroup . room == " crypto "
if ( !is OxenHostedOpenGroup ) { return }
openGroupGuidelinesView . visibility = View . VISIBLE
val recyclerViewLayoutParams = 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
2021-06-23 05:11:21 +02:00
conversationRecyclerView . layoutParams = recyclerViewLayoutParams
}
2021-06-24 03:22:32 +02:00
private fun setUpTypingObserver ( ) {
ApplicationContext . getInstance ( this ) . typingStatusRepository . getTypists ( threadID ) . observe ( this ) { state ->
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
typingIndicatorViewContainer . isVisible = recipients . isNotEmpty ( ) && isScrolledToBottom
2021-06-24 03:22:32 +02:00
typingIndicatorViewContainer . setTypists ( recipients )
inputBarHeightChanged ( inputBar . height )
}
2021-06-25 03:11:03 +02:00
if ( TextSecurePreferences . isTypingIndicatorsEnabled ( this ) ) {
inputBar . inputBarEditText . addTextChangedListener ( object : SimpleTextWatcher ( ) {
override fun onTextChanged ( text : String ? ) {
ApplicationContext . getInstance ( this @ConversationActivityV2 ) . typingStatusSender . onTypingStarted ( threadID )
}
} )
}
2021-06-24 03:22:32 +02:00
}
2021-06-30 02:45:31 +02:00
private fun setUpRecipientObserver ( ) {
thread . addListener ( this )
}
2021-06-24 03:43:51 +02:00
private fun getLatestOpenGroupInfoIfNeeded ( ) {
val openGroup = DatabaseFactory . getLokiThreadDatabase ( this ) . getOpenGroupChat ( threadID ) ?: return
OpenGroupAPIV2 . getMemberCount ( openGroup . room , openGroup . server ) . successUi { updateSubtitle ( ) }
}
2021-06-24 06:21:05 +02:00
private fun setUpBlockedBanner ( ) {
if ( thread . isGroupRecipient ) { return }
val contactDB = DatabaseFactory . getSessionContactDatabase ( this )
val sessionID = thread . address . toString ( )
val contact = contactDB . getContactWithSessionID ( sessionID )
val name = contact ?. displayName ( Contact . ContactContext . REGULAR ) ?: sessionID
blockedBannerTextView . text = resources . getString ( R . string . activity _conversation _blocked _banner _text , name )
blockedBanner . isVisible = thread . isBlocked
blockedBanner . setOnClickListener { unblock ( ) }
}
2021-06-24 07:46:36 +02:00
private fun setUpLinkPreviewObserver ( ) {
val linkPreviewViewModel = ViewModelProviders . of ( this , LinkPreviewViewModel . Factory ( LinkPreviewRepository ( this ) ) ) [ LinkPreviewViewModel :: class . java ]
this . linkPreviewViewModel = linkPreviewViewModel
if ( ! TextSecurePreferences . isLinkPreviewsEnabled ( this ) ) {
linkPreviewViewModel . onUserCancel ( ) ; return
}
linkPreviewViewModel . linkPreviewState . observe ( this , { previewState : LinkPreviewState ? ->
if ( previewState == null ) return @observe
if ( previewState . isLoading ) {
2021-06-24 08:23:37 +02:00
inputBar . draftLinkPreview ( )
2021-06-25 01:19:21 +02:00
} else if ( previewState . linkPreview . isPresent ) {
2021-06-24 08:23:37 +02:00
inputBar . updateLinkPreviewDraft ( glide , previewState . linkPreview . get ( ) )
2021-06-25 01:19:21 +02:00
} else {
inputBar . cancelLinkPreviewDraft ( )
2021-06-24 07:46:36 +02:00
}
} )
}
2021-06-25 02:18:04 +02:00
private fun scrollToFirstUnreadMessageIfNeeded ( ) {
2021-06-25 01:38:26 +02:00
val lastSeenTimestamp = DatabaseFactory . getThreadDatabase ( this ) . getLastSeenAndHasSent ( threadID ) . first ( )
val lastSeenItemPosition = adapter . findLastSeenItemPosition ( lastSeenTimestamp ) ?: return
2021-06-25 02:18:04 +02:00
if ( lastSeenItemPosition <= 3 ) { return }
2021-06-25 01:38:26 +02:00
conversationRecyclerView . scrollToPosition ( lastSeenItemPosition )
}
2021-06-07 01:48:01 +02:00
override fun onPrepareOptionsMenu ( menu : Menu ) : Boolean {
2021-06-29 03:49:10 +02:00
ConversationMenuHelper . onPrepareOptionsMenu ( menu , menuInflater , thread , 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 ( ) {
saveDraft ( )
super . onDestroy ( )
}
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 {
if ( thread . isContactRecipient ) {
blockedBanner . isVisible = thread . isBlocked
}
updateSubtitle ( )
2021-06-30 06:05:30 +02:00
showOrHideInputIfNeeded ( )
}
}
private fun showOrHideInputIfNeeded ( ) {
if ( thread . isClosedGroupRecipient ) {
val group = DatabaseFactory . getGroupDatabase ( this ) . getGroup ( thread . address . toGroupString ( ) ) . orNull ( )
val isActive = ( group ?. isActive == true )
inputBar . showInput = isActive
} else {
inputBar . showInput = true
2021-06-30 02:45:31 +02:00
}
}
2021-06-25 01:44:27 +02:00
private fun markAllAsRead ( ) {
2021-06-25 02:18:04 +02:00
val messages = DatabaseFactory . getThreadDatabase ( this ) . setRead ( threadID , true )
if ( thread . isGroupRecipient ) {
for ( message in messages ) {
MarkReadReceiver . scheduleDeletion ( this , message . expirationInfo )
}
} else {
MarkReadReceiver . process ( this , messages )
}
2021-07-21 05:58:07 +02:00
ApplicationContext . getInstance ( this ) . messageNotifier . updateNotification ( this , threadID )
2021-06-25 01:44:27 +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 ( typingIndicatorViewContainer . isVisible ) toPx ( 36 , resources ) else 0
2021-06-18 03:00:52 +02:00
// Recycler view
2021-06-16 01:51:50 +02:00
val recyclerViewLayoutParams = conversationRecyclerView . layoutParams as RelativeLayout . LayoutParams
2021-06-25 07:11:38 +02:00
recyclerViewLayoutParams . bottomMargin = newValue + typingIndicatorHeight
2021-06-16 01:51:50 +02:00
conversationRecyclerView . layoutParams = recyclerViewLayoutParams
2021-06-18 07:11:41 +02:00
// Additional content container
val additionalContentContainerLayoutParams = additionalContentContainer . layoutParams as RelativeLayout . LayoutParams
additionalContentContainerLayoutParams . bottomMargin = newValue
additionalContentContainer . layoutParams = additionalContentContainerLayoutParams
2021-06-18 03:00:52 +02:00
// Attachment options
2021-06-18 02:16:15 +02:00
val attachmentButtonHeight = inputBar . attachmentsButtonContainer . height
2021-06-18 08:24:56 +02:00
val bottomMargin = ( newValue - inputBar . additionalContentHeight - attachmentButtonHeight ) / 2
2021-06-18 02:16:15 +02:00
val margin = toPx ( 8 , resources )
val attachmentOptionsContainerLayoutParams = attachmentOptionsContainer . layoutParams as RelativeLayout . LayoutParams
attachmentOptionsContainerLayoutParams . bottomMargin = bottomMargin + attachmentButtonHeight + margin
attachmentOptionsContainer . layoutParams = attachmentOptionsContainerLayoutParams
2021-06-23 07:14:19 +02:00
// Scroll to bottom button
val scrollToBottomButtonLayoutParams = scrollToBottomButton . layoutParams as RelativeLayout . LayoutParams
scrollToBottomButtonLayoutParams . bottomMargin = newValue + additionalContentContainer . height + toPx ( 12 , resources )
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 ) {
2021-06-29 07:48:40 +02:00
if ( TextSecurePreferences . isLinkPreviewsEnabled ( this ) ) {
linkPreviewViewModel ?. onTextChanged ( this , inputBar . text , 0 , 0 )
}
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 ( this ) && ! TextSecurePreferences . hasSeenLinkPreviewSuggestionDialog ( this ) ) {
LinkPreviewDialog {
setUpLinkPreviewObserver ( )
linkPreviewViewModel ?. onEnabled ( )
linkPreviewViewModel ?. onTextChanged ( this , inputBar . text , 0 , 0 )
} . show ( supportFragmentManager , " Link Preview Dialog " )
TextSecurePreferences . setHasSeenLinkPreviewSuggestionDialog ( this )
}
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 ( !is ShowingMentionCandidatesView ) {
additionalContentContainer . removeAllViews ( )
val view = MentionCandidatesView ( this )
view . glide = glide
2021-06-25 07:11:38 +02:00
view . onCandidateSelected = { handleMentionSelected ( it ) }
2021-06-25 06:42:04 +02:00
additionalContentContainer . addView ( view )
val candidates = MentionsManager . getMentionCandidates ( query , threadID , thread . isOpenGroupRecipient )
this . mentionCandidatesView = view
view . show ( candidates , threadID )
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 , threadID , thread . isOpenGroupRecipient )
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 ) { additionalContentContainer . removeAllViews ( ) }
}
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
2021-06-29 08:01:02 +02:00
val allButtonContainers = listOf ( cameraButtonContainer , libraryButtonContainer , documentButtonContainer , 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 ( ) {
2021-06-16 07:49:39 +02:00
inputBarRecordingView . show ( )
2021-06-17 08:07:11 +02:00
inputBar . alpha = 0.0f
val animation = ValueAnimator . ofObject ( FloatEvaluator ( ) , 1.0f , 0.0f )
animation . duration = 250L
animation . addUpdateListener { animator ->
inputBar . alpha = animator . animatedValue as Float
}
animation . start ( )
}
2021-06-18 07:54:24 +02:00
private fun expandVoiceMessageLockView ( ) {
val animation = ValueAnimator . ofObject ( FloatEvaluator ( ) , lockView . scaleX , 1.10f )
animation . duration = 250L
animation . addUpdateListener { animator ->
lockView . scaleX = animator . animatedValue as Float
lockView . scaleY = animator . animatedValue as Float
}
animation . start ( )
}
private fun collapseVoiceMessageLockView ( ) {
val animation = ValueAnimator . ofObject ( FloatEvaluator ( ) , lockView . scaleX , 1.0f )
animation . duration = 250L
animation . addUpdateListener { animator ->
lockView . scaleX = animator . animatedValue as Float
lockView . scaleY = animator . animatedValue as Float
}
animation . start ( )
}
private fun hideVoiceMessageUI ( ) {
val chevronImageView = inputBarRecordingView . inputBarChevronImageView
val slideToCancelTextView = inputBarRecordingView . inputBarSlideToCancelTextView
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 ( )
}
inputBarRecordingView . hide ( )
}
override fun handleVoiceMessageUIHidden ( ) {
2021-06-17 08:07:11 +02:00
inputBar . alpha = 1.0f
val animation = ValueAnimator . ofObject ( FloatEvaluator ( ) , 0.0f , 1.0f )
animation . duration = 250L
animation . addUpdateListener { animator ->
inputBar . alpha = animator . animatedValue as Float
}
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
val alpha = if ( !is ScrolledToBottom ) 1.0f else 0.0f
// FIXME: Checking isScrolledToBottom is a quick fix for an issue where the
// typing indicator overlays the recycler view when scrolled up
val wasTypingIndicatorVisibleBefore = typingIndicatorViewContainer . isVisible
typingIndicatorViewContainer . isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom
val isTypingIndicatorVisibleAfter = typingIndicatorViewContainer . isVisible
if ( isTypingIndicatorVisibleAfter != wasTypingIndicatorVisibleBefore ) {
inputBarHeightChanged ( inputBar . height )
}
2021-06-23 07:14:19 +02:00
scrollToBottomButton . alpha = alpha
2021-06-25 02:55:50 +02:00
unreadCount = min ( unreadCount , layoutManager . findFirstVisibleItemPosition ( ) )
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 ( ) {
2021-06-24 02:04:43 +02:00
val formattedUnreadCount = if ( unreadCount < 100 ) unreadCount . toString ( ) else " 99+ "
unreadCountTextView . text = formattedUnreadCount
val textSize = if ( unreadCount < 100 ) 12.0f else 9.0f
unreadCountTextView . setTextSize ( TypedValue . COMPLEX _UNIT _DIP , textSize )
2021-06-24 02:18:52 +02:00
unreadCountTextView . setTypeface ( Typeface . DEFAULT , if ( unreadCount < 100 ) Typeface . BOLD else Typeface . NORMAL )
unreadCountIndicator . isVisible = ( unreadCount != 0 )
2021-06-23 06:48:29 +02:00
}
2021-06-24 03:38:06 +02:00
private fun updateSubtitle ( ) {
muteIconImageView . isVisible = thread . isMuted
conversationSubtitleView . isVisible = true
if ( thread . isMuted ) {
conversationSubtitleView . text = getString ( R . string . ConversationActivity _muted _until _date , DateUtils . getFormattedDateTime ( thread . mutedUntil , " EEE, MMM d, yyyy HH:mm " , Locale . getDefault ( ) ) )
} else if ( thread . isGroupRecipient ) {
val openGroup = DatabaseFactory . getLokiThreadDatabase ( this ) . getOpenGroupChat ( threadID )
if ( openGroup != null ) {
val userCount = DatabaseFactory . getLokiAPIDatabase ( this ) . getUserCount ( openGroup . room , openGroup . server ) ?: 0
conversationSubtitleView . text = getString ( R . string . ConversationActivity _member _count , userCount )
} else {
conversationSubtitleView . isVisible = false
}
} else {
conversationSubtitleView . isVisible = false
}
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
}
2021-06-29 02:39:00 +02:00
return ConversationMenuHelper . onOptionItemSelected ( this , item , thread )
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 ) {
adapter . toggleSelection ( message , position )
val actionModeCallback = ConversationActionModeCallback ( adapter , threadID , this )
2021-06-28 08:28:00 +02:00
actionModeCallback . delegate = this
2021-06-07 06:04:55 +02:00
actionModeCallback . updateActionModeMenu ( actionMode . menu )
if ( adapter . selectedItems . isEmpty ( ) ) {
actionMode . finish ( )
this . actionMode = null
}
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
}
}
2021-06-09 03:37:50 +02:00
// `position` is the adapter position; not the visual position
private fun handleSwipeToReply ( message : MessageRecord , position : Int ) {
2021-07-01 03:27:52 +02:00
inputBar . draftQuote ( thread , 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
2021-06-07 03:37:20 +02:00
val actionModeCallback = ConversationActionModeCallback ( adapter , 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 )
this . actionMode = if ( android . os . Build . VERSION . SDK _INT >= android . os . Build . VERSION_CODES . M ) {
startActionMode ( actionModeCallback , ActionMode . TYPE _PRIMARY )
} else {
startActionMode ( actionModeCallback )
}
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
val chevronImageView = inputBarRecordingView . inputBarChevronImageView
val slideToCancelTextView = inputBarRecordingView . inputBarSlideToCancelTextView
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 ) ) {
2021-06-17 06:01:43 +02:00
inputBarRecordingView . lock ( )
} else {
2021-06-28 03:26:13 +02:00
val recordButtonOverlay = 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 }
lockView . getLocationOnScreen ( lockViewLocation )
val hitRect = Rect ( lockViewLocation [ 0 ] - lockViewHitMargin , 0 ,
lockViewLocation [ 0 ] + lockView . width + lockViewHitMargin , lockViewLocation [ 1 ] + lockView . height )
return hitRect . contains ( x , y )
2021-06-17 02:53:56 +02:00
}
2021-06-24 06:21:05 +02:00
private fun unblock ( ) {
2021-06-28 08:28:00 +02:00
if ( ! thread . isContactRecipient ) { return }
DatabaseFactory . getRecipientDatabase ( this ) . setBlocked ( thread , false )
2021-06-24 06:21:05 +02:00
}
2021-06-25 06:42:04 +02:00
2021-06-25 07:11:38 +02:00
private fun handleMentionSelected ( mention : Mention ) {
if ( currentMentionStartIndex == - 1 ) { return }
mentions . add ( mention )
val previousText = inputBar . text
val newText = previousText . substring ( 0 , currentMentionStartIndex ) + " @ " + mention . displayName + " "
inputBar . text = newText
inputBar . inputBarEditText . setSelection ( newText . length )
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
conversationRecyclerView . scrollToPosition ( lastSeenItemPosition )
}
2021-07-14 01:37:18 +02:00
override fun playVoiceMessageAtIndexIfPossible ( indexInAdapter : Int ) {
if ( indexInAdapter < 0 || indexInAdapter >= adapter . itemCount ) { return }
val viewHolder = conversationRecyclerView . findViewHolderForAdapterPosition ( indexInAdapter ) as ? ConversationAdapter . VisibleMessageViewHolder
val nextVisibleMessageView = viewHolder ?. view ?: return
nextVisibleMessageView . messageContentView . mainContainer . children . forEach { view ->
if ( view is VoiceMessageView ) {
return @forEach view . togglePlayback ( )
2021-07-08 01:25:43 +02:00
}
}
}
2021-06-28 02:00:18 +02:00
override fun sendMessage ( ) {
2021-06-28 05:36:15 +02:00
if ( thread . isContactRecipient && thread . isBlocked ) {
BlockedDialog ( thread ) . show ( supportFragmentManager , " Blocked Dialog " )
return
}
2021-06-28 05:29:17 +02:00
if ( inputBar . linkPreview != null || inputBar . quote != null ) {
sendAttachments ( listOf ( ) , getMessageBody ( ) , inputBar . quote , inputBar . linkPreview )
} 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 ( ) )
startActivityForResult ( MediaSendActivity . buildEditorIntent ( this , listOf ( media ) , thread , getMessageBody ( ) ) , ConversationActivityV2 . PICK _FROM _LIBRARY )
}
2021-07-19 05:52:50 +02:00
private fun sendTextOnlyMessage ( hasPermissionToSendSeed : Boolean = false ) {
val text = getMessageBody ( )
val userPublicKey = TextSecurePreferences . getLocalNumber ( this )
val isNoteToSelf = ( thread . isContactRecipient && thread . address . toString ( ) == userPublicKey )
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
2021-06-25 07:20:54 +02:00
val outgoingTextMessage = OutgoingTextMessage . from ( message , thread )
2021-06-25 07:24:34 +02:00
// Clear the input bar
inputBar . text = " "
2021-06-29 02:05:39 +02:00
inputBar . cancelQuoteDraft ( )
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 = DatabaseFactory . getSmsDatabase ( this ) . insertMessageOutbox ( threadID , outgoingTextMessage , false , message . sentTimestamp !! ) { }
// Send it
2021-06-25 07:20:54 +02:00
MessageSender . send ( message , thread . address )
2021-06-25 07:24:34 +02:00
// Send a typing stopped message
ApplicationContext . getInstance ( this ) . typingStatusSender . onTypingStopped ( 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 ( )
2021-07-05 07:48:46 +02:00
val sender = if ( it . isOutgoing ) fromSerialized ( TextSecurePreferences . getLocalNumber ( this ) !! ) 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 , thread , attachments , quote , linkPreview )
2021-06-28 02:00:18 +02:00
// Clear the input bar
inputBar . text = " "
2021-06-29 02:05:39 +02:00
inputBar . cancelQuoteDraft ( )
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 = DatabaseFactory . getMmsDatabase ( this ) . insertMessageOutbox ( outgoingTextMessage , threadID , false ) { }
// Send it
2021-06-28 05:29:17 +02:00
MessageSender . send ( message , thread . address , attachments , quote , linkPreview )
2021-06-28 02:00:18 +02:00
// Send a typing stopped message
ApplicationContext . getInstance ( this ) . typingStatusSender . onTypingStopped ( threadID )
}
2021-06-25 07:53:47 +02:00
private fun showGIFPicker ( ) {
AttachmentManager . selectGif ( this , ConversationActivityV2 . PICK _GIF )
}
private fun showDocumentPicker ( ) {
AttachmentManager . selectDocument ( this , ConversationActivityV2 . PICK _DOCUMENT )
}
private fun pickFromLibrary ( ) {
AttachmentManager . selectGallery ( this , ConversationActivityV2 . PICK _FROM _LIBRARY , thread , inputBar . text . trim ( ) )
}
private fun showCamera ( ) {
attachmentManager . capturePhoto ( this , ConversationActivityV2 . TAKE _PHOTO )
}
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
}
TAKE _PHOTO -> {
2021-06-29 07:17:14 +02:00
if ( resultCode != RESULT _OK ) { return }
2021-06-25 08:09:37 +02:00
val uri = attachmentManager . captureUri ?: return
2021-06-28 02:44:00 +02:00
prepMediaForSending ( uri , AttachmentManager . MediaType . IMAGE ) . 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 -> {
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 ( ! thread . isOpenGroupRecipient ) { return }
val extras = intent ?. extras ?: return
if ( !in tent . hasExtra ( SelectContactsActivity . selectedContactsKey ) ) { return }
val selectedContacts = extras . getStringArray ( selectedContactsKey ) !!
val openGroup = DatabaseFactory . getLokiThreadDatabase ( this ) . getOpenGroupChat ( threadID )
for ( contact in selectedContacts ) {
val recipient = Recipient . from ( this , fromSerialized ( contact ) , true )
val message = VisibleMessage ( )
message . sentTimestamp = System . currentTimeMillis ( )
val openGroupInvitation = OpenGroupInvitation ( )
openGroupInvitation . name = openGroup !! . name
openGroupInvitation . url = openGroup !! . joinURL
message . openGroupInvitation = openGroupInvitation
val outgoingTextMessage = OutgoingTextMessage . fromOpenGroupInvitation ( openGroupInvitation , recipient , message . sentTimestamp )
DatabaseFactory . getSmsDatabase ( this ) . insertMessageOutbox ( - 1 , outgoingTextMessage , message . sentTimestamp !! )
MessageSender . send ( message , recipient . address )
}
}
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-06-29 02:05:39 +02:00
override fun deleteMessages ( messages : Set < MessageRecord > ) {
val messageCount = messages . size
val messageDataProvider = MessagingModuleConfiguration . shared . messageDataProvider
val messageDB = DatabaseFactory . getLokiMessageDatabase ( this @ConversationActivityV2 )
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 )
val openGroup = DatabaseFactory . getLokiThreadDatabase ( this ) . getOpenGroupChat ( threadID )
builder . setPositiveButton ( R . string . delete ) { _ , _ ->
if ( openGroup != null ) {
val messageServerIDs = mutableMapOf < Long , MessageRecord > ( )
for ( message in messages ) {
val messageServerID = messageDB . getServerID ( message . id , ! message . isMms ) ?: continue
messageServerIDs [ messageServerID ] = message
}
for ( ( messageServerID , message ) in messageServerIDs ) {
OpenGroupAPIV2 . deleteMessage ( messageServerID , openGroup . room , openGroup . server )
. success {
messageDataProvider . deleteMessage ( message . id , ! message . isMms )
} . failUi { error ->
Toast . makeText ( this @ConversationActivityV2 , " Couldn't delete message due to error: $error " , Toast . LENGTH _LONG ) . show ( )
}
}
} else {
2021-06-30 05:49:23 +02:00
for ( message in messages ) {
if ( message . isMms ) {
DatabaseFactory . getMmsDatabase ( this @ConversationActivityV2 ) . delete ( message . id )
} else {
DatabaseFactory . getSmsDatabase ( this @ConversationActivityV2 ) . deleteMessage ( message . id )
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
}
override fun banUser ( messages : Set < MessageRecord > ) {
2021-06-29 02:05:39 +02:00
val builder = AlertDialog . Builder ( this )
val sessionID = messages . first ( ) . individualRecipient . address . toString ( )
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 )
val openGroup = DatabaseFactory . getLokiThreadDatabase ( this ) . getOpenGroupChat ( threadID ) !!
builder . setPositiveButton ( R . string . ban ) { _ , _ ->
OpenGroupAPIV2 . ban ( sessionID , openGroup . room , openGroup . server ) . successUi {
Toast . makeText ( this @ConversationActivityV2 , " Successfully banned user " , Toast . LENGTH _LONG ) . show ( )
} . failUi { error ->
Toast . makeText ( this @ConversationActivityV2 , " Couldn't ban user due to error: $error " , Toast . LENGTH _LONG ) . show ( )
}
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 )
val sessionID = messages . first ( ) . individualRecipient . address . toString ( )
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 )
val openGroup = DatabaseFactory . getLokiThreadDatabase ( this ) . getOpenGroupChat ( threadID ) !!
builder . setPositiveButton ( R . string . ban ) { _ , _ ->
OpenGroupAPIV2 . banAndDeleteAll ( sessionID , openGroup . room , openGroup . server ) . successUi {
Toast . makeText ( this @ConversationActivityV2 , " Successfully banned user and deleted all their messages " , Toast . LENGTH _LONG ) . show ( )
} . failUi { error ->
Toast . makeText ( this @ConversationActivityV2 , " Couldn't execute request due to error: $error " , Toast . LENGTH _LONG ) . show ( )
}
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 }
val builder = StringBuilder ( )
for ( message in sortedMessages ) {
val body = MentionUtilities . highlightMentions ( message . body , message . threadId , this )
if ( TextUtils . isEmpty ( body ) ) { continue }
2021-07-08 05:37:08 +02:00
val formattedTimestamp = DateUtils . getDisplayFormattedTimeSpanString ( this , Locale . getDefault ( ) , message . timestamp )
2021-06-29 02:05:39 +02:00
builder . append ( " $formattedTimestamp : $body " ) . append ( '\n' )
}
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 > ) {
2021-07-01 03:06:11 +02:00
messages . 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 > ) {
2021-07-01 03:27:52 +02:00
inputBar . draftQuote ( thread , messages . first ( ) , glide )
2021-06-29 02:05:39 +02:00
endActionMode ( )
2021-06-28 08:28:00 +02:00
}
private fun sendMediaSavedNotification ( ) {
if ( thread . isGroupRecipient ) { return }
val timestamp = System . currentTimeMillis ( )
val kind = DataExtractionNotification . Kind . MediaSaved ( timestamp )
val message = DataExtractionNotification ( kind )
MessageSender . send ( message , thread . address )
}
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 {
2021-06-25 06:42:04 +02:00
var result = inputBar . inputBarEditText . text ?. trim ( ) ?: " "
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 " )
}
}
2021-06-25 07:20:54 +02:00
return result . toString ( )
2021-06-25 06:42:04 +02:00
}
2021-06-22 08:23:47 +02:00
private fun saveDraft ( ) {
2021-07-09 01:24:43 +02:00
val text = inputBar ?. text ?. trim ( ) ?: return
2021-06-22 08:23:47 +02:00
if ( text . isEmpty ( ) ) { return }
val drafts = Drafts ( )
drafts . add ( DraftDatabase . Draft ( DraftDatabase . Draft . TEXT , text ) )
val draftDB = DatabaseFactory . getDraftDatabase ( this )
draftDB . insertDrafts ( threadID , drafts )
}
// endregion
2021-06-29 03:49:10 +02:00
// region Search
2021-06-29 06:00:47 +02:00
private fun setUpSearchResultObserver ( ) {
val searchViewModel = ViewModelProvider ( this ) . get ( SearchViewModel :: class . java )
this . searchViewModel = searchViewModel
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 {
jumpToMessage ( it . messageRecipient . address , it . receivedTimestampMs , Runnable { searchViewModel . onMissingResult ( ) } )
}
2021-06-29 06:00:47 +02:00
}
this . searchBottomBar . setData ( result . position , result . getResults ( ) . size )
} )
}
2021-06-29 03:49:10 +02:00
fun onSearchQueryUpdated ( query : String ? ) {
adapter . onSearchQueryUpdated ( query )
}
2021-06-29 06:00:47 +02:00
override fun onSearchMoveUpPressed ( ) {
this . searchViewModel ?. onMoveUp ( )
}
override fun onSearchMoveDownPressed ( ) {
this . searchViewModel ?. onMoveDown ( )
}
2021-06-29 07:35:53 +02:00
private fun jumpToMessage ( author : Address , timestamp : Long , onMessageNotFound : Runnable ? ) {
SimpleTask . run ( lifecycle , {
DatabaseFactory . getMmsSmsDatabase ( this ) . getMessagePositionInConversation ( threadID , timestamp , author )
} ) { p : Int -> moveToMessagePosition ( p , onMessageNotFound ) }
}
private fun moveToMessagePosition ( position : Int , onMessageNotFound : Runnable ? ) {
if ( position >= 0 ) {
conversationRecyclerView . scrollToPosition ( position )
} else {
onMessageNotFound ?. run ( )
}
}
2021-06-29 03:49:10 +02:00
// endregion
2021-05-31 06:06:02 +02:00
}