feat(chat-room): 채팅 쿼터 광고/캔 충전 흐름을 연결한다
This commit is contained in:
@@ -14,21 +14,31 @@ 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.load
|
import coil.load
|
||||||
|
import com.yandex.mobile.ads.common.AdError
|
||||||
|
import com.yandex.mobile.ads.common.AdRequestConfiguration
|
||||||
|
import com.yandex.mobile.ads.common.AdRequestError
|
||||||
|
import com.yandex.mobile.ads.common.ImpressionData
|
||||||
|
import com.yandex.mobile.ads.rewarded.Reward
|
||||||
|
import com.yandex.mobile.ads.rewarded.RewardedAd
|
||||||
|
import com.yandex.mobile.ads.rewarded.RewardedAdEventListener
|
||||||
|
import com.yandex.mobile.ads.rewarded.RewardedAdLoadListener
|
||||||
|
import com.yandex.mobile.ads.rewarded.RewardedAdLoader
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import kr.co.vividnext.sodalive.BuildConfig
|
||||||
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.base.SodaDialog
|
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||||
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterType
|
import kr.co.vividnext.sodalive.chat.character.detail.detail.CharacterType
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
|
|
||||||
import kr.co.vividnext.sodalive.databinding.ActivityChatRoomBinding
|
import kr.co.vividnext.sodalive.databinding.ActivityChatRoomBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||||
|
import kr.co.vividnext.sodalive.chat.talk.room.quota.ChatRoomQuotaCanOption
|
||||||
|
import kr.co.vividnext.sodalive.chat.talk.room.quota.ChatRoomQuotaChargeType
|
||||||
import kr.co.vividnext.sodalive.user.UserRepository
|
import kr.co.vividnext.sodalive.user.UserRepository
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
||||||
ActivityChatRoomBinding::inflate
|
ActivityChatRoomBinding::inflate
|
||||||
@@ -48,13 +58,18 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
private var hasMoreMessages: Boolean = true // Repository 연동 시 서버 값으로 갱신 예정
|
private var hasMoreMessages: Boolean = true // Repository 연동 시 서버 값으로 갱신 예정
|
||||||
private var nextCursor: Long? = null // 가장 오래된 메시지의 timestamp 등
|
private var nextCursor: Long? = null // 가장 오래된 메시지의 timestamp 등
|
||||||
|
|
||||||
// 쿼터/카운트다운 상태
|
// 쿼터/광고 상태
|
||||||
private var quotaTimer: android.os.CountDownTimer? = null
|
private var currentTotalRemaining: Int = Int.MAX_VALUE
|
||||||
|
private var chatQuotaRewardedAdLoader: RewardedAdLoader? = null
|
||||||
|
private var chatQuotaRewardedAd: RewardedAd? = null
|
||||||
|
private var hasRewardHandledForCurrentAd: Boolean = false
|
||||||
|
private var isQuotaPurchaseInFlight: Boolean = false
|
||||||
|
private var isChatQuotaRewardedAdShowing: Boolean = false
|
||||||
|
|
||||||
// 5.3 헤더 데이터 (7.x 연동 전까지는 nullable 보관)
|
// 5.3 헤더 데이터 (7.x 연동 전까지는 nullable 보관)
|
||||||
private var characterInfo: CharacterInfo? = null
|
private var characterInfo: CharacterInfo? = null
|
||||||
|
|
||||||
private fun noticePrefKey(roomId: Long) = "chat_notice_hidden_room_${roomId}"
|
private fun noticePrefKey(roomId: Long): String = "chat_notice_hidden_room_$roomId"
|
||||||
private fun isNoticeHidden(): Boolean = ChatRoomPreferenceManager.getBoolean(noticePrefKey(roomId), false)
|
private fun isNoticeHidden(): Boolean = ChatRoomPreferenceManager.getBoolean(noticePrefKey(roomId), false)
|
||||||
private fun setNoticeHidden(hidden: Boolean) {
|
private fun setNoticeHidden(hidden: Boolean) {
|
||||||
ChatRoomPreferenceManager.putBoolean(noticePrefKey(roomId), hidden)
|
ChatRoomPreferenceManager.putBoolean(noticePrefKey(roomId), hidden)
|
||||||
@@ -132,7 +147,9 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
// 타입 배지 텍스트 및 배경
|
// 타입 배지 텍스트 및 배경
|
||||||
val (badgeText, badgeBg) = when (info.characterType) {
|
val (badgeText, badgeBg) = when (info.characterType) {
|
||||||
CharacterType.CLONE -> getString(R.string.chat_character_type_clone) to R.drawable.bg_character_status_clone
|
CharacterType.CLONE -> getString(R.string.chat_character_type_clone) to R.drawable.bg_character_status_clone
|
||||||
CharacterType.CHARACTER -> getString(R.string.chat_character_type_character) to R.drawable.bg_character_status_character
|
CharacterType.CHARACTER -> {
|
||||||
|
getString(R.string.chat_character_type_character) to R.drawable.bg_character_status_character
|
||||||
|
}
|
||||||
}
|
}
|
||||||
binding.tvCharacterTypeBadge.text = badgeText
|
binding.tvCharacterTypeBadge.text = badgeText
|
||||||
binding.tvCharacterTypeBadge.setBackgroundResource(badgeBg)
|
binding.tvCharacterTypeBadge.setBackgroundResource(badgeBg)
|
||||||
@@ -198,7 +215,11 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPurchaseQuota() {
|
override fun onPurchaseQuota() {
|
||||||
onPurchaseQuotaClicked()
|
onQuotaNoticeAction(ChatQuotaNoticeAction.REWARDED_AD)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQuotaNoticeAction(action: ChatQuotaNoticeAction) {
|
||||||
|
this@ChatRoomActivity.onQuotaNoticeAction(action)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -381,7 +402,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 응답에 포함된 쿼터 상태로 UI 갱신
|
// 응답에 포함된 쿼터 상태로 UI 갱신
|
||||||
updateQuotaUi(response.nextRechargeAtEpoch)
|
updateQuotaUi(response.totalRemaining)
|
||||||
}, { error ->
|
}, { error ->
|
||||||
// 실패: 타이핑 인디케이터 제거 및 FAILED로 업데이트
|
// 실패: 타이핑 인디케이터 제거 및 FAILED로 업데이트
|
||||||
chatAdapter.hideTypingIndicator()
|
chatAdapter.hideTypingIndicator()
|
||||||
@@ -461,17 +482,34 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
// endregion 6.2 Send flow
|
// endregion 6.2 Send flow
|
||||||
|
|
||||||
// region Quota handling
|
// region Quota handling
|
||||||
private fun onPurchaseQuotaClicked() {
|
private fun onQuotaNoticeAction(action: ChatQuotaNoticeAction) {
|
||||||
|
if (isQuotaPurchaseInFlight) return
|
||||||
|
|
||||||
|
when (action) {
|
||||||
|
ChatQuotaNoticeAction.REWARDED_AD -> showChatQuotaRewardedAd()
|
||||||
|
ChatQuotaNoticeAction.PURCHASE_10_CAN -> purchaseChatQuota(ChatRoomQuotaCanOption.CAN_10)
|
||||||
|
ChatQuotaNoticeAction.PURCHASE_20_CAN -> purchaseChatQuota(ChatRoomQuotaCanOption.CAN_20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun purchaseChatQuota(canOption: ChatRoomQuotaCanOption) {
|
||||||
|
if (isQuotaPurchaseInFlight) return
|
||||||
|
|
||||||
val token = "Bearer ${SharedPreferenceManager.token}"
|
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
isQuotaPurchaseInFlight = true
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
chatRepository.purchaseChatQuota(roomId, token)
|
chatRepository.purchaseChatQuota(
|
||||||
|
roomId = roomId,
|
||||||
|
token = token,
|
||||||
|
chargeType = ChatRoomQuotaChargeType.CAN,
|
||||||
|
canOption = canOption
|
||||||
|
)
|
||||||
|
.doFinally { isQuotaPurchaseInFlight = false }
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe({ resp ->
|
.subscribe({ resp ->
|
||||||
// 쿼터 UI 갱신
|
updateQuotaUi(resp.totalRemaining)
|
||||||
updateQuotaUi(resp.nextRechargeAtEpoch)
|
|
||||||
|
|
||||||
// 결제 성공 시 로컬 캔 차감(30캔) 및 헤더 배지 즉시 반영
|
val newCan = (SharedPreferenceManager.can - canOption.needCan).coerceAtLeast(0)
|
||||||
val newCan = (SharedPreferenceManager.can - 30).coerceAtLeast(0)
|
|
||||||
SharedPreferenceManager.can = newCan
|
SharedPreferenceManager.can = newCan
|
||||||
binding.tvCanBadge.text = newCan.moneyFormat()
|
binding.tvCanBadge.text = newCan.moneyFormat()
|
||||||
}, { err ->
|
}, { err ->
|
||||||
@@ -480,33 +518,47 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateQuotaUi(nextRechargeAtEpoch: Long?) {
|
private fun purchaseRewardedChatQuota() {
|
||||||
if (nextRechargeAtEpoch != null) {
|
if (isQuotaPurchaseInFlight) return
|
||||||
// 입력창 숨김 및 안내 표시 + 카운트다운 시작
|
|
||||||
|
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
isQuotaPurchaseInFlight = true
|
||||||
|
compositeDisposable.add(
|
||||||
|
chatRepository.purchaseChatQuota(
|
||||||
|
roomId = roomId,
|
||||||
|
token = token,
|
||||||
|
chargeType = ChatRoomQuotaChargeType.AD
|
||||||
|
)
|
||||||
|
.doFinally { isQuotaPurchaseInFlight = false }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe({ resp ->
|
||||||
|
updateQuotaUi(resp.totalRemaining)
|
||||||
|
}, { err ->
|
||||||
|
showToast(err.message ?: getString(R.string.chat_quota_purchase_failed))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateQuotaUi(totalRemaining: Int) {
|
||||||
|
currentTotalRemaining = totalRemaining
|
||||||
|
|
||||||
|
if (totalRemaining <= 0) {
|
||||||
binding.inputContainer.isVisible = false
|
binding.inputContainer.isVisible = false
|
||||||
val timeText = formatEpochToHms(nextRechargeAtEpoch)
|
ensureQuotaNoticeShown()
|
||||||
ensureQuotaNoticeShown(timeText)
|
|
||||||
startQuotaCountdown(nextRechargeAtEpoch)
|
|
||||||
} else {
|
} else {
|
||||||
// 입력창 표시 및 안내 제거
|
|
||||||
binding.inputContainer.isVisible = true
|
binding.inputContainer.isVisible = true
|
||||||
stopQuotaCountdown()
|
|
||||||
ensureQuotaNoticeRemoved()
|
ensureQuotaNoticeRemoved()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (totalRemaining <= 1) {
|
||||||
|
preloadChatQuotaRewardedAd()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureQuotaNoticeShown(timeText: String?) {
|
private fun ensureQuotaNoticeShown() {
|
||||||
val idx = items.indexOfLast { it is ChatListItem.QuotaNotice }
|
val idx = items.indexOfLast { it is ChatListItem.QuotaNotice }
|
||||||
val newItem = ChatListItem.QuotaNotice(timeText = timeText)
|
if (idx >= 0) return
|
||||||
if (idx >= 0) {
|
appendMessage(ChatListItem.QuotaNotice)
|
||||||
val old = items[idx] as ChatListItem.QuotaNotice
|
|
||||||
// 동일 시간 텍스트면 불필요한 갱신 회피
|
|
||||||
if (old.timeText == newItem.timeText) return
|
|
||||||
items[idx] = newItem
|
|
||||||
chatAdapter.setItems(items)
|
|
||||||
} else {
|
|
||||||
appendMessage(newItem)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureQuotaNoticeRemoved() {
|
private fun ensureQuotaNoticeRemoved() {
|
||||||
@@ -517,81 +569,104 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startQuotaCountdown(targetEpoch: Long?) {
|
private val chatQuotaRewardedAdLoadListener = object : RewardedAdLoadListener {
|
||||||
stopQuotaCountdown()
|
override fun onAdLoaded(rewardedAd: RewardedAd) {
|
||||||
if (targetEpoch == null) return
|
chatQuotaRewardedAdLoader = null
|
||||||
val targetMs = if (targetEpoch < 1_000_000_000_000L) targetEpoch * 1000 else targetEpoch
|
clearChatQuotaRewardedAd()
|
||||||
val now = System.currentTimeMillis()
|
chatQuotaRewardedAd = rewardedAd
|
||||||
val duration = targetMs - now
|
hasRewardHandledForCurrentAd = false
|
||||||
if (duration <= 0) {
|
}
|
||||||
checkQuotaStatus()
|
|
||||||
|
override fun onAdFailedToLoad(adRequestError: AdRequestError) {
|
||||||
|
chatQuotaRewardedAdLoader = null
|
||||||
|
chatQuotaRewardedAd = null
|
||||||
|
hasRewardHandledForCurrentAd = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val chatQuotaRewardedAdEventListener = object : RewardedAdEventListener {
|
||||||
|
override fun onAdShown() {
|
||||||
|
isChatQuotaRewardedAdShowing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAdFailedToShow(adError: AdError) {
|
||||||
|
isChatQuotaRewardedAdShowing = false
|
||||||
|
clearChatQuotaRewardedAd()
|
||||||
|
preloadChatQuotaRewardedAd(force = true)
|
||||||
|
showToast(getString(R.string.chat_quota_rewarded_ad_unavailable))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAdDismissed() {
|
||||||
|
isChatQuotaRewardedAdShowing = false
|
||||||
|
val rewardHandled = hasRewardHandledForCurrentAd
|
||||||
|
clearChatQuotaRewardedAd()
|
||||||
|
if (!rewardHandled && currentTotalRemaining <= 1) {
|
||||||
|
preloadChatQuotaRewardedAd(force = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAdClicked() = Unit
|
||||||
|
|
||||||
|
override fun onAdImpression(impressionData: ImpressionData?) = Unit
|
||||||
|
|
||||||
|
override fun onRewarded(reward: Reward) {
|
||||||
|
if (hasRewardHandledForCurrentAd) return
|
||||||
|
hasRewardHandledForCurrentAd = true
|
||||||
|
purchaseRewardedChatQuota()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun preloadChatQuotaRewardedAd(force: Boolean = false) {
|
||||||
|
if (!force && (chatQuotaRewardedAd != null || chatQuotaRewardedAdLoader != null)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
quotaTimer = object : android.os.CountDownTimer(duration, 1000L) {
|
|
||||||
override fun onTick(millisUntilFinished: Long) {
|
|
||||||
val timeText =
|
|
||||||
formatMillisToHms((millisUntilFinished + DISPLAY_FUDGE_MS).coerceAtLeast(0L))
|
|
||||||
// 안내 갱신
|
|
||||||
ensureQuotaNoticeShown(timeText)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinish() {
|
val adUnitId = BuildConfig.YANDEX_REWARDED_CHAT_QUOTA_AD_UNIT_ID
|
||||||
ensureQuotaNoticeShown(
|
if (adUnitId.isBlank()) return
|
||||||
SodaLiveApplicationHolder.get()
|
|
||||||
.getString(R.string.screen_audio_content_detail_time_default)
|
|
||||||
)
|
|
||||||
checkQuotaStatus()
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopQuotaCountdown() {
|
chatQuotaRewardedAdLoader?.setAdLoadListener(null)
|
||||||
quotaTimer?.cancel()
|
chatQuotaRewardedAdLoader = RewardedAdLoader(this).apply {
|
||||||
quotaTimer = null
|
setAdLoadListener(chatQuotaRewardedAdLoadListener)
|
||||||
}
|
}
|
||||||
|
chatQuotaRewardedAdLoader?.loadAd(
|
||||||
private fun checkQuotaStatus() {
|
AdRequestConfiguration.Builder(adUnitId).build()
|
||||||
val token = "Bearer ${SharedPreferenceManager.token}"
|
|
||||||
compositeDisposable.add(
|
|
||||||
chatRepository.getChatQuotaStatus(roomId, token)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({ resp ->
|
|
||||||
updateQuotaUi(resp.nextRechargeAtEpoch)
|
|
||||||
}, { /* 무시: 다음 틱에 재시도 가능 */ })
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatEpochToHms(epoch: Long?): String? {
|
private fun showChatQuotaRewardedAd() {
|
||||||
if (epoch == null) return null
|
if (isQuotaPurchaseInFlight || isChatQuotaRewardedAdShowing) return
|
||||||
val ms = if (epoch < 1_000_000_000_000L) epoch * 1000 else epoch
|
|
||||||
val remain = ms - System.currentTimeMillis()
|
val rewardedAd = chatQuotaRewardedAd
|
||||||
val displayMs = (remain + DISPLAY_FUDGE_MS).coerceAtLeast(0L)
|
if (rewardedAd == null) {
|
||||||
return if (displayMs > 0L) {
|
preloadChatQuotaRewardedAd(force = true)
|
||||||
formatMillisToHms(displayMs)
|
showToast(getString(R.string.chat_quota_rewarded_ad_unavailable))
|
||||||
} else {
|
return
|
||||||
SodaLiveApplicationHolder.get()
|
}
|
||||||
.getString(R.string.screen_audio_content_detail_time_default)
|
|
||||||
|
hasRewardHandledForCurrentAd = false
|
||||||
|
isChatQuotaRewardedAdShowing = true
|
||||||
|
rewardedAd.setAdEventListener(chatQuotaRewardedAdEventListener)
|
||||||
|
runCatching {
|
||||||
|
rewardedAd.show(this)
|
||||||
|
}.onFailure {
|
||||||
|
isChatQuotaRewardedAdShowing = false
|
||||||
|
clearChatQuotaRewardedAd()
|
||||||
|
preloadChatQuotaRewardedAd(force = true)
|
||||||
|
showToast(getString(R.string.chat_quota_rewarded_ad_unavailable))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatMillisToHms(ms: Long): String {
|
private fun clearChatQuotaRewardedAd() {
|
||||||
var totalSec = (ms / 1000).coerceAtLeast(0)
|
chatQuotaRewardedAd?.setAdEventListener(null)
|
||||||
val hours = totalSec / 3600
|
chatQuotaRewardedAd = null
|
||||||
totalSec %= 3600
|
hasRewardHandledForCurrentAd = false
|
||||||
val minutes = totalSec / 60
|
isChatQuotaRewardedAdShowing = false
|
||||||
val seconds = totalSec % 60
|
|
||||||
return String.format(
|
|
||||||
locale = Locale.getDefault(),
|
|
||||||
"%02d:%02d:%02d",
|
|
||||||
hours,
|
|
||||||
minutes,
|
|
||||||
seconds
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
private fun releaseChatQuotaRewardedAd() {
|
||||||
stopQuotaCountdown()
|
chatQuotaRewardedAdLoader?.setAdLoadListener(null)
|
||||||
super.onDestroy()
|
chatQuotaRewardedAdLoader = null
|
||||||
|
clearChatQuotaRewardedAd()
|
||||||
}
|
}
|
||||||
// endregion Quota handling
|
// endregion Quota handling
|
||||||
|
|
||||||
@@ -605,11 +680,17 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
.subscribe({ localList ->
|
.subscribe({ localList ->
|
||||||
if (localList.isNotEmpty() && items.isEmpty()) {
|
if (localList.isNotEmpty() && items.isEmpty()) {
|
||||||
val localItems = localList
|
val localItems = localList
|
||||||
.sortedWith(compareBy<ChatMessage> { it.createdAt }.thenBy { it.messageId }
|
.sortedWith(
|
||||||
.thenBy { it.localId ?: "" })
|
compareBy<ChatMessage> { it.createdAt }
|
||||||
|
.thenBy { it.messageId }
|
||||||
|
.thenBy { it.localId ?: "" }
|
||||||
|
)
|
||||||
.map { msg ->
|
.map { msg ->
|
||||||
if (msg.mine) ChatListItem.UserMessage(msg)
|
if (msg.mine) {
|
||||||
else ChatListItem.AiMessage(msg, characterInfo?.name)
|
ChatListItem.UserMessage(msg)
|
||||||
|
} else {
|
||||||
|
ChatListItem.AiMessage(msg, characterInfo?.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
items.clear()
|
items.clear()
|
||||||
items.addAll(localItems)
|
items.addAll(localItems)
|
||||||
@@ -665,7 +746,7 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
isLoading = false
|
isLoading = false
|
||||||
|
|
||||||
// 쿼터 UI 갱신
|
// 쿼터 UI 갱신
|
||||||
updateQuotaUi(response.nextRechargeAtEpoch)
|
updateQuotaUi(response.totalRemaining)
|
||||||
|
|
||||||
// 7.3: 오래된 메시지 정리(백그라운드)
|
// 7.3: 오래된 메시지 정리(백그라운드)
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
@@ -760,8 +841,11 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
.map { it.toDomain() }
|
.map { it.toDomain() }
|
||||||
.filter { !existingIds.contains(it.messageId) }
|
.filter { !existingIds.contains(it.messageId) }
|
||||||
.map { domain ->
|
.map { domain ->
|
||||||
if (domain.mine) ChatListItem.UserMessage(domain)
|
if (domain.mine) {
|
||||||
else ChatListItem.AiMessage(domain, characterInfo?.name)
|
ChatListItem.UserMessage(domain)
|
||||||
|
} else {
|
||||||
|
ChatListItem.AiMessage(domain, characterInfo?.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 상단에 추가하면서 스크롤 위치 보정
|
// 상단에 추가하면서 스크롤 위치 보정
|
||||||
@@ -955,8 +1039,12 @@ class ChatRoomActivity : BaseActivity<ActivityChatRoomBinding>(
|
|||||||
).show(resources.displayMetrics.widthPixels)
|
).show(resources.displayMetrics.widthPixels)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
releaseChatQuotaRewardedAd()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DISPLAY_FUDGE_MS: Long = 5_000L
|
|
||||||
const val EXTRA_ROOM_ID: String = "extra_room_id"
|
const val EXTRA_ROOM_ID: String = "extra_room_id"
|
||||||
|
|
||||||
fun newIntent(context: Context, roomId: Long): Intent {
|
fun newIntent(context: Context, roomId: Long): Intent {
|
||||||
|
|||||||
Reference in New Issue
Block a user