session-android/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
2021-06-17 10:53:56 +10:00

220 lines
8.5 KiB
Kotlin

package org.thoughtcrime.securesms.conversation.v2
import android.animation.FloatEvaluator
import android.animation.ValueAnimator
import android.content.res.Resources
import android.database.Cursor
import android.os.Bundle
import android.util.Log
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.widget.RelativeLayout
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_conversation_v2.*
import kotlinx.android.synthetic.main.activity_conversation_v2.view.*
import kotlinx.android.synthetic.main.activity_conversation_v2_action_bar.*
import kotlinx.android.synthetic.main.view_input_bar.view.*
import kotlinx.android.synthetic.main.view_input_bar_recording.view.*
import network.loki.messenger.R
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.mms.GlideApp
import kotlin.math.abs
import kotlin.math.sqrt
class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate {
private var threadID: Long = -1
private var actionMode: ActionMode? = null
// 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 ->
handlePress(message, position)
},
onItemSwipeToReply = { message, position ->
handleSwipeToReply(message, position)
},
onItemLongPress = { message, position ->
handleLongPress(message, position)
}
)
adapter.setHasStableIds(true)
adapter
}
private val thread by lazy {
DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadID)!!
}
private val glide by lazy { GlideApp.with(this) }
private val screenWidth by lazy {
Resources.getSystem().displayMetrics.widthPixels
}
// 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()
setUpToolBar()
inputBar.delegate = this
}
private fun setUpRecyclerView() {
conversationRecyclerView.adapter = adapter
val layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, true)
conversationRecyclerView.layoutManager = layoutManager
// 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)
}
})
}
private fun setUpToolBar() {
val actionBar = supportActionBar!!
actionBar.setCustomView(R.layout.activity_conversation_v2_action_bar)
actionBar.setDisplayShowCustomEnabled(true)
conversationTitleView.text = thread.toShortString()
profilePictureView.glide = glide
profilePictureView.update(thread, threadID)
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
ConversationMenuHelper.onPrepareOptionsMenu(menu, menuInflater, thread, this) { onOptionsItemSelected(it) }
super.onPrepareOptionsMenu(menu)
return true
}
// endregion
// region Updating
override fun inputBarHeightChanged(newValue: Int) {
val recyclerViewLayoutParams = conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams
recyclerViewLayoutParams.bottomMargin = newValue
conversationRecyclerView.layoutParams = recyclerViewLayoutParams
}
override fun showVoiceMessageUI() {
inputBarRecordingView.show()
}
// endregion
// region Interaction
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// TODO: Implement
return super.onOptionsItemSelected(item)
}
// `position` is the adapter position; not the visual position
private fun handlePress(message: MessageRecord, position: Int) {
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
}
}
}
// `position` is the adapter position; not the visual position
private fun handleSwipeToReply(message: MessageRecord, position: Int) {
}
// `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)
}
} else {
adapter.toggleSelection(message, position)
actionModeCallback.updateActionModeMenu(actionMode.menu)
if (adapter.selectedItems.isEmpty()) {
actionMode.finish()
this.actionMode = null
}
}
}
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
}
}
override fun onMicrophoneButtonCancel(event: MotionEvent) {
resetVoiceMessageUI()
}
override fun onMicrophoneButtonUp(event: MotionEvent) {
resetVoiceMessageUI()
}
private fun resetVoiceMessageUI() {
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()
}
}
// endregion
}