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

380 lines
17 KiB
Kotlin
Raw Normal View History

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
import android.database.Cursor
2021-06-17 05:18:09 +02:00
import android.graphics.Rect
import android.os.Bundle
2021-06-18 08:24:56 +02:00
import android.util.Log
2021-06-04 07:10:58 +02:00
import android.view.ActionMode
import android.view.Menu
2021-06-07 01:48:01 +02:00
import android.view.MenuItem
2021-06-17 02:53:56 +02:00
import android.view.MotionEvent
2021-06-16 01:51:50 +02:00
import android.widget.RelativeLayout
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
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.*
import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.*
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.*
import network.loki.messenger.R
2021-06-18 03:00:52 +02:00
import org.session.libsession.messaging.mentions.MentionsManager
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
2021-06-17 06:34:50 +02:00
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
2021-06-16 01:51:50 +02:00
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate
2021-06-18 03:00:52 +02:00
import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCandidatesView
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MessageRecord
2021-06-17 05:18:09 +02:00
import org.thoughtcrime.securesms.loki.utilities.toPx
import org.thoughtcrime.securesms.mms.GlideApp
2021-06-17 02:53:56 +02:00
import kotlin.math.abs
2021-06-17 05:18:09 +02:00
import kotlin.math.roundToInt
2021-06-17 02:53:56 +02:00
import kotlin.math.sqrt
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate, InputBarRecordingViewDelegate {
2021-06-18 07:54:24 +02:00
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
private var threadID: Long = -1
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-08 07:29:02 +02:00
// TODO: Selected message background color
// TODO: Overflow menu background + text color
private val adapter by lazy {
val cursor = DatabaseFactory.getMmsSmsDatabase(this).getConversation(threadID)
val adapter = ConversationAdapter(
this,
cursor,
onItemPress = { message, position, view ->
handlePress(message, position, view)
},
2021-06-09 03:37:50 +02:00
onItemSwipeToReply = { message, position ->
handleSwipeToReply(message, position)
},
onItemLongPress = { message, position ->
handleLongPress(message, position)
}
)
adapter
}
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
// 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()
}
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)
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-15 06:55:57 +02:00
private fun setUpToolBar() {
val actionBar = supportActionBar!!
actionBar.setCustomView(R.layout.activity_conversation_v2_action_bar)
actionBar.setDisplayShowCustomEnabled(true)
conversationTitleView.text = thread.toShortString()
2021-06-02 05:28:02 +02:00
profilePictureView.glide = glide
profilePictureView.update(thread, threadID)
}
2021-06-07 01:48:01 +02:00
2021-06-17 06:34:50 +02:00
private fun setUpInputBar() {
inputBar.delegate = this
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-07 01:48:01 +02:00
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, thread, this) { onOptionsItemSelected(it) }
super.onPrepareOptionsMenu(menu)
return true
2021-06-07 01:48:01 +02:00
}
// endregion
2021-06-18 07:54:24 +02:00
// region Updating & Animation
2021-06-16 01:51:50 +02:00
override fun inputBarHeightChanged(newValue: Int) {
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-18 07:11:41 +02:00
recyclerViewLayoutParams.bottomMargin = newValue + additionalContentContainer.height
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
val attachmentButtonHeight = inputBar.attachmentsButtonContainer.height
2021-06-18 08:24:56 +02:00
val bottomMargin = (newValue - inputBar.additionalContentHeight - attachmentButtonHeight) / 2
val margin = toPx(8, resources)
val attachmentOptionsContainerLayoutParams = attachmentOptionsContainer.layoutParams as RelativeLayout.LayoutParams
attachmentOptionsContainerLayoutParams.bottomMargin = bottomMargin + attachmentButtonHeight + margin
attachmentOptionsContainer.layoutParams = attachmentOptionsContainerLayoutParams
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-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()
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() {
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-16 01:51:50 +02:00
// endregion
// region Interaction
2021-06-07 01:48:01 +02:00
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// TODO: Implement
2021-06-07 01:48:01 +02:00
return super.onOptionsItemSelected(item)
}
2021-06-04 05:15:43 +02:00
2021-06-09 03:37:50 +02:00
// `position` is the adapter position; not the visual position
private fun handlePress(message: MessageRecord, position: Int, view: VisibleMessageView) {
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
}
} else {
view.onContentClick()
}
}
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
private fun handleLongPress(message: MessageRecord, position: Int) {
val actionMode = this.actionMode
val actionModeCallback = ConversationActionModeCallback(adapter, threadID, this)
if (actionMode == null) { // Nothing should be selected if this is the case
adapter.toggleSelection(message, position)
this.actionMode = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
startActionMode(actionModeCallback, ActionMode.TYPE_PRIMARY)
} else {
startActionMode(actionModeCallback)
}
2021-06-04 07:10:58 +02:00
} else {
adapter.toggleSelection(message, position)
actionModeCallback.updateActionModeMenu(actionMode.menu)
if (adapter.selectedItems.isEmpty()) {
actionMode.finish()
this.actionMode = null
}
2021-06-04 07:10:58 +02:00
}
}
2021-06-17 02:53:56 +02:00
override fun onMicrophoneButtonMove(event: MotionEvent) {
val rawX = event.rawX
val chevronImageView = 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 {
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
}
// endregion
}