fix(dm): 채팅방 입력과 프로필 표시를 보정한다

This commit is contained in:
2026-07-11 22:03:16 +09:00
parent 30f7bd7556
commit d436efdc66
2 changed files with 102 additions and 11 deletions

View File

@@ -4,15 +4,15 @@ 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.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
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
@@ -25,6 +25,7 @@ class DmChatRoomActivity : BaseActivity<ActivityDmChatRoomBinding>(
private val viewModel: DmChatRoomViewModel by viewModel()
private val messageAdapter = DmChatMessageAdapter { localId -> viewModel.retry(localId) }
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(this, layoutInflater) }
private lateinit var layoutManager: LinearLayoutManager
private var roomId: Long = 0L
private var creatorId: Long = 0L
@@ -119,6 +120,9 @@ class DmChatRoomActivity : BaseActivity<ActivityDmChatRoomBinding>(
viewModel.roomOpenedEventLiveData.observe(this) {
if (it.consume() == true) connectRealtimeIfStarted()
}
viewModel.isRealtimeConnectingLiveData.observe(this) { isConnecting ->
updateRealtimeLoadingDialog(isConnecting)
}
}
private fun bindContent(state: DmChatRoomUiState.Content) {
@@ -129,7 +133,7 @@ class DmChatRoomActivity : BaseActivity<ActivityDmChatRoomBinding>(
error(R.drawable.ic_placeholder_profile)
transformations(CircleCropTransformation())
}
binding.ivBackgroundProfile.loadUrl(state.opponentProfileImageUrl) {
binding.ivBackgroundProfile.loadUrl(SharedPreferenceManager.profileImage) {
placeholder(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()
if (text.isBlank()) return
viewModel.sendText(text)
if (viewModel.sendText(text)) {
binding.etMessage.setText("")
setSendButtonEnabled(false)
hideKeyboard(binding.etMessage)
}
}
private fun setSendButtonEnabled(enabled: Boolean) {
@@ -182,10 +186,12 @@ class DmChatRoomActivity : BaseActivity<ActivityDmChatRoomBinding>(
viewModel.connectRealtime()
}
private fun hideKeyboard(view: View) {
val imm = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.hideSoftInputFromWindow(view.windowToken, 0)
view.clearFocus()
private fun updateRealtimeLoadingDialog(isConnecting: Boolean) {
if (isConnecting) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
companion object {

View File

@@ -14,6 +14,8 @@ class DmChatRoomActivitySourceTest {
).readText()
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_CREATOR_ID"))
assertTrue(source.contains("fun newIntentByRoomId"))
@@ -26,6 +28,11 @@ class DmChatRoomActivitySourceTest {
assertTrue(source.contains("EditorInfo.IME_ACTION_SEND"))
assertTrue(source.contains("binding.ivSend.setOnClickListener"))
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("viewModel.loadOlderMessages()"))
assertTrue(source.contains("prependedMessageCountLiveData.observe(this)"))
@@ -35,6 +42,10 @@ class DmChatRoomActivitySourceTest {
assertTrue(source.contains("viewModel.connectRealtime()"))
assertTrue(source.contains("roomOpenedEventLiveData.observe(this)"))
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("viewModel.leaveRealtime()"))
assertFalse(source.contains("if (isStarted) viewModel.connectRealtime()"))
@@ -53,6 +64,80 @@ class DmChatRoomActivitySourceTest {
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 {
val candidates = listOf(File(relativePath), File("../$relativePath"))
return candidates.firstOrNull { it.exists() }