fix(dm): 채팅방 입력과 프로필 표시를 보정한다
This commit is contained in:
@@ -4,15 +4,15 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.TextWatcher
|
import android.text.TextWatcher
|
||||||
import android.view.View
|
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityDmChatRoomBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityDmChatRoomBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
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.model.DmChatRoomUiState
|
||||||
@@ -25,6 +25,7 @@ class DmChatRoomActivity : BaseActivity<ActivityDmChatRoomBinding>(
|
|||||||
|
|
||||||
private val viewModel: DmChatRoomViewModel by viewModel()
|
private val viewModel: DmChatRoomViewModel by viewModel()
|
||||||
private val messageAdapter = DmChatMessageAdapter { localId -> viewModel.retry(localId) }
|
private val messageAdapter = DmChatMessageAdapter { localId -> viewModel.retry(localId) }
|
||||||
|
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(this, layoutInflater) }
|
||||||
private lateinit var layoutManager: LinearLayoutManager
|
private lateinit var layoutManager: LinearLayoutManager
|
||||||
private var roomId: Long = 0L
|
private var roomId: Long = 0L
|
||||||
private var creatorId: Long = 0L
|
private var creatorId: Long = 0L
|
||||||
@@ -119,6 +120,9 @@ class DmChatRoomActivity : BaseActivity<ActivityDmChatRoomBinding>(
|
|||||||
viewModel.roomOpenedEventLiveData.observe(this) {
|
viewModel.roomOpenedEventLiveData.observe(this) {
|
||||||
if (it.consume() == true) connectRealtimeIfStarted()
|
if (it.consume() == true) connectRealtimeIfStarted()
|
||||||
}
|
}
|
||||||
|
viewModel.isRealtimeConnectingLiveData.observe(this) { isConnecting ->
|
||||||
|
updateRealtimeLoadingDialog(isConnecting)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindContent(state: DmChatRoomUiState.Content) {
|
private fun bindContent(state: DmChatRoomUiState.Content) {
|
||||||
@@ -129,7 +133,7 @@ class DmChatRoomActivity : BaseActivity<ActivityDmChatRoomBinding>(
|
|||||||
error(R.drawable.ic_placeholder_profile)
|
error(R.drawable.ic_placeholder_profile)
|
||||||
transformations(CircleCropTransformation())
|
transformations(CircleCropTransformation())
|
||||||
}
|
}
|
||||||
binding.ivBackgroundProfile.loadUrl(state.opponentProfileImageUrl) {
|
binding.ivBackgroundProfile.loadUrl(SharedPreferenceManager.profileImage) {
|
||||||
placeholder(R.drawable.ic_placeholder_profile)
|
placeholder(R.drawable.ic_placeholder_profile)
|
||||||
error(R.drawable.ic_placeholder_profile)
|
error(R.drawable.ic_placeholder_profile)
|
||||||
}
|
}
|
||||||
@@ -143,10 +147,10 @@ class DmChatRoomActivity : BaseActivity<ActivityDmChatRoomBinding>(
|
|||||||
val text = binding.etMessage.text?.toString().orEmpty()
|
val text = binding.etMessage.text?.toString().orEmpty()
|
||||||
if (text.isBlank()) return
|
if (text.isBlank()) return
|
||||||
|
|
||||||
viewModel.sendText(text)
|
if (viewModel.sendText(text)) {
|
||||||
binding.etMessage.setText("")
|
binding.etMessage.setText("")
|
||||||
setSendButtonEnabled(false)
|
setSendButtonEnabled(false)
|
||||||
hideKeyboard(binding.etMessage)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setSendButtonEnabled(enabled: Boolean) {
|
private fun setSendButtonEnabled(enabled: Boolean) {
|
||||||
@@ -182,10 +186,12 @@ class DmChatRoomActivity : BaseActivity<ActivityDmChatRoomBinding>(
|
|||||||
viewModel.connectRealtime()
|
viewModel.connectRealtime()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideKeyboard(view: View) {
|
private fun updateRealtimeLoadingDialog(isConnecting: Boolean) {
|
||||||
val imm = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager
|
if (isConnecting) {
|
||||||
imm?.hideSoftInputFromWindow(view.windowToken, 0)
|
loadingDialog.show(screenWidth)
|
||||||
view.clearFocus()
|
} else {
|
||||||
|
loadingDialog.dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class DmChatRoomActivitySourceTest {
|
|||||||
).readText()
|
).readText()
|
||||||
|
|
||||||
assertTrue(source.contains("BaseActivity<ActivityDmChatRoomBinding>"))
|
assertTrue(source.contains("BaseActivity<ActivityDmChatRoomBinding>"))
|
||||||
|
assertTrue(source.contains("import kr.co.vividnext.sodalive.common.LoadingDialog"))
|
||||||
|
assertTrue(source.contains("private val loadingDialog: LoadingDialog by lazy"))
|
||||||
assertTrue(source.contains("const val EXTRA_ROOM_ID"))
|
assertTrue(source.contains("const val EXTRA_ROOM_ID"))
|
||||||
assertTrue(source.contains("const val EXTRA_CREATOR_ID"))
|
assertTrue(source.contains("const val EXTRA_CREATOR_ID"))
|
||||||
assertTrue(source.contains("fun newIntentByRoomId"))
|
assertTrue(source.contains("fun newIntentByRoomId"))
|
||||||
@@ -26,6 +28,11 @@ class DmChatRoomActivitySourceTest {
|
|||||||
assertTrue(source.contains("EditorInfo.IME_ACTION_SEND"))
|
assertTrue(source.contains("EditorInfo.IME_ACTION_SEND"))
|
||||||
assertTrue(source.contains("binding.ivSend.setOnClickListener"))
|
assertTrue(source.contains("binding.ivSend.setOnClickListener"))
|
||||||
assertTrue(source.contains("viewModel.sendText"))
|
assertTrue(source.contains("viewModel.sendText"))
|
||||||
|
assertTrue(source.contains("if (viewModel.sendText(text))"))
|
||||||
|
assertFalse(source.contains("viewModel.sendText(text)\n binding.etMessage.setText(\"\")"))
|
||||||
|
assertFalse(source.contains("hideKeyboard"))
|
||||||
|
assertFalse(source.contains("hideSoftInputFromWindow"))
|
||||||
|
assertFalse(source.contains("view.clearFocus()"))
|
||||||
assertTrue(source.contains("findFirstVisibleItemPosition()"))
|
assertTrue(source.contains("findFirstVisibleItemPosition()"))
|
||||||
assertTrue(source.contains("viewModel.loadOlderMessages()"))
|
assertTrue(source.contains("viewModel.loadOlderMessages()"))
|
||||||
assertTrue(source.contains("prependedMessageCountLiveData.observe(this)"))
|
assertTrue(source.contains("prependedMessageCountLiveData.observe(this)"))
|
||||||
@@ -35,6 +42,10 @@ class DmChatRoomActivitySourceTest {
|
|||||||
assertTrue(source.contains("viewModel.connectRealtime()"))
|
assertTrue(source.contains("viewModel.connectRealtime()"))
|
||||||
assertTrue(source.contains("roomOpenedEventLiveData.observe(this)"))
|
assertTrue(source.contains("roomOpenedEventLiveData.observe(this)"))
|
||||||
assertTrue(source.contains("if (it.consume() == true) connectRealtimeIfStarted()"))
|
assertTrue(source.contains("if (it.consume() == true) connectRealtimeIfStarted()"))
|
||||||
|
assertTrue(source.contains("isRealtimeConnectingLiveData.observe(this)"))
|
||||||
|
assertTrue(source.contains("updateRealtimeLoadingDialog(isConnecting)"))
|
||||||
|
assertTrue(source.contains("loadingDialog.show(screenWidth)"))
|
||||||
|
assertTrue(source.contains("loadingDialog.dismiss()"))
|
||||||
assertTrue(source.contains("connectRealtimeIfStarted()"))
|
assertTrue(source.contains("connectRealtimeIfStarted()"))
|
||||||
assertTrue(source.contains("viewModel.leaveRealtime()"))
|
assertTrue(source.contains("viewModel.leaveRealtime()"))
|
||||||
assertFalse(source.contains("if (isStarted) viewModel.connectRealtime()"))
|
assertFalse(source.contains("if (isStarted) viewModel.connectRealtime()"))
|
||||||
@@ -53,6 +64,80 @@ class DmChatRoomActivitySourceTest {
|
|||||||
assertTrue(source.contains("putExtra(EXTRA_CREATOR_ID, creatorId)"))
|
assertTrue(source.contains("putExtra(EXTRA_CREATOR_ID, creatorId)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `DM 메시지 UI는 좌우 정렬 기존 버블 텍스트 전송만 유지한다`() {
|
||||||
|
val myMessage = projectFile(
|
||||||
|
"app/src/main/res/layout/item_dm_chat_my_message.xml"
|
||||||
|
).readText()
|
||||||
|
val opponentMessage = projectFile(
|
||||||
|
"app/src/main/res/layout/item_dm_chat_opponent_message.xml"
|
||||||
|
).readText()
|
||||||
|
val roomLayout = projectFile(
|
||||||
|
"app/src/main/res/layout/activity_dm_chat_room.xml"
|
||||||
|
).readText()
|
||||||
|
val activity = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt"
|
||||||
|
).readText()
|
||||||
|
val adapter = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/ui/DmChatMessageAdapter.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(myMessage.contains("android:background=\"@drawable/bg_chat_user_message\""))
|
||||||
|
assertTrue(myMessage.contains("android:id=\"@+id/tv_time\""))
|
||||||
|
assertTrue(myMessage.contains("app:layout_constraintEnd_toStartOf=\"@id/message_container\""))
|
||||||
|
assertTrue(myMessage.contains("app:layout_constraintEnd_toEndOf=\"parent\""))
|
||||||
|
assertTrue(myMessage.contains("app:layout_constraintStart_toStartOf=\"parent\""))
|
||||||
|
assertTrue(myMessage.contains("app:layout_constraintStart_toEndOf=\"@id/tv_time\""))
|
||||||
|
assertTrue(myMessage.contains("app:layout_constraintHorizontal_bias=\"1\""))
|
||||||
|
assertTrue(myMessage.contains("app:layout_constraintHorizontal_chainStyle=\"packed\""))
|
||||||
|
assertTrue(myMessage.contains("app:layout_constrainedWidth=\"true\""))
|
||||||
|
assertTrue(opponentMessage.contains("android:background=\"@drawable/bg_chat_ai_message\""))
|
||||||
|
assertTrue(opponentMessage.contains("android:id=\"@+id/tv_time\""))
|
||||||
|
assertTrue(opponentMessage.contains("app:layout_constraintStart_toStartOf=\"parent\""))
|
||||||
|
assertTrue(opponentMessage.contains("app:layout_constraintStart_toEndOf=\"@id/iv_profile\""))
|
||||||
|
assertTrue(opponentMessage.contains("app:layout_constraintStart_toEndOf=\"@id/message_group\""))
|
||||||
|
assertTrue(adapter.contains("import kr.co.vividnext.sodalive.chat.talk.room.formatMessageTime"))
|
||||||
|
assertTrue(adapter.contains("tvTime.text = formatMessageTime(item.createdAt)"))
|
||||||
|
assertTrue(adapter.contains("MESSAGE_MAX_WIDTH_RATIO = 0.68f"))
|
||||||
|
|
||||||
|
assertTrue(roomLayout.contains("@+id/et_message"))
|
||||||
|
assertTrue(roomLayout.contains("@+id/iv_send"))
|
||||||
|
assertFalse(roomLayout.contains("voice"))
|
||||||
|
assertFalse(activity.contains("sendVoiceMessage"))
|
||||||
|
assertFalse(activity.contains("record"))
|
||||||
|
assertFalse(activity.contains("playVoice"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `AppDI는 DM WebSocket raw JSON logger를 production 생성 경로에 주입한다`() {
|
||||||
|
val appDi = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(appDi.contains("import com.orhanobut.logger.Logger"))
|
||||||
|
assertTrue(appDi.contains("rawJsonLogger = { rawJson ->"))
|
||||||
|
assertTrue(appDi.contains("Logger.d(\"DM WebSocket raw JSON: ${'$'}rawJson\")"))
|
||||||
|
assertFalse(appDi.contains("DmChatSocketClient(okHttpClient = get(), gson = get(), baseUrl = baseUrl)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `DM 채팅방 Activity 프로필 배경은 내 프로필 이미지를 사용하고 헤더는 상대 프로필을 유지한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/dm/DmChatRoomActivity.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
val foregroundProfileBinding = source.substringAfter("binding.ivProfile.loadUrl(")
|
||||||
|
.substringBefore("binding.ivBackgroundProfile.loadUrl(")
|
||||||
|
val backgroundProfileBinding = source.substringAfter("binding.ivBackgroundProfile.loadUrl(")
|
||||||
|
.substringBefore("messageAdapter.submitItems")
|
||||||
|
|
||||||
|
assertTrue(source.contains("import kr.co.vividnext.sodalive.common.SharedPreferenceManager"))
|
||||||
|
assertTrue(foregroundProfileBinding.contains("state.opponentProfileImageUrl"))
|
||||||
|
assertTrue(foregroundProfileBinding.contains("transformations(CircleCropTransformation())"))
|
||||||
|
assertTrue(backgroundProfileBinding.contains("SharedPreferenceManager.profileImage"))
|
||||||
|
assertFalse(backgroundProfileBinding.contains("state.opponentProfileImageUrl"))
|
||||||
|
}
|
||||||
|
|
||||||
private fun projectFile(relativePath: String): File {
|
private fun projectFile(relativePath: String): File {
|
||||||
val candidates = listOf(File(relativePath), File("../$relativePath"))
|
val candidates = listOf(File(relativePath), File("../$relativePath"))
|
||||||
return candidates.firstOrNull { it.exists() }
|
return candidates.firstOrNull { it.exists() }
|
||||||
|
|||||||
Reference in New Issue
Block a user