feat(chat): DM 채팅방 Activity를 추가한다
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.chat.dm
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityDmChatRoomBinding
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.model.DmChatRoomUiState
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.ui.DmChatMessageAdapter
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class DmChatRoomActivity : BaseActivity<ActivityDmChatRoomBinding>(
|
||||
ActivityDmChatRoomBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: DmChatRoomViewModel by viewModel()
|
||||
private val messageAdapter = DmChatMessageAdapter { localId -> viewModel.retry(localId) }
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
private var roomId: Long = 0L
|
||||
private var creatorId: Long = 0L
|
||||
private var isStarted: Boolean = false
|
||||
private var firstVisiblePositionBeforePrepend: Int = RecyclerView.NO_POSITION
|
||||
private var firstVisibleTopBeforePrepend: Int = 0
|
||||
|
||||
override fun setupView() {
|
||||
roomId = intent.getLongExtra(EXTRA_ROOM_ID, 0L)
|
||||
creatorId = intent.getLongExtra(EXTRA_CREATOR_ID, 0L)
|
||||
|
||||
setupHeader()
|
||||
setupRecyclerView()
|
||||
setupInput()
|
||||
bindViewModel()
|
||||
|
||||
viewModel.enter(roomId = roomId, creatorId = creatorId)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
isStarted = true
|
||||
viewModel.connectRealtime()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
isStarted = false
|
||||
viewModel.disconnectRealtime()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
private fun setupHeader() {
|
||||
binding.ivBack.setOnClickListener { finish() }
|
||||
binding.ivProfile.setImageResource(R.drawable.ic_placeholder_profile)
|
||||
binding.ivBackgroundProfile.setImageResource(R.drawable.ic_placeholder_profile)
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
layoutManager = LinearLayoutManager(this).apply {
|
||||
stackFromEnd = true
|
||||
}
|
||||
binding.rvMessages.layoutManager = layoutManager
|
||||
binding.rvMessages.adapter = messageAdapter
|
||||
(binding.rvMessages.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
|
||||
binding.rvMessages.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
if (layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||
rememberScrollBeforePrepend()
|
||||
viewModel.loadOlderMessages()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupInput() {
|
||||
setSendButtonEnabled(false)
|
||||
binding.etMessage.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
setSendButtonEnabled(!s.isNullOrBlank())
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) = Unit
|
||||
})
|
||||
binding.etMessage.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
||||
if (binding.ivSend.isEnabled) binding.ivSend.performClick()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
binding.ivSend.setOnClickListener { onSendClicked() }
|
||||
}
|
||||
|
||||
private fun bindViewModel() {
|
||||
viewModel.chatRoomStateLiveData.observe(this) { state ->
|
||||
when (state) {
|
||||
is DmChatRoomUiState.Content -> bindContent(state)
|
||||
is DmChatRoomUiState.Error -> showToast(state.message.orEmpty())
|
||||
DmChatRoomUiState.Loading -> Unit
|
||||
}
|
||||
}
|
||||
viewModel.finishEventLiveData.observe(this) {
|
||||
if (it == true) finish()
|
||||
}
|
||||
viewModel.prependedMessageCountLiveData.observe(this) { count ->
|
||||
if (count > 0) restoreScrollAfterPrepend(count)
|
||||
}
|
||||
viewModel.roomOpenedEventLiveData.observe(this) {
|
||||
if (it.consume() == true) connectRealtimeIfStarted()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindContent(state: DmChatRoomUiState.Content) {
|
||||
val shouldScrollToBottom = isNearBottom()
|
||||
binding.tvName.text = state.opponentNickname
|
||||
binding.ivProfile.loadUrl(state.opponentProfileImageUrl) {
|
||||
placeholder(R.drawable.ic_placeholder_profile)
|
||||
error(R.drawable.ic_placeholder_profile)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
binding.ivBackgroundProfile.loadUrl(state.opponentProfileImageUrl) {
|
||||
placeholder(R.drawable.ic_placeholder_profile)
|
||||
error(R.drawable.ic_placeholder_profile)
|
||||
}
|
||||
messageAdapter.submitItems(state.messages)
|
||||
if (shouldScrollToBottom && state.messages.isNotEmpty()) {
|
||||
binding.rvMessages.scrollToPosition(state.messages.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSendClicked() {
|
||||
val text = binding.etMessage.text?.toString().orEmpty()
|
||||
if (text.isBlank()) return
|
||||
|
||||
viewModel.sendText(text)
|
||||
binding.etMessage.setText("")
|
||||
setSendButtonEnabled(false)
|
||||
hideKeyboard(binding.etMessage)
|
||||
}
|
||||
|
||||
private fun setSendButtonEnabled(enabled: Boolean) {
|
||||
binding.ivSend.isEnabled = enabled
|
||||
binding.ivSend.alpha = if (enabled) 1.0f else 0.4f
|
||||
}
|
||||
|
||||
private fun rememberScrollBeforePrepend() {
|
||||
firstVisiblePositionBeforePrepend = layoutManager.findFirstVisibleItemPosition()
|
||||
val firstVisibleView = layoutManager.findViewByPosition(firstVisiblePositionBeforePrepend)
|
||||
firstVisibleTopBeforePrepend = firstVisibleView?.top ?: 0
|
||||
}
|
||||
|
||||
private fun restoreScrollAfterPrepend(count: Int) {
|
||||
if (firstVisiblePositionBeforePrepend == RecyclerView.NO_POSITION) return
|
||||
layoutManager.scrollToPositionWithOffset(
|
||||
firstVisiblePositionBeforePrepend + count,
|
||||
firstVisibleTopBeforePrepend
|
||||
)
|
||||
firstVisiblePositionBeforePrepend = RecyclerView.NO_POSITION
|
||||
firstVisibleTopBeforePrepend = 0
|
||||
}
|
||||
|
||||
private fun isNearBottom(): Boolean {
|
||||
val itemCount = messageAdapter.itemCount
|
||||
if (itemCount == 0) return true
|
||||
val lastVisible = layoutManager.findLastVisibleItemPosition()
|
||||
return lastVisible >= itemCount - NEAR_BOTTOM_THRESHOLD
|
||||
}
|
||||
|
||||
private fun connectRealtimeIfStarted() {
|
||||
if (!isStarted) return
|
||||
viewModel.connectRealtime()
|
||||
}
|
||||
|
||||
private fun hideKeyboard(view: View) {
|
||||
val imm = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
view.clearFocus()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_ROOM_ID: String = "extra_room_id"
|
||||
const val EXTRA_CREATOR_ID: String = "extra_creator_id"
|
||||
private const val NEAR_BOTTOM_THRESHOLD = 2
|
||||
|
||||
fun newIntentByRoomId(context: Context, roomId: Long): Intent {
|
||||
return Intent(context, DmChatRoomActivity::class.java).apply {
|
||||
putExtra(EXTRA_ROOM_ID, roomId)
|
||||
}
|
||||
}
|
||||
|
||||
fun newIntentByCreatorId(context: Context, creatorId: Long): Intent {
|
||||
return Intent(context, DmChatRoomActivity::class.java).apply {
|
||||
putExtra(EXTRA_CREATOR_ID, creatorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user