diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt index 9cb8320..0c8c737 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt @@ -63,6 +63,7 @@ import kr.co.vividnext.sodalive.live.room.chat.LiveRoomDonationChat import kr.co.vividnext.sodalive.live.room.chat.LiveRoomDonationStatusChat import kr.co.vividnext.sodalive.live.room.chat.LiveRoomJoinChat import kr.co.vividnext.sodalive.live.room.chat.LiveRoomNormalChat +import kr.co.vividnext.sodalive.live.room.chat.LiveRoomRouletteDonationChat import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageDialog import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewModel @@ -880,7 +881,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB RoulettePreviewDialog( activity = this, preview = it, - onClickSpin = {}, + onClickSpin = { spinRoulette() }, layoutInflater = layoutInflater ).show() } @@ -1248,6 +1249,40 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } } + private fun spinRoulette() { + viewModel.spinRoulette(roomId = roomId) { can, randomlySelectedItem -> + val rawMessage = "[$randomlySelectedItem] 당첨!" + val rouletteRawMessage = Gson().toJson( + LiveRoomChatRawMessage( + type = LiveRoomChatRawMessageType.ROULETTE_DONATION, + message = rawMessage, + can = can, + donationMessage = "", + ) + ) + + agora.sendRawMessageToGroup( + rawMessage = rouletteRawMessage.toByteArray(), + onSuccess = { + handler.post { + chatAdapter.items.add( + LiveRoomRouletteDonationChat( + profileUrl = SharedPreferenceManager.profileImage, + nickname = SharedPreferenceManager.nickname, + chat = rawMessage + ) + ) + invalidateChat() + viewModel.addDonationCan(can) + } + }, + onFailure = { + viewModel.refundRouletteDonation(roomId) + } + ) + } + } + private fun joinChannel(roomInfo: GetRoomInfoResponse) { loadingDialog.show(width = screenWidth, message = "라이브에 입장하고 있습니다.") @@ -1330,6 +1365,20 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB ) } } + + LiveRoomChatRawMessageType.ROULETTE_DONATION -> { + handler.post { + chatAdapter.items.add( + LiveRoomRouletteDonationChat( + profileUrl = profileUrl, + nickname = nickname, + chat = rawMessage.message + ) + ) + invalidateChat() + viewModel.addDonationCan(rawMessage.can) + } + } } } else { val chat = message.text diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt index f71f7b1..698e79e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt @@ -24,6 +24,7 @@ import kr.co.vividnext.sodalive.live.roulette.RouletteItem import kr.co.vividnext.sodalive.live.roulette.RoulettePreview import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewItem import kr.co.vividnext.sodalive.live.roulette.RouletteRepository +import kr.co.vividnext.sodalive.live.roulette.SpinRouletteRequest import kr.co.vividnext.sodalive.report.ReportRepository import kr.co.vividnext.sodalive.report.ReportRequest import kr.co.vividnext.sodalive.report.ReportType @@ -33,6 +34,7 @@ import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.io.File +import kotlin.random.Random class LiveRoomViewModel( private val repository: LiveRepository, @@ -823,6 +825,108 @@ class LiveRoomViewModel( } } + fun spinRoulette(roomId: Long, complete: (Int, String) -> Unit) { + if (!_isLoading.value!!) { + _isLoading.value = true + compositeDisposable.add( + rouletteRepository.spinRoulette( + request = SpinRouletteRequest(roomId = roomId), + token = "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + + val data = it.data + if ( + it.success && + data != null && + data.isActive && + data.items.isNotEmpty() + ) { + SharedPreferenceManager.can -= data.can + randomSelectRouletteItem(data.can, data.items, complete) + } else { + val message = it.message ?: "룰렛을 사용할 수 없습니다. 다시 시도해 주세요." + _toastLiveData.postValue(message) + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("룰렛을 사용할 수 없습니다. 다시 시도해 주세요.") + } + ) + ) + } + } + + fun refundRouletteDonation(roomId: Long) { + _isLoading.postValue(true) + + compositeDisposable.add( + rouletteRepository.refundRouletteDonation( + roomId, + "Bearer ${SharedPreferenceManager.token}" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success) { + _toastLiveData.postValue( + "후원에 실패했습니다.\n다시 후원해주세요.\n" + + "계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + ) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue( + "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요." + ) + } + ) + ) + } + + private fun randomSelectRouletteItem( + can: Int, + items: List, + complete: (Int, String) -> Unit + ) { + _isLoading.value = true + val rouletteItemTitles = items.asSequence().map { it.title }.toList() + val cumulativeWeights = items.runningFold(0) { sum, item -> + sum + item.weight + } + val totalWeight = items.asSequence().map { it.weight }.sum() + val randomValue = Random.nextInt(0, totalWeight) + + for (index in 1 until cumulativeWeights.size) { + if (randomValue < cumulativeWeights[index]) { + _isLoading.value = false + complete(can, rouletteItemTitles[index - 1]) + return + } + } + + _isLoading.value = false + complete(can, rouletteItemTitles.last()) + } + private fun calculatePercentages(options: List): List { val totalWeight = options.sumOf { it.weight } val updatedOptions = options.asSequence().map { option -> diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt index 9d5a943..85b9762 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt @@ -174,6 +174,7 @@ data class LiveRoomNormalChat( itemBinding.tvNickname.text = nickname itemBinding.ivBg.visibility = View.VISIBLE + itemBinding.ivRoulette.visibility = View.GONE itemBinding.tvCreatorOrManager.visibility = View.GONE when (rank + 1) { @@ -288,6 +289,7 @@ data class LiveRoomDonationChat( itemBinding.ivCan.visibility = View.VISIBLE itemBinding.ivBg.visibility = View.GONE itemBinding.ivCrown.visibility = View.GONE + itemBinding.ivRoulette.visibility = View.GONE itemBinding.tvCreatorOrManager.visibility = View.GONE if (donationMessage.isNotBlank()) { @@ -334,3 +336,61 @@ data class LiveRoomDonationChat( itemBinding.root.setPadding(33) } } + +data class LiveRoomRouletteDonationChat( + @SerializedName("profileUrl") val profileUrl: String, + @SerializedName("nickname") val nickname: String, + @SerializedName("chat") val chat: String +) : LiveRoomChat() { + override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) { + val itemBinding = binding as ItemLiveRoomChatBinding + val spChat = SpannableString(chat) + spChat.setSpan( + ForegroundColorSpan( + ContextCompat.getColor( + context, + R.color.color_ffe500 + ) + ), + 0, + chat.indexOf("]", 0, true) + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + + val spNickname = SpannableString("${nickname}님의 룰렛 결과?") + spNickname.setSpan( + CustomTypefaceSpan( + ResourcesCompat.getFont( + context, + R.font.gmarket_sans_medium + ) + ), + 0, + nickname.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + + itemBinding.ivProfile.load(profileUrl) { + crossfade(true) + placeholder(R.drawable.ic_place_holder) + transformations(RoundedCornersTransformation(23.3f.dpToPx())) + } + itemBinding.tvChat.text = spChat + itemBinding.tvNickname.text = spNickname + itemBinding.ivProfile.setOnClickListener {} + + itemBinding.ivCan.visibility = View.GONE + itemBinding.ivBg.visibility = View.GONE + itemBinding.ivCrown.visibility = View.GONE + itemBinding.tvCreatorOrManager.visibility = View.GONE + itemBinding.ivRoulette.visibility = View.VISIBLE + + itemBinding.tvDonationMessage.visibility = View.GONE + + itemBinding.llMessageBg.setPadding(0) + itemBinding.llMessageBg.background = null + + itemBinding.root.setBackgroundResource(R.drawable.bg_round_corner_6_7_c25264) + itemBinding.root.setPadding(33) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatRawMessage.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatRawMessage.kt index 7c23e2d..e04691e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatRawMessage.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatRawMessage.kt @@ -20,5 +20,7 @@ enum class LiveRoomChatRawMessageType { @SerializedName("DONATION_STATUS") DONATION_STATUS, @SerializedName("TOGGLE_ROULETTE") - TOGGLE_ROULETTE + TOGGLE_ROULETTE, + @SerializedName("ROULETTE_DONATION") + ROULETTE_DONATION } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteRepository.kt index 67aa2ac..07dbd14 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteRepository.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteRepository.kt @@ -1,5 +1,7 @@ package kr.co.vividnext.sodalive.live.roulette +import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.live.roulette.config.CreateOrUpdateRouletteRequest import kr.co.vividnext.sodalive.live.roulette.config.RouletteApi @@ -16,4 +18,16 @@ class RouletteRepository(private val api: RouletteApi) { creatorId = creatorId, authHeader = token ) + + fun spinRoulette(request: SpinRouletteRequest, token: String) = api.spinRoulette( + request = request, + authHeader = token + ) + + fun refundRouletteDonation(roomId: Long, token: String): Single> { + return api.refundRouletteDonation( + id = roomId, + authHeader = token + ) + } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/SpinRouletteRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/SpinRouletteRequest.kt new file mode 100644 index 0000000..4a48804 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/SpinRouletteRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.roulette + +import com.google.gson.annotations.SerializedName + +data class SpinRouletteRequest( + @SerializedName("roomId") val roomId: Long, + @SerializedName("container") val container: String = "aos" +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/config/RouletteApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/config/RouletteApi.kt index f0ac76e..4b50433 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/config/RouletteApi.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/config/RouletteApi.kt @@ -3,10 +3,12 @@ package kr.co.vividnext.sodalive.live.roulette.config import io.reactivex.rxjava3.core.Single import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.live.roulette.GetRouletteResponse +import kr.co.vividnext.sodalive.live.roulette.SpinRouletteRequest import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST +import retrofit2.http.Path import retrofit2.http.Query interface RouletteApi { @@ -21,4 +23,16 @@ interface RouletteApi { @Query("creatorId") creatorId: Long, @Header("Authorization") authHeader: String ): Single> + + @POST("/roulette/spin") + fun spinRoulette( + @Body request: SpinRouletteRequest, + @Header("Authorization") authHeader: String + ): Single> + + @POST("/roulette/refund/{id}") + fun refundRouletteDonation( + @Path("id") id: Long, + @Header("Authorization") authHeader: String + ): Single> } diff --git a/app/src/main/res/layout/item_live_room_chat.xml b/app/src/main/res/layout/item_live_room_chat.xml index cc7b280..e7ed6af 100644 --- a/app/src/main/res/layout/item_live_room_chat.xml +++ b/app/src/main/res/layout/item_live_room_chat.xml @@ -39,6 +39,15 @@ android:layout_gravity="end|bottom" android:contentDescription="@null" android:visibility="gone" /> + + #3BB9F1 #2E6279 #CF5C37 + #FFE500