feat(live-room): 라이브룸 채팅 삭제 기능 구현을 반영한다

This commit is contained in:
2026-03-20 10:51:16 +09:00
parent b17a0dcc0e
commit a4ba3088b0
8 changed files with 482 additions and 26 deletions

View File

@@ -144,9 +144,14 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
private lateinit var imm: InputMethodManager
private val handler = Handler(Looper.getMainLooper())
private val chatAdapter = LiveRoomChatAdapter { userId ->
showLiveRoomUserProfileDialog(userId = userId)
}
private val chatAdapter = LiveRoomChatAdapter(
onClickProfile = { userId ->
showLiveRoomUserProfileDialog(userId = userId)
},
onLongClickNormalChat = { chat ->
onLongClickChat(chat)
}
)
private lateinit var layoutManager: LinearLayoutManager
private var rvChatBaseBottomMargin: Int? = null
@@ -1633,8 +1638,128 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
}
private fun onLongClickChat(chat: LiveRoomNormalChat) {
if (!isHost) {
return
}
LiveDialog(
activity = this,
layoutInflater = layoutInflater,
title = getString(R.string.screen_live_room_chat_delete_title),
desc = getString(
R.string.screen_live_room_chat_delete_message_format,
chat.nickname,
chat.chat
),
confirmButtonTitle = getString(R.string.confirm_delete_title),
confirmButtonClick = {
deleteChat(chat)
},
cancelButtonTitle = getString(R.string.cancel),
cancelButtonClick = {}
).show(screenWidth)
}
private fun createChatId(): String {
return "${SharedPreferenceManager.userId}_${System.currentTimeMillis()}_${Random.nextInt(1000, 9999)}"
}
private fun deleteChat(chat: LiveRoomNormalChat) {
if (chat.chatId.isNotBlank()) {
removeNormalChatById(chat.chatId)
} else {
removeNormalChatByUserAndMessage(
userId = chat.userId,
message = chat.chat
)
}
agora.sendRawMessageToGroup(
rawMessage = Gson().toJson(
LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.DELETE_CHAT,
message = chat.chat,
can = 0,
donationMessage = "",
chatId = chat.chatId.takeIf { it.isNotBlank() },
targetUserId = chat.userId
)
).toByteArray()
)
}
private fun deleteChatsByUser(userId: Long) {
if (userId <= 0L) {
return
}
removeChatsByUserId(userId)
agora.sendRawMessageToGroup(
rawMessage = Gson().toJson(
LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.DELETE_CHAT_BY_USER,
message = "",
can = 0,
donationMessage = "",
targetUserId = userId
)
).toByteArray()
)
}
private fun removeNormalChatById(chatId: String) {
if (chatId.isBlank()) {
return
}
val removed = chatAdapter.items.removeAll { chat ->
chat is LiveRoomNormalChat && chat.chatId == chatId
}
if (removed) {
invalidateChat()
}
}
private fun removeNormalChatByUserAndMessage(userId: Long, message: String) {
if (userId <= 0L || message.isBlank()) {
return
}
val targetIndex = chatAdapter.items.indexOfFirst { chat ->
chat is LiveRoomNormalChat && chat.userId == userId && chat.chat == message
}
if (targetIndex >= 0) {
chatAdapter.items.removeAt(targetIndex)
invalidateChat()
}
}
private fun removeChatsByUserId(userId: Long) {
if (userId <= 0L) {
return
}
val removed = chatAdapter.items.removeAll { chat ->
when (chat) {
is LiveRoomNormalChat -> chat.userId == userId
is LiveRoomDonationChat -> chat.memberId == userId
is LiveRoomRouletteDonationChat -> chat.memberId == userId
else -> false
}
}
if (removed) {
invalidateChat()
}
}
private fun kickOut(userId: Long) {
viewModel.kickOut(roomId, userId)
deleteChatsByUser(userId)
agora.sendRawMessageToPeer(
receiverUid = userId.toString(),
requestType = LiveRoomRequestType.KICK_OUT
@@ -1725,24 +1850,37 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
).show()
} else if (binding.etChat.text.isNotBlank() && nickname.isNotBlank() && profileUrl.isNotBlank()) {
val message = binding.etChat.text.toString()
val chatId = createChatId()
chatAdapter.items.add(
LiveRoomNormalChat(
userId = SharedPreferenceManager.userId,
profileUrl = profileUrl,
nickname = nickname,
rank = rank,
chat = message
chat = message,
chatId = chatId
)
)
invalidateChat()
agora.inputChat(message) {
Toast.makeText(
applicationContext,
getString(R.string.screen_live_room_connection_issue),
Toast.LENGTH_SHORT
).show()
}
agora.sendRawMessageToGroup(
rawMessage = Gson().toJson(
LiveRoomChatRawMessage(
type = LiveRoomChatRawMessageType.NORMAL_CHAT,
message = message,
can = 0,
donationMessage = "",
chatId = chatId
)
).toByteArray(),
onFailure = {
Toast.makeText(
applicationContext,
getString(R.string.screen_live_room_connection_issue),
Toast.LENGTH_SHORT
).show()
}
)
binding.etChat.setText("")
}
}
@@ -2093,6 +2231,81 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
}
LiveRoomChatRawMessageType.NORMAL_CHAT -> {
if (message.message.isBlank()) {
return
}
if (
memberId.toLong() != SharedPreferenceManager.userId &&
!viewModel.isNotBlockedMember(memberId.toLong())
) {
return
}
val chatId = message.chatId ?: ""
if (chatId.isBlank()) {
return
}
handler.post {
val alreadyExists = chatAdapter.items.any { chat ->
chat is LiveRoomNormalChat && chat.chatId == chatId
}
if (!alreadyExists) {
chatAdapter.items.add(
LiveRoomNormalChat(
userId = memberId.toLong(),
profileUrl = profileUrl,
nickname = nickname,
rank = rank,
chat = message.message,
chatId = chatId
)
)
invalidateChat()
}
}
}
LiveRoomChatRawMessageType.DELETE_CHAT -> {
if (!viewModel.isEqualToHostId(memberId.toInt())) {
return
}
val chatId = message.chatId
if (!chatId.isNullOrBlank()) {
handler.post {
removeNormalChatById(chatId)
}
return
}
val targetUserId = message.targetUserId ?: return
if (message.message.isBlank()) {
return
}
handler.post {
removeNormalChatByUserAndMessage(
userId = targetUserId,
message = message.message
)
}
}
LiveRoomChatRawMessageType.DELETE_CHAT_BY_USER -> {
if (!viewModel.isEqualToHostId(memberId.toInt())) {
return
}
val targetUserId = message.targetUserId ?: return
handler.post {
removeChatsByUserId(targetUserId)
}
}
LiveRoomChatRawMessageType.DONATION -> {
handler.post {
chatAdapter.items.add(
@@ -2149,6 +2362,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
handler.post {
chatAdapter.items.add(
LiveRoomRouletteDonationChat(
memberId = memberId.toLong(),
profileUrl = profileUrl,
nickname = nickname,
rouletteResult = message.message
@@ -3789,6 +4003,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
handler.post {
chatAdapter.items.add(
LiveRoomRouletteDonationChat(
memberId = SharedPreferenceManager.userId,
profileUrl = SharedPreferenceManager.profileImage,
nickname = SharedPreferenceManager.nickname,
rouletteResult = randomItem

View File

@@ -39,7 +39,8 @@ abstract class LiveRoomChat {
abstract fun bind(
context: Context,
binding: ViewBinding,
onClickProfile: ((Long) -> Unit)? = null
onClickProfile: ((Long) -> Unit)? = null,
onLongClickChat: ((LiveRoomNormalChat) -> Unit)? = null
)
}
@@ -48,7 +49,12 @@ data class LiveRoomJoinChat(
val nickname: String
) : LiveRoomChat() {
override var type = LiveRoomChatType.JOIN
override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) {
override fun bind(
context: Context,
binding: ViewBinding,
onClickProfile: ((Long) -> Unit)?,
onLongClickChat: ((LiveRoomNormalChat) -> Unit)?
) {
(binding as ItemLiveRoomJoinChatBinding).tvJoin.setTextColor(
ContextCompat.getColor(context, R.color.color_eeeeee)
)
@@ -88,7 +94,12 @@ data class LiveRoomSystemNoticeChat(
val message: String
) : LiveRoomChat() {
override var type = LiveRoomChatType.JOIN
override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) {
override fun bind(
context: Context,
binding: ViewBinding,
onClickProfile: ((Long) -> Unit)?,
onLongClickChat: ((LiveRoomNormalChat) -> Unit)?
) {
val itemBinding = binding as ItemLiveRoomJoinChatBinding
itemBinding.tvJoin.setTextColor(
ContextCompat.getColor(context, R.color.color_eeeeee)
@@ -104,9 +115,15 @@ data class LiveRoomNormalChat(
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("nickname") val nickname: String,
@SerializedName("rank") val rank: Int,
@SerializedName("chat") val chat: String
@SerializedName("chat") val chat: String,
@SerializedName("chatId") val chatId: String = ""
) : LiveRoomChat() {
override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) {
override fun bind(
context: Context,
binding: ViewBinding,
onClickProfile: ((Long) -> Unit)?,
onLongClickChat: ((LiveRoomNormalChat) -> Unit)?
) {
val itemBinding = binding as ItemLiveRoomChatBinding
itemBinding.ivProfile.load(profileUrl) {
crossfade(true)
@@ -222,6 +239,16 @@ data class LiveRoomNormalChat(
} else {
itemBinding.llMessageBg.setBackgroundResource(R.drawable.bg_round_corner_3_3_99000000)
}
itemBinding.root.setOnLongClickListener {
if (onLongClickChat != null) {
onLongClickChat(this@LiveRoomNormalChat)
true
} else {
false
}
}
itemBinding.ivCan.visibility = View.GONE
itemBinding.tvDonationMessage.visibility = View.GONE
itemBinding.root.setBackgroundResource(0)
@@ -239,7 +266,12 @@ data class LiveRoomDonationChat(
@SerializedName("donationMessage") val donationMessage: String,
val isSecret: Boolean = false
) : LiveRoomChat() {
override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) {
override fun bind(
context: Context,
binding: ViewBinding,
onClickProfile: ((Long) -> Unit)?,
onLongClickChat: ((LiveRoomNormalChat) -> Unit)?
) {
val itemBinding = binding as ItemLiveRoomChatBinding
val defaultProfileSize = 33.3f.dpToPx().toInt()
val defaultCrownSize = 16.7f.dpToPx().toInt()
@@ -324,6 +356,7 @@ data class LiveRoomDonationChat(
itemBinding.llMessageBg.setPadding(0)
itemBinding.llMessageBg.background = null
itemBinding.root.setOnLongClickListener(null)
if (isSecret) {
itemBinding.root.setBackgroundResource(R.drawable.bg_round_corner_6_7_cc59548f)
@@ -366,11 +399,17 @@ data class LiveRoomDonationChat(
@Keep
data class LiveRoomRouletteDonationChat(
@SerializedName("memberId") val memberId: Long,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("nickname") val nickname: String,
@SerializedName("rouletteResult") val rouletteResult: String
) : LiveRoomChat() {
override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) {
override fun bind(
context: Context,
binding: ViewBinding,
onClickProfile: ((Long) -> Unit)?,
onLongClickChat: ((LiveRoomNormalChat) -> Unit)?
) {
val itemBinding = binding as ItemLiveRoomChatBinding
val defaultProfileSize = 33.3f.dpToPx().toInt()
val defaultCrownSize = 16.7f.dpToPx().toInt()
@@ -433,6 +472,7 @@ data class LiveRoomRouletteDonationChat(
itemBinding.llMessageBg.setPadding(0)
itemBinding.llMessageBg.background = null
itemBinding.root.setOnLongClickListener(null)
itemBinding.root.setBackgroundResource(R.drawable.bg_round_corner_6_7_ccc25264)
itemBinding.root.setPadding(33)

View File

@@ -10,7 +10,8 @@ import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDonationStatusChatBindin
import kr.co.vividnext.sodalive.databinding.ItemLiveRoomJoinChatBinding
class LiveRoomChatAdapter(
private val onClickProfile: (Long) -> Unit
private val onClickProfile: (Long) -> Unit,
private val onLongClickNormalChat: ((LiveRoomNormalChat) -> Unit)? = null
) : RecyclerView.Adapter<LiveRoomChatViewHolder>() {
val items = mutableListOf<LiveRoomChat>()
@@ -53,7 +54,7 @@ class LiveRoomChatAdapter(
}
override fun onBindViewHolder(holder: LiveRoomChatViewHolder, position: Int) {
holder.bind(items[position], onClickProfile)
holder.bind(items[position], onClickProfile, onLongClickNormalChat)
}
override fun getItemCount() = items.count()
@@ -65,7 +66,11 @@ class LiveRoomChatAdapter(
abstract class LiveRoomChatViewHolder(binding: ViewBinding) :
RecyclerView.ViewHolder(binding.root) {
abstract fun bind(chat: LiveRoomChat, onClickProfile: ((Long) -> Unit)? = null)
abstract fun bind(
chat: LiveRoomChat,
onClickProfile: ((Long) -> Unit)? = null,
onLongClickChat: ((LiveRoomNormalChat) -> Unit)? = null
)
}
class LiveRoomNormalChatViewHolder(
@@ -74,8 +79,9 @@ class LiveRoomNormalChatViewHolder(
) : LiveRoomChatViewHolder(binding) {
override fun bind(
chat: LiveRoomChat,
onClickProfile: ((Long) -> Unit)?
) = chat.bind(context, binding, onClickProfile)
onClickProfile: ((Long) -> Unit)?,
onLongClickChat: ((LiveRoomNormalChat) -> Unit)?
) = chat.bind(context, binding, onClickProfile, onLongClickChat)
}
class LiveRoomDonationStatusChatViewHolder(
@@ -84,7 +90,8 @@ class LiveRoomDonationStatusChatViewHolder(
) : LiveRoomChatViewHolder(binding) {
override fun bind(
chat: LiveRoomChat,
onClickProfile: ((Long) -> Unit)?
onClickProfile: ((Long) -> Unit)?,
onLongClickChat: ((LiveRoomNormalChat) -> Unit)?
) = chat.bind(context, binding)
}
@@ -94,6 +101,7 @@ class LiveRoomJoinChatViewHolder(
) : LiveRoomChatViewHolder(binding) {
override fun bind(
chat: LiveRoomChat,
onClickProfile: ((Long) -> Unit)?
onClickProfile: ((Long) -> Unit)?,
onLongClickChat: ((LiveRoomNormalChat) -> Unit)?
) = chat.bind(context, binding)
}

View File

@@ -13,7 +13,9 @@ data class LiveRoomChatRawMessage(
@SerializedName("signatureImageUrl") val signatureImageUrl: String? = null,
@SerializedName("donationMessage") val donationMessage: String?,
@SerializedName("isActiveRoulette") val isActiveRoulette: Boolean? = null,
@SerializedName("isChatFrozen") val isChatFrozen: Boolean? = null
@SerializedName("isChatFrozen") val isChatFrozen: Boolean? = null,
@SerializedName("chatId") val chatId: String? = null,
@SerializedName("targetUserId") val targetUserId: Long? = null
)
enum class LiveRoomChatRawMessageType {
@@ -35,6 +37,15 @@ enum class LiveRoomChatRawMessageType {
@SerializedName("TOGGLE_CHAT_FREEZE")
TOGGLE_CHAT_FREEZE,
@SerializedName("NORMAL_CHAT")
NORMAL_CHAT,
@SerializedName("DELETE_CHAT")
DELETE_CHAT,
@SerializedName("DELETE_CHAT_BY_USER")
DELETE_CHAT_BY_USER,
@SerializedName("ROULETTE_DONATION")
ROULETTE_DONATION,

View File

@@ -467,6 +467,8 @@
<string name="screen_live_room_chat_freeze_warning">Chat is frozen. You cannot type right now.</string>
<string name="screen_live_room_chat_freeze_started">Chat has been frozen.</string>
<string name="screen_live_room_chat_freeze_ended">Chat freeze has been disabled.</string>
<string name="screen_live_room_chat_delete_title">Delete chat</string>
<string name="screen_live_room_chat_delete_message_format">%1$s: %2$s</string>
<string name="screen_live_room_signature_off_label">Sign OFF</string>
<string name="screen_live_room_v2v_signature_off_label">Caption OFF</string>
<string name="screen_live_room_v2v_signature_on_label">Caption ON</string>

View File

@@ -466,6 +466,8 @@
<string name="screen_live_room_chat_freeze_warning">チャットが凍結中のため入力できません。</string>
<string name="screen_live_room_chat_freeze_started">チャットを凍結しました。</string>
<string name="screen_live_room_chat_freeze_ended">チャット凍結を解除しました。</string>
<string name="screen_live_room_chat_delete_title">チャット削除</string>
<string name="screen_live_room_chat_delete_message_format">%1$s: %2$s</string>
<string name="screen_live_room_signature_off_label">シグ OFF</string>
<string name="screen_live_room_v2v_signature_off_label">字幕 OFF</string>
<string name="screen_live_room_v2v_signature_on_label">字幕 ON</string>

View File

@@ -466,6 +466,8 @@
<string name="screen_live_room_chat_freeze_warning">채팅창이 얼려져 있어 입력할 수 없습니다.</string>
<string name="screen_live_room_chat_freeze_started">채팅창을 얼렸습니다.</string>
<string name="screen_live_room_chat_freeze_ended">채팅창 얼리기를 해제했습니다</string>
<string name="screen_live_room_chat_delete_title">채팅 삭제</string>
<string name="screen_live_room_chat_delete_message_format">%1$s: %2$s</string>
<string name="screen_live_room_signature_off_label">시그 OFF</string>
<string name="screen_live_room_v2v_signature_off_label">자막 OFF</string>
<string name="screen_live_room_v2v_signature_on_label">자막 ON</string>