2021-05-31 06:06:02 +02:00
|
|
|
package org.thoughtcrime.securesms.conversation.v2
|
|
|
|
|
2021-06-16 07:49:39 +02:00
|
|
|
import android.animation.FloatEvaluator
|
|
|
|
import android.animation.ValueAnimator
|
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-05-31 06:06:02 +02:00
|
|
|
import android.os.Bundle
|
2021-06-23 06:48:29 +02:00
|
|
|
import android.util.Log
|
2021-06-24 02:04:43 +02:00
|
|
|
import android.util.TypedValue
|
2021-06-23 05:11:21 +02:00
|
|
|
import android.view.*
|
2021-06-16 01:51:50 +02:00
|
|
|
import android.widget.RelativeLayout
|
2021-06-24 02:18:52 +02:00
|
|
|
import androidx.core.view.isVisible
|
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-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-05-31 06:06:02 +02:00
|
|
|
import network.loki.messenger.R
|
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-18 03:00:52 +02:00
|
|
|
import org.session.libsession.messaging.mentions.MentionsManager
|
2021-06-24 03:43:51 +02:00
|
|
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
2021-06-24 07:46:36 +02:00
|
|
|
import org.session.libsession.utilities.TextSecurePreferences
|
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-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
|
|
|
|
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
|
2021-06-21 06:48:27 +02:00
|
|
|
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
|
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-24 07:46:36 +02:00
|
|
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository
|
|
|
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
|
|
|
|
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState
|
2021-06-17 05:18:09 +02:00
|
|
|
import org.thoughtcrime.securesms.loki.utilities.toPx
|
2021-06-01 08:17:14 +02:00
|
|
|
import org.thoughtcrime.securesms.mms.GlideApp
|
2021-06-24 03:38:06 +02:00
|
|
|
import org.thoughtcrime.securesms.util.DateUtils
|
|
|
|
import java.util.*
|
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,
|
|
|
|
InputBarRecordingViewDelegate, ConversationRecyclerViewDelegate {
|
|
|
|
private val scrollButtonFullVisibilityThreshold by lazy { toPx(120.0f, resources) }
|
|
|
|
private val scrollButtonNoVisibilityThreshold by lazy { toPx(20.0f, resources) }
|
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-17 05:18:09 +02:00
|
|
|
private var isLockViewExpanded = false
|
2021-06-17 08:29:57 +02:00
|
|
|
private var isShowingAttachmentOptions = false
|
2021-06-18 03:05:14 +02:00
|
|
|
private var mentionCandidatesView: MentionCandidatesView? = null
|
2021-06-25 02:02:59 +02:00
|
|
|
private var unreadCount = 0
|
|
|
|
|
|
|
|
private val layoutManager: LinearLayoutManager
|
|
|
|
get() { return conversationRecyclerView.layoutManager as LinearLayoutManager }
|
2021-05-31 06:06:02 +02:00
|
|
|
|
2021-06-08 07:29:02 +02:00
|
|
|
// TODO: Selected message background color
|
|
|
|
// TODO: Overflow menu background + text color
|
|
|
|
|
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-21 06:48:27 +02:00
|
|
|
onItemPress = { message, position, view ->
|
|
|
|
handlePress(message, position, view)
|
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-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 {
|
|
|
|
const val THREAD_ID = "thread_id"
|
|
|
|
}
|
|
|
|
// endregion
|
|
|
|
|
|
|
|
// region Lifecycle
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
|
|
|
super.onCreate(savedInstanceState, isReady)
|
|
|
|
setContentView(R.layout.activity_conversation_v2)
|
|
|
|
threadID = intent.getLongExtra(THREAD_ID, -1)
|
|
|
|
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-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-25 01:38:26 +02:00
|
|
|
scrollToFirstUnreadMessage()
|
2021-06-25 01:44:27 +02:00
|
|
|
markAllAsRead()
|
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-23 06:48:29 +02:00
|
|
|
conversationRecyclerView.delegate = this
|
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-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-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)
|
|
|
|
// Document button
|
|
|
|
documentButtonContainer.addView(documentButton)
|
|
|
|
documentButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
|
|
|
// Library button
|
|
|
|
libraryButtonContainer.addView(libraryButton)
|
|
|
|
libraryButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
|
|
|
// Camera button
|
|
|
|
cameraButtonContainer.addView(cameraButton)
|
|
|
|
cameraButton.layoutParams = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)
|
|
|
|
}
|
|
|
|
|
2021-06-22 08:23:47 +02:00
|
|
|
private fun restoreDraftIfNeeded() {
|
|
|
|
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-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 (!isOxenHostedOpenGroup) { 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()
|
|
|
|
typingIndicatorViewContainer.isVisible = recipients.isNotEmpty()
|
|
|
|
typingIndicatorViewContainer.setTypists(recipients)
|
|
|
|
inputBarHeightChanged(inputBar.height)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 01:38:26 +02:00
|
|
|
private fun scrollToFirstUnreadMessage() {
|
|
|
|
val lastSeenTimestamp = DatabaseFactory.getThreadDatabase(this).getLastSeenAndHasSent(threadID).first()
|
|
|
|
val lastSeenItemPosition = adapter.findLastSeenItemPosition(lastSeenTimestamp) ?: return
|
|
|
|
conversationRecyclerView.scrollToPosition(lastSeenItemPosition)
|
|
|
|
}
|
|
|
|
|
2021-06-07 01:48:01 +02:00
|
|
|
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
2021-06-07 06:04:55 +02:00
|
|
|
ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, thread, this) { onOptionsItemSelected(it) }
|
|
|
|
super.onPrepareOptionsMenu(menu)
|
|
|
|
return true
|
2021-06-07 01:48:01 +02:00
|
|
|
}
|
2021-06-22 08:23:47 +02:00
|
|
|
|
|
|
|
override fun onDestroy() {
|
|
|
|
saveDraft()
|
|
|
|
super.onDestroy()
|
|
|
|
}
|
2021-06-01 08:17:14 +02:00
|
|
|
// endregion
|
|
|
|
|
2021-06-18 07:54:24 +02:00
|
|
|
// region Updating & Animation
|
2021-06-25 01:44:27 +02:00
|
|
|
private fun markAllAsRead() {
|
|
|
|
DatabaseFactory.getThreadDatabase(this).setRead(threadID, true)
|
|
|
|
}
|
|
|
|
|
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-24 03:22:32 +02:00
|
|
|
recyclerViewLayoutParams.bottomMargin = newValue + additionalContentContainer.height + 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-24 07:46:36 +02:00
|
|
|
linkPreviewViewModel?.onTextChanged(this, inputBar.text, 0, 0)
|
2021-06-18 07:54:24 +02:00
|
|
|
// TODO: Implement the full mention show/hide logic
|
2021-06-18 03:00:52 +02:00
|
|
|
if (newContent.contains("@")) {
|
|
|
|
showMentionCandidates()
|
2021-06-18 03:05:14 +02:00
|
|
|
} else {
|
|
|
|
hideMentionCandidates()
|
2021-06-18 03:00:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun showMentionCandidates() {
|
2021-06-18 07:11:41 +02:00
|
|
|
additionalContentContainer.removeAllViews()
|
2021-06-18 03:00:52 +02:00
|
|
|
val mentionCandidatesView = MentionCandidatesView(this)
|
|
|
|
mentionCandidatesView.glide = glide
|
2021-06-18 07:11:41 +02:00
|
|
|
additionalContentContainer.addView(mentionCandidatesView)
|
2021-06-18 03:00:52 +02:00
|
|
|
val mentionCandidates = MentionsManager.getMentionCandidates("", threadID, thread.isOpenGroupRecipient)
|
2021-06-18 03:05:14 +02:00
|
|
|
this.mentionCandidatesView = mentionCandidatesView
|
2021-06-18 03:00:52 +02:00
|
|
|
mentionCandidatesView.show(mentionCandidates, threadID)
|
2021-06-18 03:05:14 +02:00
|
|
|
mentionCandidatesView.alpha = 0.0f
|
|
|
|
val animation = ValueAnimator.ofObject(FloatEvaluator(), mentionCandidatesView.alpha, 1.0f)
|
|
|
|
animation.duration = 250L
|
|
|
|
animation.addUpdateListener { animator ->
|
|
|
|
mentionCandidatesView.alpha = animator.animatedValue as Float
|
|
|
|
}
|
|
|
|
animation.start()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun hideMentionCandidates() {
|
|
|
|
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
|
2021-06-18 07:11:41 +02:00
|
|
|
if (animator.animatedFraction == 1.0f) { additionalContentContainer.removeAllViews() }
|
2021-06-18 03:05:14 +02:00
|
|
|
}
|
|
|
|
animation.start()
|
2021-06-18 03:00:52 +02:00
|
|
|
}
|
|
|
|
|
2021-06-17 08:29:57 +02:00
|
|
|
override fun toggleAttachmentOptions() {
|
|
|
|
val targetAlpha = if (isShowingAttachmentOptions) 0.0f else 1.0f
|
|
|
|
val allButtons = listOf( cameraButtonContainer, libraryButtonContainer, documentButtonContainer, gifButtonContainer)
|
2021-06-18 01:51:44 +02:00
|
|
|
val isReversed = isShowingAttachmentOptions // Run the animation in reverse
|
|
|
|
val count = allButtons.size
|
2021-06-17 08:29:57 +02:00
|
|
|
allButtons.indices.forEach { index ->
|
|
|
|
val view = allButtons[index]
|
|
|
|
val animation = ValueAnimator.ofObject(FloatEvaluator(), view.alpha, targetAlpha)
|
|
|
|
animation.duration = 250L
|
2021-06-18 01:51:44 +02:00
|
|
|
animation.startDelay = if (isReversed) 50L * (count - index.toLong()) else 50L * index.toLong()
|
2021-06-17 08:29:57 +02:00
|
|
|
animation.addUpdateListener { animator ->
|
|
|
|
view.alpha = animator.animatedValue as Float
|
|
|
|
}
|
|
|
|
animation.start()
|
|
|
|
}
|
|
|
|
isShowingAttachmentOptions = !isShowingAttachmentOptions
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
|
|
override fun handleConversationRecyclerViewBottomOffsetChanged(bottomOffset: Int) {
|
|
|
|
val rawAlpha = (bottomOffset.toFloat() - scrollButtonNoVisibilityThreshold) /
|
|
|
|
(scrollButtonFullVisibilityThreshold - scrollButtonNoVisibilityThreshold)
|
|
|
|
val alpha = max(min(rawAlpha, 1.0f), 0.0f)
|
2021-06-23 07:14:19 +02:00
|
|
|
scrollToBottomButton.alpha = alpha
|
2021-06-25 02:02:59 +02:00
|
|
|
unreadCount = layoutManager.findFirstVisibleItemPosition()
|
|
|
|
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-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-01 08:17:14 +02:00
|
|
|
// TODO: Implement
|
2021-06-07 01:48:01 +02:00
|
|
|
return super.onOptionsItemSelected(item)
|
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-21 06:48:27 +02:00
|
|
|
private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView) {
|
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)
|
|
|
|
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-21 06:48:27 +02:00
|
|
|
view.onContentClick()
|
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-06-18 07:11:41 +02:00
|
|
|
inputBar.draftQuote(message)
|
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-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 (!isLockViewExpanded) {
|
2021-06-18 07:54:24 +02:00
|
|
|
expandVoiceMessageLockView()
|
2021-06-17 05:18:09 +02:00
|
|
|
isLockViewExpanded = true
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (isLockViewExpanded) {
|
2021-06-18 07:54:24 +02:00
|
|
|
collapseVoiceMessageLockView()
|
2021-06-17 05:18:09 +02:00
|
|
|
isLockViewExpanded = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-17 02:53:56 +02:00
|
|
|
override fun onMicrophoneButtonCancel(event: MotionEvent) {
|
2021-06-18 07:54:24 +02:00
|
|
|
hideVoiceMessageUI()
|
2021-06-17 02:53:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onMicrophoneButtonUp(event: MotionEvent) {
|
2021-06-17 06:01:43 +02:00
|
|
|
if (isValidLockViewLocation(event.rawX.roundToInt(), event.rawY.roundToInt())) {
|
|
|
|
inputBarRecordingView.lock()
|
|
|
|
} else {
|
2021-06-18 07:54:24 +02:00
|
|
|
hideVoiceMessageUI()
|
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() {
|
|
|
|
// TODO: Implement
|
|
|
|
}
|
2021-05-31 06:06:02 +02:00
|
|
|
// endregion
|
2021-06-22 08:23:47 +02:00
|
|
|
|
|
|
|
// region General
|
|
|
|
private fun saveDraft() {
|
|
|
|
val text = inputBar.text.trim()
|
|
|
|
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-05-31 06:06:02 +02:00
|
|
|
}
|