diff --git a/app/build.gradle b/app/build.gradle
index 9d8c574..e13d187 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -50,6 +50,8 @@ android {
buildConfigField 'String', 'BASE_URL', '"https://api.sodalive.net"'
buildConfigField 'String', 'BOOTPAY_APP_ID', '"64c35be1d25985001dc50c87"'
+ buildConfigField 'String', 'AGORA_APP_ID', '"4566e5b98f434b9eabb63435f794a1f9"'
+ buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"e89e30f9ee584fda9454db3c0e387632"'
}
debug {
@@ -59,6 +61,8 @@ android {
buildConfigField 'String', 'BASE_URL', '"https://test-api.sodalive.net"'
buildConfigField 'String', 'BOOTPAY_APP_ID', '"6242a7772701800023f68b2e"'
+ buildConfigField 'String', 'AGORA_APP_ID', '"d28c80855d314a599cd7c15280920699"'
+ buildConfigField 'String', 'AGORA_APP_CERTIFICATE', '"29ef33b7c37e4b80b74af9a6e9b2af5e"'
}
}
compileOptions {
@@ -131,4 +135,8 @@ dependencies {
// bootpay
implementation "io.github.bootpay:android:4.3.4"
+
+ // agora
+ implementation "io.agora.rtc:voice-sdk:4.1.0-1"
+ implementation 'io.agora.rtm:rtm-sdk:1.5.3'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f0aa820..4036e32 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -39,6 +39,7 @@
+
{
+ override fun onSuccess(p0: Void?) {
+ Logger.e("sendMessage - onSuccess")
+ }
+
+ override fun onFailure(p0: ErrorInfo) {
+ Logger.e("sendMessage fail - ${p0.errorCode}")
+ Logger.e("sendMessage fail - ${p0.errorDescription}")
+ }
+ }
+ )
+ }
+
+ fun joinRtcChannel(uid: Int, rtcToken: String, channelName: String) {
+ rtcEngine!!.joinChannel(
+ rtcToken,
+ channelName,
+ "",
+ uid
+ )
+ }
+
+ fun createRtmChannelAndLogin(
+ uid: String,
+ rtmToken: String,
+ channelName: String,
+ rtmChannelListener: RtmChannelListener,
+ rtmChannelJoinSuccess: () -> Unit,
+ rtmChannelJoinFail: () -> Unit
+ ) {
+ rtmChannel = rtmClient!!.createChannel(channelName, rtmChannelListener)
+ rtmClient!!.login(
+ rtmToken,
+ uid,
+ object : ResultCallback {
+ override fun onSuccess(p0: Void?) {
+ rtmChannel!!.join(object : ResultCallback {
+ override fun onSuccess(p0: Void?) {
+ Logger.e("rtmChannel join - onSuccess")
+ rtmChannelJoinSuccess()
+ }
+
+ override fun onFailure(p0: ErrorInfo?) {
+ rtmChannelJoinFail()
+ }
+ })
+ }
+
+ override fun onFailure(p0: ErrorInfo?) {
+ }
+ }
+ )
+ }
+
+ fun sendRawMessageToGroup(
+ rawMessage: ByteArray,
+ onSuccess: (() -> Unit)? = null,
+ onFailure: (() -> Unit)? = null
+ ) {
+ val message = rtmClient!!.createMessage()
+ message.rawMessage = rawMessage
+ rtmChannel!!.sendMessage(
+ message,
+ object : ResultCallback {
+ override fun onSuccess(p0: Void?) {
+ Logger.e("sendMessage - onSuccess")
+ onSuccess?.invoke()
+ }
+
+ override fun onFailure(p0: ErrorInfo) {
+ Logger.e("sendMessage fail - ${p0.errorCode}")
+ Logger.e("sendMessage fail - ${p0.errorDescription}")
+ onFailure?.invoke()
+ }
+ }
+ )
+ }
+
+ fun setClientRole(role: Int) {
+ rtcEngine!!.setClientRole(role)
+ }
+
+ fun muteLocalAudioStream(muted: Boolean) {
+ rtcEngine?.muteLocalAudioStream(muted)
+ }
+
+ fun muteAllRemoteAudioStreams(mute: Boolean) {
+ rtcEngine?.muteAllRemoteAudioStreams(mute)
+ }
+
+ fun sendRawMessageToPeer(
+ receiverUid: String,
+ requestType: LiveRoomRequestType,
+ onSuccess: () -> Unit
+ ) {
+ val option = SendMessageOptions()
+
+ val message = rtmClient!!.createMessage()
+ message.rawMessage = requestType.toString().toByteArray()
+
+ rtmClient!!.sendMessageToPeer(
+ receiverUid,
+ message,
+ option,
+ object : ResultCallback {
+ override fun onSuccess(aVoid: Void?) {
+ onSuccess()
+ }
+
+ override fun onFailure(errorInfo: ErrorInfo) {
+ }
+ }
+ )
+ }
+
+ fun rtmChannelIsNull(): Boolean {
+ return rtmChannel == null
+ }
+
+ fun getConnectionState(): Int {
+ return rtcEngine!!.connectionState
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt
index 9dde486..81c1496 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt
@@ -25,4 +25,6 @@ object Constants {
const val EXTRA_LIVE_RESERVATION_RESPONSE = "extra_live_reservation_response"
const val EXTRA_CONTENT_ID = "extra_content_id"
+
+ const val LIVE_SERVICE_NOTIFICATION_ID: Int = 2
}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/SodaLiveService.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/SodaLiveService.kt
new file mode 100644
index 0000000..2242bf8
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/common/SodaLiveService.kt
@@ -0,0 +1,90 @@
+package kr.co.vividnext.sodalive.common
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.live.LiveViewModel
+import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
+import org.koin.android.ext.android.inject
+
+class SodaLiveService : Service() {
+
+ private val liveViewModel: LiveViewModel by inject()
+
+ var roomId: Long = 0
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val content = intent?.getStringExtra("content") ?: "라이브 진행중"
+ roomId = intent?.getLongExtra("roomId", 0) ?: 0L
+ updateNotification(content)
+ return START_STICKY
+ }
+
+ private fun updateNotification(content: String) {
+ startForeground(Constants.LIVE_SERVICE_NOTIFICATION_ID, createNotification(content))
+ }
+
+ private fun createNotification(content: String): Notification {
+ val notificationChannelId = "soda_live_service_foreground_service_channel"
+ val notificationManager =
+ getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel =
+ NotificationChannel(
+ notificationChannelId,
+ getString(R.string.app_name),
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ val intent = Intent(this, LiveRoomActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
+ )
+
+ val notificationBuilder = NotificationCompat.Builder(this, notificationChannelId)
+ .setSmallIcon(R.drawable.ic_noti)
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText(content)
+ .setOngoing(true)
+ .setSilent(true)
+ .setContentIntent(pendingIntent)
+
+ return notificationBuilder.build()
+ }
+
+ override fun onDestroy() {
+ liveViewModel.quitRoom(roomId) { }
+ super.onDestroy()
+ }
+
+ override fun onTaskRemoved(rootIntent: Intent?) {
+ stopSelf()
+ }
+
+ companion object {
+ fun stopService(context: Context) {
+ val intent = Intent(context, SodaLiveService::class.java)
+ context.stopService(intent)
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
index 5880722..036487d 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
@@ -9,8 +9,10 @@ import kr.co.vividnext.sodalive.live.LiveRepository
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendApi
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepository
+import kr.co.vividnext.sodalive.live.room.LiveRoomViewModel
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateViewModel
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailViewModel
+import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewModel
import kr.co.vividnext.sodalive.live.room.tag.LiveTagRepository
import kr.co.vividnext.sodalive.live.room.tag.LiveTagViewModel
import kr.co.vividnext.sodalive.live.room.update.LiveRoomEditViewModel
@@ -24,6 +26,8 @@ import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeViewModel
import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentViewModel
import kr.co.vividnext.sodalive.mypage.can.status.CanStatusViewModel
import kr.co.vividnext.sodalive.network.TokenAuthenticator
+import kr.co.vividnext.sodalive.report.ReportApi
+import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.settings.event.EventApi
import kr.co.vividnext.sodalive.settings.event.EventRepository
import kr.co.vividnext.sodalive.settings.terms.TermsApi
@@ -76,13 +80,14 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
.build()
}
- single { ApiBuilder().build(get(), UserApi::class.java) }
- single { ApiBuilder().build(get(), TermsApi::class.java) }
- single { ApiBuilder().build(get(), LiveApi::class.java) }
- single { ApiBuilder().build(get(), EventApi::class.java) }
- single { ApiBuilder().build(get(), LiveRecommendApi::class.java) }
- single { ApiBuilder().build(get(), AuthApi::class.java) }
single { ApiBuilder().build(get(), CanApi::class.java) }
+ single { ApiBuilder().build(get(), AuthApi::class.java) }
+ single { ApiBuilder().build(get(), UserApi::class.java) }
+ single { ApiBuilder().build(get(), LiveApi::class.java) }
+ single { ApiBuilder().build(get(), TermsApi::class.java) }
+ single { ApiBuilder().build(get(), EventApi::class.java) }
+ single { ApiBuilder().build(get(), ReportApi::class.java) }
+ single { ApiBuilder().build(get(), LiveRecommendApi::class.java) }
}
private val viewModelModule = module {
@@ -100,17 +105,20 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { LiveRoomCreateViewModel(get()) }
viewModel { LiveTagViewModel(get()) }
viewModel { LiveRoomEditViewModel(get()) }
+ viewModel { LiveRoomViewModel(get(), get(), get()) }
+ viewModel { LiveRoomDonationMessageViewModel(get()) }
}
private val repositoryModule = module {
factory { UserRepository(get()) }
factory { TermsRepository(get()) }
- factory { LiveRepository(get()) }
+ factory { LiveRepository(get(), get()) }
factory { EventRepository(get()) }
factory { LiveRecommendRepository(get()) }
factory { AuthRepository(get()) }
factory { CanRepository(get()) }
factory { LiveTagRepository(get()) }
+ factory { ReportRepository(get()) }
}
private val moduleList = listOf(
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/MemberBlockRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/MemberBlockRequest.kt
new file mode 100644
index 0000000..03cad5d
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/MemberBlockRequest.kt
@@ -0,0 +1,5 @@
+package kr.co.vividnext.sodalive.explorer.profile
+
+import com.google.gson.annotations.SerializedName
+
+data class MemberBlockRequest(@SerializedName("blockMemberId") val blockMemberId: Long)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt
index 272b69f..a7bff5d 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveApi.kt
@@ -8,15 +8,25 @@ import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
import kr.co.vividnext.sodalive.live.room.CancelLiveRequest
import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
+import kr.co.vividnext.sodalive.live.room.SetManagerOrSpeakerOrAudienceRequest
import kr.co.vividnext.sodalive.live.room.StartLiveRequest
import kr.co.vividnext.sodalive.live.room.create.CreateLiveRoomResponse
import kr.co.vividnext.sodalive.live.room.create.GetRecentRoomInfoResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
+import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage
+import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse
+import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationTotalResponse
+import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessage
+import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest
+import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse
+import kr.co.vividnext.sodalive.live.room.kick_out.LiveRoomKickOutRequest
+import kr.co.vividnext.sodalive.live.room.profile.GetLiveRoomUserProfileResponse
import kr.co.vividnext.sodalive.live.room.tag.GetLiveTagResponse
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.GET
+import retrofit2.http.HTTP
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
@@ -93,4 +103,83 @@ interface LiveApi {
@Part("request") request: RequestBody?,
@Header("Authorization") authHeader: String
): Single>
+
+ @GET("/live/room/info/{id}")
+ fun getRoomInfo(
+ @Path("id") id: Long,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @GET("/live/room/donation-message")
+ fun getDonationMessageList(
+ @Query("roomId") roomId: Long,
+ @Header("Authorization") authHeader: String
+ ): Single>>
+
+ @HTTP(method = "DELETE", path = "/live/room/donation-message", hasBody = true)
+ fun deleteDonationMessage(
+ @Body request: DeleteLiveRoomDonationMessage,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @GET("/live/room/{room_id}/profile/{user_id}")
+ fun getUserProfile(
+ @Path("room_id") roomId: Long,
+ @Path("user_id") userId: Long,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @GET("/live/room/{id}/donation-total")
+ fun getDonationTotal(
+ @Path("id") id: Long,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @PUT("/live/room/info/set/speaker")
+ fun setSpeaker(
+ @Body request: SetManagerOrSpeakerOrAudienceRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @PUT("/live/room/info/set/listener")
+ fun setListener(
+ @Body request: SetManagerOrSpeakerOrAudienceRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @POST("/live/room/kick-out")
+ fun kickOut(
+ @Body request: LiveRoomKickOutRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @POST("/live/room/donation")
+ fun donation(
+ @Body request: LiveRoomDonationRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @POST("/live/room/donation/refund/{id}")
+ fun refundDonation(
+ @Path("id") id: Long,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @POST("/live/room/quit")
+ fun quitRoom(
+ @Query("id") roomId: Long,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @PUT("/live/room/info/set/manager")
+ fun setManager(
+ @Body request: SetManagerOrSpeakerOrAudienceRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @GET("/live/room/{id}/donation-list")
+ fun donationStatus(
+ @Path("id") id: Long,
+ @Header("Authorization") authHeader: String
+ ): Single>
}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt
index 31c8eaa..8eb9013 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveFragment.kt
@@ -34,6 +34,7 @@ import kr.co.vividnext.sodalive.live.recommend.RecommendLiveAdapter
import kr.co.vividnext.sodalive.live.recommend_channel.LiveRecommendChannelAdapter
import kr.co.vividnext.sodalive.live.reservation.LiveReservationAdapter
import kr.co.vividnext.sodalive.live.reservation.complete.LiveReservationCompleteActivity
+import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
import kr.co.vividnext.sodalive.live.room.detail.LiveRoomDetailFragment
@@ -65,7 +66,13 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
+ val roomId = it.data?.getLongExtra(Constants.EXTRA_ROOM_ID, 0)
+ val channelName = it.data?.getStringExtra(Constants.EXTRA_ROOM_CHANNEL_NAME)
refreshSummary()
+
+ if (channelName != null) {
+ enterLiveRoom(roomId = roomId!!)
+ }
}
}
}
@@ -271,7 +278,7 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl
liveNowAdapter = LiveNowAdapter {
val detailFragment = LiveRoomDetailFragment(
it.roomId,
- onClickParticipant = {},
+ onClickParticipant = { enterLiveRoom(it.roomId) },
onClickReservation = {},
onClickModify = {},
onClickStart = {},
@@ -326,7 +333,10 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl
recyclerView.visibility = View.GONE
binding.layoutLiveNow.tvAllView.visibility = View.GONE
binding.layoutLiveNow.llNoItems.visibility = View.VISIBLE
- binding.layoutLiveNow.tvMakeRoom.setOnClickListener {}
+ binding.layoutLiveNow.tvMakeRoom.setOnClickListener {
+ val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
+ activityResultLauncher.launch(intent)
+ }
recyclerView.requestLayout()
binding.layoutLiveNow.llNoItems.requestLayout()
@@ -410,7 +420,11 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl
recyclerView.visibility = View.GONE
binding.layoutLiveReservation.tvAllView.visibility = View.GONE
binding.layoutLiveReservation.llNoItems.visibility = View.VISIBLE
- binding.layoutLiveReservation.tvMakeRoom.setOnClickListener {}
+ binding.layoutLiveReservation.tvMakeRoom.setOnClickListener {
+ val intent = Intent(requireContext(), LiveRoomCreateActivity::class.java)
+ intent.putExtra(Constants.EXTRA_LIVE_TIME_NOW, false)
+ activityResultLauncher.launch(intent)
+ }
recyclerView.requestLayout()
binding.layoutLiveReservation.llNoItems.requestLayout()
@@ -468,6 +482,11 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl
private fun startLive(roomId: Long) {
val onEnterRoomSuccess = {
viewModel.getSummary()
+ requireActivity().runOnUiThread {
+ val intent = Intent(requireContext(), LiveRoomActivity::class.java)
+ intent.putExtra(Constants.EXTRA_ROOM_ID, roomId)
+ startActivity(intent)
+ }
}
viewModel.startLive(roomId, onEnterRoomSuccess)
@@ -553,4 +572,90 @@ class LiveFragment : BaseFragment(FragmentLiveBinding::infl
}
)
}
+
+ fun enterLiveRoom(roomId: Long) {
+ val onEnterRoomSuccess = {
+ requireActivity().runOnUiThread {
+ val intent = Intent(requireContext(), LiveRoomActivity::class.java)
+ intent.putExtra(Constants.EXTRA_ROOM_ID, roomId)
+ startActivity(intent)
+ }
+ }
+
+ viewModel.getRoomDetail(roomId) {
+ if (it.channelName != null) {
+ if (it.manager.id == SharedPreferenceManager.userId) {
+ handler.postDelayed({
+ viewModel.enterRoom(roomId, onEnterRoomSuccess)
+ }, 300)
+ } else if (it.price == 0 || it.isPaid) {
+ if (it.isPrivateRoom) {
+ LiveRoomPasswordDialog(
+ activity = requireActivity(),
+ layoutInflater = layoutInflater,
+ can = 0,
+ confirmButtonClick = { password ->
+ viewModel.enterRoom(
+ roomId = roomId,
+ onSuccess = onEnterRoomSuccess,
+ password = password
+ )
+ }
+ ).show(screenWidth)
+ } else {
+ handler.postDelayed({
+ viewModel.enterRoom(roomId, onEnterRoomSuccess)
+ }, 300)
+ }
+ } else {
+ if (it.isPrivateRoom) {
+ LiveRoomPasswordDialog(
+ activity = requireActivity(),
+ layoutInflater = layoutInflater,
+ can = it.price,
+ confirmButtonClick = { password ->
+ handler.postDelayed({
+ viewModel.enterRoom(
+ roomId = roomId,
+ onSuccess = onEnterRoomSuccess,
+ password = password
+ )
+ }, 300)
+ }
+ ).show(screenWidth)
+ } else {
+ LivePaymentDialog(
+ activity = requireActivity(),
+ layoutInflater = layoutInflater,
+ title = "${it.price.moneyFormat()}코인으로 입장",
+ desc = "'${it.title}' 라이브에 참여하기 위해 결제합니다.",
+ confirmButtonTitle = "결제 후 입장",
+ confirmButtonClick = {
+ handler.postDelayed({
+ viewModel.enterRoom(roomId, onEnterRoomSuccess)
+ }, 300)
+ },
+ cancelButtonTitle = "취소",
+ cancelButtonClick = {}
+ ).show(screenWidth)
+ }
+ }
+ } else {
+ val detailFragment = LiveRoomDetailFragment(
+ it.roomId,
+ onClickParticipant = {},
+ onClickReservation = { reservationRoom(it.roomId) },
+ onClickModify = { roomDetailResponse -> modifyLive(roomDetailResponse) },
+ onClickStart = { startLive(it.roomId) },
+ onClickCancel = { cancelLive(it.roomId) }
+ )
+ if (detailFragment.isAdded) return@getRoomDetail
+
+ detailFragment.show(
+ requireActivity().supportFragmentManager,
+ detailFragment.tag
+ )
+ }
+ }
+ }
}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt
index 7167f78..fec9412 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveRepository.kt
@@ -7,14 +7,23 @@ import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest
import kr.co.vividnext.sodalive.live.room.CancelLiveRequest
import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
+import kr.co.vividnext.sodalive.live.room.SetManagerOrSpeakerOrAudienceRequest
import kr.co.vividnext.sodalive.live.room.StartLiveRequest
import kr.co.vividnext.sodalive.live.room.create.CreateLiveRoomResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
+import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage
+import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest
+import kr.co.vividnext.sodalive.live.room.kick_out.LiveRoomKickOutRequest
+import kr.co.vividnext.sodalive.user.CreatorFollowRequestRequest
+import kr.co.vividnext.sodalive.user.UserApi
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.util.TimeZone
-class LiveRepository(private val api: LiveApi) {
+class LiveRepository(
+ private val api: LiveApi,
+ private val userApi: UserApi
+) {
fun roomList(
dateString: String? = null,
status: LiveRoomStatus,
@@ -94,4 +103,109 @@ class LiveRepository(private val api: LiveApi) {
request = request,
authHeader = token
)
+
+ fun getRoomInfo(roomId: Long, token: String) = api.getRoomInfo(roomId, authHeader = token)
+
+ fun getDonationMessageList(
+ roomId: Long,
+ token: String
+ ) = api.getDonationMessageList(roomId = roomId, authHeader = token)
+
+ fun deleteDonationMessage(
+ roomId: Long,
+ uuid: String,
+ token: String
+ ) = api.deleteDonationMessage(
+ request = DeleteLiveRoomDonationMessage(
+ roomId,
+ messageUUID = uuid
+ ),
+ authHeader = token
+ )
+
+ fun getUserProfile(roomId: Long, userId: Long, token: String) = api.getUserProfile(
+ roomId = roomId,
+ userId = userId,
+ authHeader = token
+ )
+
+ fun getTotalDonationCan(
+ roomId: Long,
+ token: String
+ ) = api.getDonationTotal(roomId, authHeader = token)
+
+ fun setSpeaker(roomId: Long, userId: Long, token: String): Single> {
+ return api.setSpeaker(
+ request = SetManagerOrSpeakerOrAudienceRequest(roomId, accountId = userId),
+ authHeader = token
+ )
+ }
+
+ fun setListener(roomId: Long, userId: Long, token: String): Single> {
+ return api.setListener(
+ request = SetManagerOrSpeakerOrAudienceRequest(roomId, accountId = userId),
+ authHeader = token
+ )
+ }
+
+ fun kickOut(roomId: Long, userId: Long, token: String): Single> {
+ return api.kickOut(
+ request = LiveRoomKickOutRequest(roomId, userId),
+ authHeader = token
+ )
+ }
+
+ fun donation(
+ roomId: Long,
+ can: Int,
+ message: String,
+ token: String
+ ): Single> {
+ return api.donation(
+ request = LiveRoomDonationRequest(
+ roomId = roomId,
+ can = can,
+ message = message,
+ container = "aos"
+ ),
+ authHeader = token
+ )
+ }
+
+ fun refundDonation(roomId: Long, token: String): Single> {
+ return api.refundDonation(
+ id = roomId,
+ authHeader = token
+ )
+ }
+
+ fun quitRoom(roomId: Long, token: String): Single> {
+ return api.quitRoom(roomId = roomId, authHeader = token)
+ }
+
+ fun setManager(roomId: Long, userId: Long, token: String) = api.setManager(
+ request = SetManagerOrSpeakerOrAudienceRequest(roomId, accountId = userId),
+ authHeader = token,
+ )
+
+ fun creatorFollow(
+ creatorId: Long,
+ token: String
+ ) = userApi.creatorFollow(
+ request = CreatorFollowRequestRequest(creatorId = creatorId),
+ authHeader = token
+ )
+
+ fun creatorUnFollow(
+ creatorId: Long,
+ token: String
+ ) = userApi.creatorUnFollow(
+ request = CreatorFollowRequestRequest(creatorId = creatorId),
+ authHeader = token
+ )
+
+ fun donationStatus(
+ roomId: Long,
+ token: String
+ ) = api.donationStatus(roomId, authHeader = token)
}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt
index 51c07d7..e950489 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/LiveViewModel.kt
@@ -11,11 +11,11 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.live.recommend.GetRecommendLiveResponse
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepository
import kr.co.vividnext.sodalive.live.recommend_channel.GetRecommendChannelResponse
+import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest
+import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
import kr.co.vividnext.sodalive.live.room.CancelLiveRequest
import kr.co.vividnext.sodalive.live.room.EnterOrQuitLiveRoomRequest
import kr.co.vividnext.sodalive.live.room.LiveRoomStatus
-import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest
-import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationResponse
import kr.co.vividnext.sodalive.live.room.StartLiveRequest
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
import kr.co.vividnext.sodalive.settings.event.EventItem
@@ -245,6 +245,37 @@ class LiveViewModel(
}
}
+ fun quitRoom(roomId: Long, onSuccess: () -> Unit) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.quitRoom(roomId, "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success) {
+ onSuccess()
+ _isLoading.value = false
+ } else {
+ _isLoading.value = false
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
fun startLive(roomId: Long, onEnterRoomSuccess: () -> Unit) {
_isLoading.value = true
compositeDisposable.add(
@@ -313,7 +344,7 @@ class LiveViewModel(
)
}
- fun enterRoom(roomId: Long, onSuccess: () -> Unit, password: Int? = null) {
+ fun enterRoom(roomId: Long, onSuccess: () -> Unit, password: String? = null) {
_isLoading.value = true
val request = EnterOrQuitLiveRoomRequest(roomId, password = password)
compositeDisposable.add(
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt
index c78db54..2b130e0 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt
@@ -5,5 +5,5 @@ import com.google.gson.annotations.SerializedName
data class EnterOrQuitLiveRoomRequest(
@SerializedName("roomId") val roomId: Long,
@SerializedName("container") val container: String = "aos",
- @SerializedName("password") val password: Int? = null
+ @SerializedName("password") val password: String? = null
)
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
new file mode 100644
index 0000000..de2ec4f
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt
@@ -0,0 +1,1404 @@
+package kr.co.vividnext.sodalive.live.room
+
+import android.annotation.SuppressLint
+import android.app.AlertDialog
+import android.app.Service
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.graphics.Rect
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.method.LinkMovementMethod
+import android.text.style.ClickableSpan
+import android.view.LayoutInflater
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.widget.PopupMenu
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import coil.load
+import coil.transform.CircleCropTransformation
+import com.github.dhaval2404.imagepicker.ImagePicker
+import com.google.gson.Gson
+import com.orhanobut.logger.Logger
+import io.agora.rtc2.ClientRoleOptions
+import io.agora.rtc2.IRtcEngineEventHandler
+import io.agora.rtc2.RtcConnection
+import io.agora.rtm.RtmChannelAttribute
+import io.agora.rtm.RtmChannelListener
+import io.agora.rtm.RtmChannelMember
+import io.agora.rtm.RtmClientListener
+import io.agora.rtm.RtmMessage
+import io.agora.rtm.RtmMessageType
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.agora.Agora
+import kr.co.vividnext.sodalive.base.BaseActivity
+import kr.co.vividnext.sodalive.common.Constants
+import kr.co.vividnext.sodalive.common.LoadingDialog
+import kr.co.vividnext.sodalive.common.RealPathUtil
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.common.SodaLiveService
+import kr.co.vividnext.sodalive.databinding.ActivityLiveRoomBinding
+import kr.co.vividnext.sodalive.dialog.LiveDialog
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.extensions.moneyFormat
+import kr.co.vividnext.sodalive.live.room.chat.LiveRoomChatAdapter
+import kr.co.vividnext.sodalive.live.room.chat.LiveRoomChatRawMessage
+import kr.co.vividnext.sodalive.live.room.chat.LiveRoomChatRawMessageType
+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.donation.LiveRoomDonationDialog
+import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageDialog
+import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessageViewModel
+import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRankingDialog
+import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse
+import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileDialog
+import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileListAdapter
+import kr.co.vividnext.sodalive.live.room.profile.LiveRoomUserProfileDialog
+import kr.co.vividnext.sodalive.live.room.update.LiveRoomInfoEditDialog
+import kr.co.vividnext.sodalive.report.ProfileReportDialog
+import kr.co.vividnext.sodalive.report.ReportType
+import kr.co.vividnext.sodalive.report.UserReportDialog
+import kr.co.vividnext.sodalive.settings.notification.MemberRole
+import org.koin.android.ext.android.inject
+import java.util.regex.Pattern
+
+class LiveRoomActivity : BaseActivity(ActivityLiveRoomBinding::inflate) {
+
+ private var roomId: Long = 0
+
+ private val viewModel: LiveRoomViewModel by inject()
+ private val donationMessageViewModel: LiveRoomDonationMessageViewModel by inject()
+
+ private lateinit var speakerListAdapter: LiveRoomProfileListAdapter
+ private lateinit var loadingDialog: LoadingDialog
+
+ private lateinit var imm: InputMethodManager
+ private val handler = Handler(Looper.getMainLooper())
+
+ private val chatAdapter = LiveRoomChatAdapter { userId ->
+ showLiveRoomUserProfileDialog(userId = userId)
+ }
+ private lateinit var layoutManager: LinearLayoutManager
+
+ private lateinit var agora: Agora
+ private lateinit var roomDialog: LiveRoomDialog
+ private lateinit var roomProfileDialog: LiveRoomProfileDialog
+ private lateinit var roomInfoEditDialog: LiveRoomInfoEditDialog
+ private lateinit var roomUserProfileDialog: LiveRoomUserProfileDialog
+
+ private var isSpeakerMute = false
+ private var isMicrophoneMute = false
+ private var isSpeaker = false
+ private var isSpeakerFold = false
+ private var isAvailableDonation = false
+
+ private val onBackPressedCallback = object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ onClickQuit()
+ }
+ }
+
+ private val imageResult =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ val resultCode = result.resultCode
+ val data = result.data
+
+ if (resultCode == RESULT_OK) {
+ // Image Uri will not be null for RESULT_OK
+ val fileUri = data?.data!!
+ roomInfoEditDialog.setCoverImageUri(fileUri)
+ } else if (resultCode == ImagePicker.RESULT_ERROR) {
+ Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ agora = Agora(
+ context = this,
+ rtcEventHandler = rtcEventHandler,
+ rtmClientListener = rtmClientListener
+ )
+
+ super.onCreate(savedInstanceState)
+ onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
+
+ viewModel.getRealPathFromURI = {
+ RealPathUtil.getRealPath(applicationContext, it)
+ }
+ this.roomId = intent.getLongExtra(Constants.EXTRA_ROOM_ID, 0)
+
+ if (roomId <= 0) {
+ showToast("해당하는 라이브가 없습니다.")
+ finish()
+ return
+ }
+
+ viewModel.getMemberCan()
+ viewModel.getRoomInfo(roomId)
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ if (this::layoutManager.isInitialized) {
+ layoutManager.scrollToPosition(chatAdapter.itemCount - 1)
+ }
+
+ if (
+ viewModel.isRoomInfoInitialized() &&
+ agora.getConnectionState() ==
+ RtcConnection.CONNECTION_STATE_TYPE.CONNECTION_STATE_DISCONNECTED.ordinal
+ ) {
+ val userId = SharedPreferenceManager.userId
+ agora.joinRtcChannel(
+ uid = userId.toInt(),
+ rtcToken = viewModel.roomInfoResponse.rtcToken,
+ channelName = viewModel.roomInfoResponse.channelName
+ )
+ }
+ }
+
+ override fun setupView() {
+ bindData()
+
+ loadingDialog = LoadingDialog(this, layoutInflater)
+ imm = getSystemService(
+ Service.INPUT_METHOD_SERVICE
+ ) as InputMethodManager
+
+ roomDialog = LiveRoomDialog(this, layoutInflater)
+ roomInfoEditDialog = LiveRoomInfoEditDialog(
+ activity = this,
+ layoutInflater = layoutInflater,
+ onClickImagePicker = {
+ ImagePicker.with(this)
+ .crop()
+ .galleryOnly()
+ .galleryMimeTypes( // Exclude gif images
+ mimeTypes = arrayOf(
+ "image/png",
+ "image/jpg",
+ "image/jpeg"
+ )
+ )
+ .createIntent { imageResult.launch(it) }
+ }
+ )
+ roomProfileDialog = LiveRoomProfileDialog(
+ layoutInflater = layoutInflater,
+ activity = this,
+ roomInfoLiveData = viewModel.roomInfoLiveData,
+ isStaff = {
+ viewModel.isEqualToManagerId(
+ accountId = SharedPreferenceManager.userId.toInt()
+ )
+ },
+ onClickInviteSpeaker = { accountId ->
+ if (speakerListAdapter.itemCount <= 9) {
+ inviteSpeaker(accountId)
+ } else {
+ showToast("스피커 정원이 초과했습니다.")
+ }
+ },
+ onClickChangeListener = { accountId ->
+ if (accountId == SharedPreferenceManager.userId) {
+ handler.post {
+ viewModel.setListener(
+ roomId,
+ SharedPreferenceManager.userId
+ ) {
+ setAudience()
+ viewModel.getRoomInfo(roomId)
+ }
+ }
+
+ return@LiveRoomProfileDialog
+ }
+
+ changeListenerMessage(accountId)
+ },
+ onClickKickOut = {
+ LiveDialog(
+ activity = this,
+ layoutInflater = layoutInflater,
+ title = "내보내기",
+ desc = "${viewModel.getUserNickname(it.toInt())}님을 내보내시겠어요?",
+ confirmButtonTitle = "내보내기",
+ confirmButtonClick = { kickOut(it) },
+ cancelButtonTitle = "취소",
+ cancelButtonClick = {}
+ ).show(screenWidth)
+ },
+ onClickProfile = {
+ showLiveRoomUserProfileDialog(userId = it)
+ }
+ )
+
+ roomUserProfileDialog = LiveRoomUserProfileDialog(
+ activity = this,
+ userProfileLiveData = viewModel.userProfileLiveData,
+ layoutInflater = layoutInflater,
+ isStaff = {
+ viewModel.isEqualToManagerId(it.toInt())
+ },
+ onClickSendMessage = { userId, nickname ->
+ },
+ onClickSetManager = {
+ setManagerMessageToPeer(userId = it)
+ viewModel.setManager(roomId = roomId, userId = it) {
+ setManagerMessage()
+ showDialog(
+ content = "${viewModel.getUserNickname(it.toInt())}님을 스탭으로 지정했습니다."
+ )
+ }
+ },
+ onClickReleaseManager = {
+ changeListenerMessage(it, isFromManager = true)
+ },
+ onClickFollow = {
+ viewModel.creatorFollow(
+ creatorId = it,
+ roomId = roomId,
+ isGetUserProfile = true
+ )
+ },
+ onClickUnFollow = {
+ viewModel.creatorUnFollow(
+ creatorId = it,
+ roomId = roomId,
+ isGetUserProfile = true
+ )
+ },
+ onClickInviteSpeaker = {
+ if (speakerListAdapter.itemCount <= 9) {
+ inviteSpeaker(it)
+ } else {
+ showToast("스피커 정원이 초과했습니다.")
+ }
+ },
+ onClickChangeListener = { changeListenerMessage(it) },
+ onClickKickOut = {
+ LiveDialog(
+ activity = this,
+ layoutInflater = layoutInflater,
+ title = "내보내기",
+ desc = "${viewModel.getUserNickname(it.toInt())}님을 내보내시겠어요?",
+ confirmButtonTitle = "내보내기",
+ confirmButtonClick = { kickOut(it) },
+ cancelButtonTitle = "취소",
+ cancelButtonClick = {}
+ ).show(screenWidth)
+ },
+ onClickPopupMenu = { userId, nickname, isBlock, view ->
+ showOptionMenu(
+ this,
+ userId,
+ nickname,
+ isBlock,
+ view,
+ )
+ }
+ )
+
+ binding.tvQuit.setOnClickListener { onClickQuit() }
+ binding.flMicrophoneMute.setOnClickListener {
+ microphoneMute()
+ if (isMicrophoneMute) {
+ binding.ivMicrophoneMute.setImageResource(R.drawable.ic_mic_off)
+ binding.ivNotiMicrophoneMute.visibility = View.VISIBLE
+ } else {
+ binding.ivMicrophoneMute.setImageResource(R.drawable.ic_mic_on)
+ binding.ivNotiMicrophoneMute.visibility = View.GONE
+ }
+ }
+ binding.flSpeakerMute.setOnClickListener {
+ speakerMute()
+ if (isSpeakerMute) {
+ binding.ivSpeakerMute.setImageResource(R.drawable.ic_speaker_off)
+ } else {
+ binding.ivSpeakerMute.setImageResource(R.drawable.ic_speaker_on)
+ }
+ }
+ binding.ivSend.setOnClickListener { inputChat() }
+ binding.flDonation.setOnClickListener {
+ val dialog = LiveRoomDonationDialog(
+ this,
+ LayoutInflater.from(this)
+ ) { can, message ->
+ if (can > 0) {
+ donation(can, message)
+ } else {
+ showToast("1코인 이상 후원하실 수 있습니다.")
+ }
+ }
+
+ dialog.show(screenWidth)
+ }
+ binding.ivNotification.setOnClickListener { viewModel.toggleShowNotice() }
+ binding.rlNotice.setOnClickListener { viewModel.toggleExpandNotice() }
+ binding.tvSpeakerFold.setOnClickListener {
+ isSpeakerFold = !isSpeakerFold
+
+ if (isSpeakerFold) {
+ binding.rlSpeaker.visibility = View.VISIBLE
+ binding.rvSpeakers.visibility = View.VISIBLE
+ binding.tvSpeakerFold.text = "접기"
+ binding.tvSpeakerFold.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_live_detail_top,
+ 0,
+ 0,
+ 0
+ )
+ } else {
+ binding.rlSpeaker.visibility = View.GONE
+ binding.rvSpeakers.visibility = View.GONE
+ binding.tvSpeakerFold.text = "펼치기"
+ binding.tvSpeakerFold.setCompoundDrawablesWithIntrinsicBounds(
+ R.drawable.ic_live_detail_bottom,
+ 0,
+ 0,
+ 0
+ )
+ }
+ }
+
+ binding.tvBgSwitch.setOnClickListener { viewModel.toggleBackgroundImage() }
+ binding.llDonation.setOnClickListener {
+ LiveRoomDonationRankingDialog(
+ activity = this,
+ layoutInflater = layoutInflater,
+ viewModel = viewModel,
+ roomId = roomId
+ ).show()
+ }
+
+ setupChatAdapter()
+ setupSpeakerListAdapter()
+ }
+
+ override fun onDestroy() {
+ hideKeyboard {
+ viewModel.quitRoom(roomId) {
+ SodaLiveService.stopService(this)
+ agora.deInitAgoraEngine()
+ }
+ }
+ super.onDestroy()
+ }
+
+ private fun showOptionMenu(
+ context: Context,
+ userId: Long,
+ nickname: String,
+ isBlock: Boolean,
+ v: View
+ ) {
+ val popup = PopupMenu(context, v)
+ val inflater = popup.menuInflater
+
+ if (isBlock) {
+ inflater.inflate(R.menu.user_profile_option_menu_2, popup.menu)
+
+ popup.setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.menu_user_block -> {
+ viewModel.memberUnBlock(userId)
+ }
+
+ R.id.menu_user_report -> {
+ showUserReportDialog(userId)
+ }
+
+ R.id.menu_profile_report -> {
+ showProfileReportDialog(userId)
+ }
+ }
+
+ true
+ }
+ } else {
+ inflater.inflate(R.menu.user_profile_option_menu, popup.menu)
+
+ popup.setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.menu_user_block -> {
+ showMemberBlockDialog(userId, nickname)
+ }
+
+ R.id.menu_user_report -> {
+ showUserReportDialog(userId)
+ }
+
+ R.id.menu_profile_report -> {
+ showProfileReportDialog(userId)
+ }
+ }
+
+ true
+ }
+ }
+
+ popup.show()
+ }
+
+ private fun showMemberBlockDialog(userId: Long, nickname: String) {
+ val dialog = AlertDialog.Builder(this)
+ dialog.setTitle("사용자 차단")
+ dialog.setMessage(
+ "${nickname}님을 차단하시겠습니까?\n\n" +
+ "사용자를 차단하면 사용자는 아래 기능이 제한됩니다.\n" +
+ "- 내가 개설한 라이브 입장 불가\n" +
+ "- 나에게 메시지 보내기 불가\n" +
+ "- 내 채널의 팬Talk 작성불가"
+ )
+ dialog.setPositiveButton("차단") { _, _ ->
+ roomUserProfileDialog.dismiss()
+ viewModel.memberBlock(userId) {
+ kickOut(userId)
+ }
+ }
+ dialog.setNegativeButton("취소") { _, _ -> }
+ dialog.show()
+ }
+
+ private fun showUserReportDialog(userId: Long) {
+ val dialog = UserReportDialog(this, layoutInflater) {
+ viewModel.report(
+ type = ReportType.USER,
+ userId = userId,
+ reason = it
+ )
+ }
+
+ dialog.show(screenWidth)
+ }
+
+ private fun showProfileReportDialog(userId: Long) {
+ val dialog = ProfileReportDialog(this, layoutInflater) {
+ viewModel.report(
+ type = ReportType.PROFILE,
+ userId = userId
+ )
+ }
+
+ dialog.show(screenWidth)
+ }
+
+ private fun showLiveRoomUserProfileDialog(userId: Long) {
+ viewModel.getUserProfile(roomId = roomId, userId = userId) {
+ roomUserProfileDialog.show()
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun bindData() {
+ viewModel.isBgOn.observe(this) {
+ if (it) {
+ binding.ivCover.visibility = View.VISIBLE
+ binding.tvBgSwitch.text = "배경 ON"
+ binding.tvBgSwitch.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_9970ff
+ )
+ )
+ binding.tvBgSwitch
+ .setBackgroundResource(R.drawable.bg_round_corner_13_3_transparent_9970ff)
+ } else {
+ binding.ivCover.visibility = View.GONE
+ binding.tvBgSwitch.text = "배경 OFF"
+ binding.tvBgSwitch.setTextColor(
+ ContextCompat.getColor(
+ applicationContext,
+ R.color.color_eeeeee
+ )
+ )
+ binding.tvBgSwitch
+ .setBackgroundResource(R.drawable.bg_round_corner_13_3_transparent_bbbbbb)
+ }
+ }
+
+ viewModel.isLoading.observe(this) {
+ if (it) {
+ loadingDialog.show(screenWidth)
+ } else {
+ loadingDialog.dismiss()
+ }
+ }
+
+ viewModel.toastLiveData.observe(this) {
+ it?.let { showToast(it) }
+ }
+
+ donationMessageViewModel.isLoading.observe(this) {
+ if (it) {
+ loadingDialog.show(screenWidth)
+ } else {
+ loadingDialog.dismiss()
+ }
+ }
+
+ donationMessageViewModel.toastLiveData.observe(this) {
+ it?.let { showToast(it) }
+ }
+
+ viewModel.roomInfoLiveData.observe(this) { response ->
+ binding.tvTitle.text = response.title
+ binding.ivCover.load(response.coverImageUrl) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ }
+
+ isAvailableDonation = response.isAvailableDonation
+ binding.flDonation.visibility = if (response.isAvailableDonation) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+
+ if (
+ response.managerId == SharedPreferenceManager.userId &&
+ SharedPreferenceManager.role == MemberRole.CREATOR.name
+ ) {
+ binding.flDonationMessageList.visibility = View.VISIBLE
+ binding.flDonationMessageList.setOnClickListener {
+ LiveRoomDonationMessageDialog(
+ layoutInflater = LayoutInflater.from(this),
+ activity = this,
+ donationMessageListLiveData = donationMessageViewModel
+ .donationMessageListLiveData,
+ donationMessageCountLiveData = donationMessageViewModel
+ .donationMessageCountLiveData,
+ getDonationMessageList = {
+ donationMessageViewModel.getDonationMessageList(roomId = roomId)
+ },
+ deleteDonationMessage = {
+ donationMessageViewModel.deleteDonationMessage(
+ roomId = roomId,
+ uuid = it
+ )
+ },
+ copyMessage = {
+ val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+ clipboard.setPrimaryClip(ClipData.newPlainText(it, it))
+ showToast("후원 메시지가 복사되었습니다.")
+ }
+ ).show()
+ }
+ } else {
+ binding.flDonationMessageList.visibility = View.GONE
+ }
+
+ speakerListAdapter.managerId = response.managerId
+ speakerListAdapter.updateList(response.speakerList)
+
+ if (response.managerId == SharedPreferenceManager.userId) {
+ binding.ivEdit.setOnClickListener {
+ roomInfoEditDialog.setRoomInfo(response.title, response.notice)
+ roomInfoEditDialog.setCoverImageUrl(response.coverImageUrl)
+ roomInfoEditDialog.setConfirmAction { newTitle, newContent, newCoverImageUri ->
+ viewModel.editLiveRoomInfo(
+ response.roomId,
+ newTitle,
+ newContent,
+ newCoverImageUri,
+ onSuccess = {
+ binding.tvTitle.text = newTitle
+ setNoticeAndClickableUrl(binding.tvNotice, newContent)
+
+ if (newCoverImageUri != null) {
+ binding.ivCover.load(newCoverImageUri) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ }
+ }
+
+ agora.sendRawMessageToGroup(
+ rawMessage = Gson().toJson(
+ LiveRoomChatRawMessage(
+ type = LiveRoomChatRawMessageType.EDIT_ROOM_INFO,
+ message = "",
+ can = 0,
+ donationMessage = ""
+ )
+ ).toByteArray()
+ )
+ }
+ )
+ }
+
+ roomInfoEditDialog.show(screenWidth)
+ }
+
+ binding.ivEdit.visibility = View.VISIBLE
+ binding.tvQuit.text = "라이브 종료"
+
+ handler.postDelayed({
+ binding.tvQuit.requestLayout()
+ binding.ivEdit.requestLayout()
+ }, 250)
+ } else {
+ binding.ivEdit.visibility = View.GONE
+ }
+
+ binding.ivShare.setOnClickListener {
+ viewModel.shareRoomLink(
+ response.roomId,
+ response.isPrivateRoom,
+ response.password
+ ) {
+ val intent = Intent(Intent.ACTION_SEND)
+ intent.type = "text/plain"
+ intent.putExtra(Intent.EXTRA_TEXT, it)
+
+ val shareIntent = Intent.createChooser(intent, "라이브 공유")
+ startActivity(shareIntent)
+ }
+ }
+
+ binding.llViewUsers.setOnClickListener { roomProfileDialog.show() }
+ binding.tvParticipate.text = "${response.participantsCount}"
+ setNoticeAndClickableUrl(binding.tvNotice, response.notice)
+
+ binding.tvCreatorNickname.text = response.managerNickname
+ binding.ivCreatorProfile.load(response.managerProfileUrl) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(CircleCropTransformation())
+ }
+
+ binding.ivCreatorProfile.setOnClickListener {
+ if (response.managerId != SharedPreferenceManager.userId) {
+ showLiveRoomUserProfileDialog(userId = response.managerId)
+ }
+ }
+
+ if (response.isAvailableDonation) {
+ binding.ivCreatorFollow.visibility = View.VISIBLE
+
+ if (response.isFollowingManager) {
+ binding.ivCreatorFollow.setImageResource(R.drawable.btn_following)
+ binding.ivCreatorFollow.setOnClickListener {
+ viewModel.creatorUnFollow(
+ creatorId = response.managerId,
+ roomId = roomId
+ )
+ }
+ } else {
+ binding.ivCreatorFollow.setImageResource(R.drawable.btn_follow)
+ binding.ivCreatorFollow.setOnClickListener {
+ viewModel.creatorFollow(
+ creatorId = response.managerId,
+ roomId = roomId
+ )
+ }
+ }
+ } else {
+ binding.ivCreatorFollow.visibility = View.GONE
+ }
+
+ if (agora.rtmChannelIsNull()) {
+ joinChannel(response)
+ }
+ }
+
+ viewModel.isShowNotice.observe(this) {
+ if (it) {
+ binding.ivNotification.setImageResource(R.drawable.ic_notice_selected)
+ binding.rlNotice.visibility = View.VISIBLE
+ } else {
+ binding.ivNotification.setImageResource(R.drawable.ic_notice_normal)
+ binding.rlNotice.visibility = View.GONE
+ }
+ }
+
+ viewModel.isExpandNotice.observe(this) {
+ binding.tvNotice.maxLines = if (it) {
+ Int.MAX_VALUE
+ } else {
+ 1
+ }
+ }
+
+ viewModel.totalDonationCan.observe(this) {
+ binding.tvTotalCan.text = it.moneyFormat()
+ }
+ }
+
+ private fun setNoticeAndClickableUrl(textView: TextView, text: String) {
+ textView.text = text
+
+ val spannable = SpannableString(text)
+ val pattern = Pattern.compile("https?://\\S+")
+ val matcher = pattern.matcher(spannable)
+
+ while (matcher.find()) {
+ val start = matcher.start()
+ val end = matcher.end()
+ val clickableSpan = object : ClickableSpan() {
+ override fun onClick(widget: View) {
+ val url = spannable.subSequence(start, end).toString()
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
+ }
+ }
+ spannable.setSpan(clickableSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+
+ textView.text = spannable
+ textView.movementMethod = LinkMovementMethod.getInstance()
+ }
+
+ private fun onClickQuit() {
+ hideKeyboard {
+ if (viewModel.isEqualToHostId(SharedPreferenceManager.userId.toInt())) {
+ LiveDialog(
+ activity = this,
+ layoutInflater = layoutInflater,
+ title = "라이브 종료",
+ desc = "라이브를 종료하시겠습니까?\n" +
+ "라이브를 종료하면 대화내용은\n" +
+ "저장되지 않고 사라집니다.\n" +
+ "참여자들 또한 라이브가 종료되어\n" +
+ "강제퇴장 됩니다.",
+ confirmButtonTitle = "예",
+ confirmButtonClick = { finish() },
+ cancelButtonTitle = "아니오",
+ cancelButtonClick = {}
+ ).show(screenWidth)
+ } else {
+ LiveDialog(
+ activity = this,
+ layoutInflater = layoutInflater,
+ title = "라이브 나가기",
+ desc = "라이브에서 나가시겠습니까?",
+ confirmButtonTitle = "예",
+ confirmButtonClick = { finish() },
+ cancelButtonTitle = "아니오",
+ cancelButtonClick = {}
+ ).show(screenWidth)
+ }
+ }
+ }
+
+ private fun hideKeyboard(onAfterExecute: () -> Unit) {
+ handler.postDelayed({
+ imm.hideSoftInputFromWindow(
+ window.decorView.applicationWindowToken,
+ InputMethodManager.HIDE_NOT_ALWAYS
+ )
+ onAfterExecute()
+ }, 100)
+ }
+
+ private fun setupChatAdapter() {
+ val rvChat = binding.rvChat
+ layoutManager = LinearLayoutManager(
+ applicationContext,
+ LinearLayoutManager.VERTICAL,
+ false
+ )
+ layoutManager.stackFromEnd = true
+ rvChat.layoutManager = layoutManager
+ rvChat.addItemDecoration(object : RecyclerView.ItemDecoration() {
+ override fun getItemOffsets(
+ outRect: Rect,
+ view: View,
+ parent: RecyclerView,
+ state: RecyclerView.State
+ ) {
+ super.getItemOffsets(outRect, view, parent, state)
+
+ outRect.top = 10f.dpToPx().toInt()
+ outRect.bottom = 10f.dpToPx().toInt()
+ }
+ })
+ rvChat.adapter = chatAdapter
+ rvChat.setOnScrollChangeListener { _, _, _, _, _ ->
+ if (!rvChat.canScrollVertically(1)) {
+ binding.tvNewChat.visibility = View.GONE
+ }
+ }
+
+ binding.tvNewChat.setOnClickListener {
+ binding.tvNewChat.visibility = View.GONE
+ layoutManager.scrollToPosition(chatAdapter.itemCount - 1)
+ }
+ }
+
+ private fun setupSpeakerListAdapter() {
+ val rvSpeakers = binding.rvSpeakers
+ speakerListAdapter = LiveRoomProfileListAdapter()
+
+ rvSpeakers.layoutManager = GridLayoutManager(applicationContext, 5)
+ rvSpeakers.addItemDecoration(object : RecyclerView.ItemDecoration() {
+ override fun getItemOffsets(
+ outRect: Rect,
+ view: View,
+ parent: RecyclerView,
+ state: RecyclerView.State
+ ) {
+ super.getItemOffsets(outRect, view, parent, state)
+
+ outRect.top = 5f.dpToPx().toInt()
+ outRect.bottom = 5f.dpToPx().toInt()
+ }
+ })
+ rvSpeakers.adapter = speakerListAdapter
+ }
+
+ private fun inviteSpeaker(peerId: Long) {
+ agora.sendRawMessageToPeer(
+ receiverUid = peerId.toString(),
+ requestType = LiveRoomRequestType.INVITE_SPEAKER
+ ) {
+ handler.post {
+ showDialog(content = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요.")
+ }
+ }
+ }
+
+ private fun setAudience() {
+ isSpeaker = false
+ isMicrophoneMute = false
+ agora.muteLocalAudioStream(false)
+ agora.setClientRole(io.agora.rtc2.Constants.CLIENT_ROLE_AUDIENCE)
+ handler.postDelayed({
+ binding.ivMicrophoneMute.setImageResource(R.drawable.ic_mic_on)
+ binding.flMicrophoneMute.visibility = View.GONE
+ binding.ivNotiMicrophoneMute.visibility = View.GONE
+ speakerListAdapter.muteSpeakers.remove(SharedPreferenceManager.userId.toInt())
+ }, 100)
+ }
+
+ private fun setBroadcaster() {
+ isSpeaker = true
+ isMicrophoneMute = false
+ agora.muteLocalAudioStream(false)
+ agora.setClientRole(io.agora.rtc2.Constants.CLIENT_ROLE_BROADCASTER)
+ handler.postDelayed({
+ binding.flMicrophoneMute.visibility = View.VISIBLE
+ binding.ivNotiMicrophoneMute.visibility = View.GONE
+ }, 100)
+ }
+
+ private fun changeListenerMessage(peerId: Long, isFromManager: Boolean = false) {
+ agora.sendRawMessageToPeer(
+ receiverUid = peerId.toString(),
+ requestType = LiveRoomRequestType.CHANGE_LISTENER
+ ) {
+ if (isFromManager) {
+ viewModel.getRoomInfo(roomId)
+ setManagerMessage()
+ releaseManagerMessageToPeer(userId = peerId)
+
+ handler.post {
+ showDialog(
+ content = "${viewModel.getUserNickname(peerId.toInt())}님을 스탭에서 해제했어요."
+ )
+ }
+ } else {
+ handler.post {
+ showDialog(
+ content = "${viewModel.getUserNickname(peerId.toInt())}님을 리스너로 변경했어요."
+ )
+ }
+ }
+ }
+ }
+
+ private fun releaseManagerMessageToPeer(userId: Long) {
+ agora.sendRawMessageToPeer(
+ receiverUid = userId.toString(),
+ requestType = LiveRoomRequestType.RELEASE_MANAGER
+ ) {}
+ }
+
+ private fun setManagerMessageToPeer(userId: Long) {
+ agora.sendRawMessageToPeer(
+ receiverUid = userId.toString(),
+ requestType = LiveRoomRequestType.SET_MANAGER
+ ) {}
+ }
+
+ private fun setManagerMessage() {
+ agora.sendRawMessageToGroup(
+ rawMessage = Gson().toJson(
+ LiveRoomChatRawMessage(
+ type = LiveRoomChatRawMessageType.SET_MANAGER,
+ message = "",
+ can = 0,
+ donationMessage = ""
+ )
+ ).toByteArray()
+ )
+ }
+
+ private fun kickOut(userId: Long) {
+ viewModel.kickOut(roomId, userId)
+ agora.sendRawMessageToPeer(
+ receiverUid = userId.toString(),
+ requestType = LiveRoomRequestType.KICK_OUT
+ ) {
+ handler.post {
+ showDialog(
+ content = "${viewModel.getUserNickname(userId.toInt())}님을 내보냈습니다."
+ )
+ }
+ }
+ }
+
+ private fun showDialog(
+ content: String,
+ cancelTitle: String = "",
+ cancelAction: (() -> Unit)? = null,
+ confirmTitle: String = "",
+ confirmAction: (() -> Unit)? = null
+ ) {
+ roomDialog.setContent(content)
+
+ if (cancelTitle.isNotBlank() && cancelAction != null) {
+ roomDialog.setCancel(cancelTitle, cancelAction)
+ }
+
+ if (confirmTitle.isNotBlank() && confirmAction != null) {
+ roomDialog.setConfirm(confirmTitle, confirmAction)
+ }
+
+ roomDialog.show(screenWidth)
+ }
+
+ private fun microphoneMute() {
+ isMicrophoneMute = !isMicrophoneMute
+ agora.muteLocalAudioStream(isMicrophoneMute)
+
+ if (isMicrophoneMute) {
+ speakerListAdapter.muteSpeakers.add(SharedPreferenceManager.userId.toInt())
+ } else {
+ speakerListAdapter.muteSpeakers.remove(SharedPreferenceManager.userId.toInt())
+ }
+ }
+
+ private fun speakerMute() {
+ isSpeakerMute = !isSpeakerMute
+ agora.muteAllRemoteAudioStreams(isSpeakerMute)
+ }
+
+ private fun inputChat() {
+ val nickname = viewModel.getUserNickname(SharedPreferenceManager.userId.toInt())
+ val profileUrl = viewModel.getUserProfileUrl(SharedPreferenceManager.userId.toInt())
+ val rank = viewModel.getUserRank(SharedPreferenceManager.userId)
+
+ if (binding.etChat.text.isNotBlank() && nickname.isNotBlank() && profileUrl.isNotBlank()) {
+ val message = binding.etChat.text.toString()
+ chatAdapter.items.add(
+ LiveRoomNormalChat(
+ userId = SharedPreferenceManager.userId,
+ profileUrl = profileUrl,
+ nickname = nickname,
+ rank = rank,
+ chat = message
+ )
+ )
+ invalidateChat()
+
+ agora.inputChat(message)
+ binding.etChat.setText("")
+ }
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ private fun invalidateChat() {
+ chatAdapter.notifyDataSetChanged()
+ val lastVisiblePosition = layoutManager
+ .findLastVisibleItemPosition()
+ val itemTotalCount = chatAdapter.itemCount - 1
+
+ if (
+ itemTotalCount > 0 &&
+ lastVisiblePosition > itemTotalCount - 5
+ ) {
+ layoutManager
+ .scrollToPosition(chatAdapter.itemCount - 1)
+ binding.tvNewChat.visibility = View.GONE
+ } else {
+ binding.tvNewChat.visibility = View.VISIBLE
+ }
+ }
+
+ private fun donation(can: Int, message: String) {
+ val rawMessage = "${can}캔을 후원하셨습니다.\uD83D\uDCB0\uD83E\uDE99"
+ val donationRawMessage = Gson().toJson(
+ LiveRoomChatRawMessage(
+ type = LiveRoomChatRawMessageType.DONATION,
+ message = rawMessage,
+ can = can,
+ donationMessage = message
+ )
+ )
+
+ viewModel.donation(roomId, can, message) {
+ agora.sendRawMessageToGroup(
+ rawMessage = donationRawMessage.toByteArray(),
+ onSuccess = {
+ handler.post {
+ val nickname =
+ viewModel.getUserNickname(SharedPreferenceManager.userId.toInt())
+ val profileUrl =
+ viewModel.getUserProfileUrl(SharedPreferenceManager.userId.toInt())
+ chatAdapter.items.add(
+ LiveRoomDonationChat(
+ profileUrl,
+ nickname,
+ rawMessage,
+ can,
+ donationMessage = message
+ )
+ )
+ invalidateChat()
+ viewModel.addDonationCan(can)
+ }
+ },
+ onFailure = {
+ viewModel.refundDonation(roomId)
+ }
+ )
+ }
+ }
+
+ private fun joinChannel(roomInfo: GetRoomInfoResponse) {
+ val userId = SharedPreferenceManager.userId
+ agora.joinRtcChannel(
+ uid = userId.toInt(),
+ rtcToken = roomInfo.rtcToken,
+ channelName = roomInfo.channelName
+ )
+
+ agora.createRtmChannelAndLogin(
+ uid = userId.toString(),
+ rtmToken = roomInfo.rtmToken,
+ channelName = roomInfo.channelName,
+ rtmChannelListener = object : RtmChannelListener {
+ override fun onMemberCountUpdated(i: Int) {
+ Logger.e("onMemberCountUpdated: $i")
+ }
+
+ override fun onAttributesUpdated(list: List?) {}
+
+ @SuppressLint("NotifyDataSetChanged")
+ override fun onMessageReceived(message: RtmMessage, fromMember: RtmChannelMember) {
+ Logger.e("onMessageReceived - message: ${message.text}")
+ Logger.e("onMessageReceived - messageType: ${message.messageType}")
+
+ val nickname = viewModel.getUserNickname(fromMember.userId!!.toInt())
+ val profileUrl = viewModel.getUserProfileUrl(fromMember.userId!!.toInt())
+ val rank = viewModel.getUserRank(fromMember.userId!!.toLong())
+
+ if (message.messageType == RtmMessageType.RAW) {
+ val rawMessage = Gson().fromJson(
+ String(message.rawMessage),
+ LiveRoomChatRawMessage::class.java
+ )
+
+ when (rawMessage.type) {
+ LiveRoomChatRawMessageType.EDIT_ROOM_INFO,
+ LiveRoomChatRawMessageType.SET_MANAGER -> {
+ handler.post {
+ viewModel.getRoomInfo(roomId)
+ }
+ }
+
+ LiveRoomChatRawMessageType.DONATION -> {
+ handler.post {
+ chatAdapter.items.add(
+ LiveRoomDonationChat(
+ profileUrl,
+ nickname,
+ rawMessage.message,
+ rawMessage.can,
+ rawMessage.donationMessage ?: ""
+ )
+ )
+ invalidateChat()
+ viewModel.addDonationCan(rawMessage.can)
+ }
+ }
+
+ LiveRoomChatRawMessageType.DONATION_STATUS -> {
+ handler.post {
+ chatAdapter.items.add(
+ LiveRoomDonationStatusChat(
+ donationStatusString = rawMessage.message
+ )
+ )
+ chatAdapter.notifyDataSetChanged()
+ invalidateChat()
+ }
+ }
+ }
+ } else {
+ val chat = message.text
+
+ if (chat.isNotBlank()) {
+ handler.post {
+ chatAdapter.items.add(
+ LiveRoomNormalChat(
+ userId = fromMember.userId.toLong(),
+ profileUrl = profileUrl,
+ nickname = nickname,
+ rank = rank,
+ chat = chat
+ )
+ )
+ invalidateChat()
+ }
+ }
+ }
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ override fun onMemberJoined(member: RtmChannelMember) {
+ Logger.e("onMemberJoined: ${member.userId}")
+ viewModel.getRoomInfo(roomId, member.userId.toInt()) {
+ if (it.isNotBlank()) {
+ chatAdapter.items.add(LiveRoomJoinChat(it))
+ invalidateChat()
+ }
+ }
+ }
+
+ override fun onMemberLeft(member: RtmChannelMember) {
+ Logger.e("onMemberLeft: ${member.userId}")
+ if (!viewModel.isEqualToHostId(member.userId.toInt())) {
+ viewModel.getRoomInfo(roomId)
+ }
+ }
+ },
+ rtmChannelJoinSuccess = {
+ if (userId == roomInfo.managerId) {
+ setBroadcaster()
+ } else {
+ setAudience()
+ }
+
+ val intent = Intent(this, SodaLiveService::class.java)
+ intent.putExtra("roomId", roomId)
+ intent.putExtra("content", "라이브 진행중 - ${roomInfo.title}")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(intent)
+ } else {
+ startService(intent)
+ }
+ },
+ rtmChannelJoinFail = {
+ agoraConnectFail()
+ }
+ )
+ }
+
+ private fun agoraConnectFail() {
+ showToast("라이브에 접속하지 못했습니다.\n다시 시도해 주세요.")
+ finish()
+ }
+
+ private val rtcEventHandler = object : IRtcEngineEventHandler() {
+ @SuppressLint("NotifyDataSetChanged")
+ override fun onAudioVolumeIndication(
+ speakers: Array,
+ totalVolume: Int
+ ) {
+ super.onAudioVolumeIndication(speakers, totalVolume)
+ val activeSpeakerIds = speakers
+ .asSequence()
+ .filter { it.volume > 0 }
+ .map { it.uid }
+ .toList()
+
+ Logger.e("onAudioVolumeIndication - $activeSpeakerIds")
+ handler.post {
+ speakerListAdapter.activeSpeakers.clear()
+ speakerListAdapter.activeSpeakers.addAll(activeSpeakerIds)
+
+ if (activeSpeakerIds.contains(0) && !isMicrophoneMute) {
+ speakerListAdapter.activeSpeakers.add(SharedPreferenceManager.userId.toInt())
+ }
+ speakerListAdapter.notifyDataSetChanged()
+ }
+ }
+
+ override fun onJoinChannelSuccess(channel: String, uid: Int, elapsed: Int) {
+ super.onJoinChannelSuccess(channel, uid, elapsed)
+ Logger.e("onJoinChannelSuccess - uid: $uid, channel: $channel")
+ }
+
+ override fun onActiveSpeaker(uid: Int) {
+ Logger.e("onActiveSpeaker - uid: $uid")
+ super.onActiveSpeaker(uid)
+ }
+
+ override fun onClientRoleChanged(
+ oldRole: Int,
+ newRole: Int,
+ newRoleOptions: ClientRoleOptions?
+ ) {
+ super.onClientRoleChanged(oldRole, newRole, newRoleOptions)
+ Logger.e(
+ "onClientRoleChanged - $oldRole - $newRole - " +
+ "${newRoleOptions?.audienceLatencyLevel}"
+ )
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ override fun onUserMuteAudio(uid: Int, muted: Boolean) {
+ super.onUserMuteAudio(uid, muted)
+ handler.post {
+ if (muted) {
+ speakerListAdapter.muteSpeakers.add(uid)
+ } else {
+ speakerListAdapter.muteSpeakers.remove(uid)
+ }
+ speakerListAdapter.notifyDataSetChanged()
+ }
+ Logger.e("onUserMuteAudio - uid: $uid, muted: $muted")
+ }
+
+ override fun onUserJoined(uid: Int, elapsed: Int) {
+ super.onUserJoined(uid, elapsed)
+ Logger.e("onUserJoined - uid: $uid")
+ viewModel.getRoomInfo(roomId)
+ speakerListAdapter.muteSpeakers.remove(uid)
+ }
+
+ override fun onUserOffline(uid: Int, reason: Int) {
+ super.onUserOffline(uid, reason)
+ Logger.e("onUserOffline - uid: $uid")
+ if (viewModel.isEqualToHostId(uid)) {
+ handler.post {
+ showToast("라이브가 종료되었습니다.")
+ finish()
+ }
+ } else {
+ viewModel.getRoomInfo(roomId)
+ speakerListAdapter.muteSpeakers.remove(uid)
+ }
+ }
+ }
+
+ private val rtmClientListener = object : RtmClientListener {
+ override fun onConnectionStateChanged(state: Int, reason: Int) {
+ val text =
+ "Connection state changed to $state Reason: $reason".trimIndent()
+
+ Logger.e(text)
+ }
+
+ override fun onTokenExpired() {}
+ override fun onTokenPrivilegeWillExpire() {}
+
+ override fun onPeersOnlineStatusChanged(map: Map?) {}
+ override fun onMessageReceived(rtmMessage: RtmMessage, peerId: String) {
+ Logger.e("text - ${rtmMessage.text}")
+ Logger.e("rawMessage - ${String(rtmMessage.rawMessage)}")
+ Logger.e("messageType - ${rtmMessage.messageType}")
+ if (rtmMessage.messageType == RtmMessageType.RAW) {
+ val rawMessage = String(rtmMessage.rawMessage)
+
+ if (rawMessage == LiveRoomRequestType.CHANGE_LISTENER.toString()) {
+ handler.post {
+ viewModel.setListener(
+ roomId,
+ SharedPreferenceManager.userId
+ ) {
+ setAudience()
+ viewModel.getRoomInfo(roomId)
+
+ if (roomUserProfileDialog.isShowing()) {
+ viewModel.getUserProfile(
+ roomId = roomId,
+ userId = peerId.toLong()
+ ) {}
+ }
+ }
+ }
+
+ return
+ }
+
+ if (rawMessage == LiveRoomRequestType.INVITE_SPEAKER.toString() &&
+ !isSpeaker
+ ) {
+ handler.post {
+ showDialog(
+ content = "스피커로 초대되었어요",
+ cancelTitle = "다음에요",
+ cancelAction = {},
+ confirmTitle = "스피커로 참여하기",
+ confirmAction = {
+ handler.post {
+ viewModel.setSpeaker(
+ roomId,
+ SharedPreferenceManager.userId
+ ) {
+ showDialog(content = "스피커가 되었어요!")
+ setBroadcaster()
+ viewModel.getRoomInfo(roomId)
+ }
+ }
+ }
+ )
+ }
+
+ return
+ }
+
+ if (rawMessage == LiveRoomRequestType.KICK_OUT.toString()) {
+ handler.post {
+ finish()
+ showToast(
+ "${viewModel.getManagerNickname()}님이 라이브에서 내보냈습니다."
+ )
+ }
+
+ return
+ }
+
+ if (rawMessage == LiveRoomRequestType.SET_MANAGER.toString()) {
+ if (isSpeaker) {
+ setAudience()
+ }
+ handler.post {
+ showDialog(
+ content = "${viewModel.getManagerNickname()}님이 스탭으로 지정했습니다."
+ )
+ }
+ return
+ }
+
+ if (rawMessage == LiveRoomRequestType.RELEASE_MANAGER.toString()) {
+ handler.post {
+ showDialog(
+ content = "${viewModel.getManagerNickname()}님이 스탭에서 해제했습니다."
+ )
+ }
+ viewModel.getRoomInfo(roomId = roomId)
+ return
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDialog.kt
new file mode 100644
index 0000000..44b15f0
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomDialog.kt
@@ -0,0 +1,99 @@
+package kr.co.vividnext.sodalive.live.room
+
+import android.app.Activity
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.WindowManager
+import android.widget.LinearLayout
+import androidx.appcompat.app.AlertDialog
+import kr.co.vividnext.sodalive.databinding.DialogLiveRoomBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+
+class LiveRoomDialog(
+ activity: Activity,
+ layoutInflater: LayoutInflater,
+) {
+
+ private val alertDialog: AlertDialog
+ private val dialogView = DialogLiveRoomBinding.inflate(layoutInflater)
+ private val handler = Handler(Looper.getMainLooper())
+
+ init {
+ val dialogBuilder = AlertDialog.Builder(activity)
+ dialogBuilder.setView(dialogView.root)
+
+ alertDialog = dialogBuilder.create()
+ alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ }
+
+ fun show(width: Int) {
+ if (!alertDialog.isShowing) {
+ alertDialog.show()
+
+ val lp = WindowManager.LayoutParams()
+ lp.copyFrom(alertDialog.window?.attributes)
+ lp.width = width - (53.4f.dpToPx()).toInt()
+ lp.height = WindowManager.LayoutParams.WRAP_CONTENT
+
+ alertDialog.window?.attributes = lp
+
+ if (dialogView.llActionButtons.visibility == View.GONE) {
+ alertDialog.setCancelable(true)
+ val llContentLp = dialogView.llContent.layoutParams as LinearLayout.LayoutParams
+ llContentLp.bottomMargin = 0f.dpToPx().toInt()
+ dialogView.llContent.layoutParams = llContentLp
+
+ handler.postDelayed({
+ dismiss()
+ }, 1000)
+ } else {
+ alertDialog.setCancelable(false)
+ val llContentLp = dialogView.llContent.layoutParams as LinearLayout.LayoutParams
+ llContentLp.bottomMargin = 10f.dpToPx().toInt()
+ dialogView.llContent.layoutParams = llContentLp
+ }
+ }
+ }
+
+ private fun dismiss() {
+ alertDialog.dismiss()
+ dialogView.tvContent.text = ""
+ dialogView.llActionButtons.visibility = View.GONE
+ dialogView.tvCancel.text = ""
+ dialogView.tvConfirm.text = ""
+ dialogView.tvCancel.setOnClickListener { }
+ dialogView.tvConfirm.setOnClickListener { }
+
+ val llContentLp = dialogView.llContent.layoutParams as LinearLayout.LayoutParams
+ llContentLp.bottomMargin = 10f.dpToPx().toInt()
+ dialogView.llContent.layoutParams = llContentLp
+ }
+
+ fun setCancel(title: String, action: () -> Unit) {
+ dialogView.tvCancel.text = title
+ dialogView.tvCancel.setOnClickListener {
+ dismiss()
+ action()
+ }
+
+ dialogView.llActionButtons.visibility = View.VISIBLE
+ }
+
+ fun setConfirm(title: String, action: () -> Unit) {
+ dialogView.tvConfirm.text = title
+ dialogView.tvConfirm.setOnClickListener {
+ dismiss()
+ action()
+ }
+
+ dialogView.llActionButtons.visibility = View.VISIBLE
+ }
+
+ fun setContent(content: String) {
+ dialogView.tvContent.text = content
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRequestType.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRequestType.kt
new file mode 100644
index 0000000..baabfe6
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomRequestType.kt
@@ -0,0 +1,11 @@
+package kr.co.vividnext.sodalive.live.room
+
+import com.google.gson.annotations.SerializedName
+
+enum class LiveRoomRequestType {
+ @SerializedName("INVITE_SPEAKER") INVITE_SPEAKER,
+ @SerializedName("CHANGE_LISTENER") CHANGE_LISTENER,
+ @SerializedName("KICK_OUT") KICK_OUT,
+ @SerializedName("SET_MANAGER") SET_MANAGER,
+ @SerializedName("RELEASE_MANAGER") RELEASE_MANAGER,
+}
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
new file mode 100644
index 0000000..acd4fdf
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt
@@ -0,0 +1,778 @@
+package kr.co.vividnext.sodalive.live.room
+
+import android.net.Uri
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.google.firebase.dynamiclinks.ShortDynamicLink
+import com.google.firebase.dynamiclinks.ktx.androidParameters
+import com.google.firebase.dynamiclinks.ktx.dynamicLinks
+import com.google.firebase.dynamiclinks.ktx.iosParameters
+import com.google.firebase.dynamiclinks.ktx.shortLinkAsync
+import com.google.firebase.ktx.Firebase
+import com.google.gson.Gson
+import com.orhanobut.logger.Logger
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kr.co.vividnext.sodalive.base.BaseViewModel
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.live.LiveRepository
+import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse
+import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse
+import kr.co.vividnext.sodalive.live.room.profile.GetLiveRoomUserProfileResponse
+import kr.co.vividnext.sodalive.live.room.update.EditLiveRoomInfoRequest
+import kr.co.vividnext.sodalive.report.ReportRepository
+import kr.co.vividnext.sodalive.report.ReportRequest
+import kr.co.vividnext.sodalive.report.ReportType
+import kr.co.vividnext.sodalive.user.UserRepository
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import java.io.File
+
+class LiveRoomViewModel(
+ private val repository: LiveRepository,
+ private val userRepository: UserRepository,
+ private val reportRepository: ReportRepository
+) : BaseViewModel() {
+ private val _roomInfoLiveData = MutableLiveData()
+ val roomInfoLiveData: LiveData
+ get() = _roomInfoLiveData
+
+ private val _toastLiveData = MutableLiveData()
+ val toastLiveData: LiveData
+ get() = _toastLiveData
+
+ private val _isShowNotice = MutableLiveData(true)
+ val isShowNotice: LiveData
+ get() = _isShowNotice
+
+ private val _isExpandNotice = MutableLiveData(false)
+ val isExpandNotice: LiveData
+ get() = _isExpandNotice
+
+ private val _totalDonationCan = MutableLiveData(0)
+ val totalDonationCan: LiveData
+ get() = _totalDonationCan
+
+ private val _userProfileLiveData = MutableLiveData()
+ val userProfileLiveData: LiveData
+ get() = _userProfileLiveData
+
+ lateinit var roomInfoResponse: GetRoomInfoResponse
+
+ fun isRoomInfoInitialized() = this::roomInfoResponse.isInitialized
+
+ private var _isLoading = MutableLiveData(false)
+ val isLoading: LiveData
+ get() = _isLoading
+
+ private var _isBgOn = MutableLiveData(true)
+ val isBgOn: LiveData
+ get() = _isBgOn
+
+ lateinit var getRealPathFromURI: (Uri) -> String?
+
+ fun getUserNickname(memberId: Int): String {
+ for (manager in roomInfoResponse.managerList) {
+ if (manager.id.toInt() == memberId) {
+ return manager.nickname
+ }
+ }
+
+ for (speaker in roomInfoResponse.speakerList) {
+ if (speaker.id.toInt() == memberId) {
+ return speaker.nickname
+ }
+ }
+
+ for (listener in roomInfoResponse.listenerList) {
+ if (listener.id.toInt() == memberId) {
+ return listener.nickname
+ }
+ }
+
+ return ""
+ }
+
+ fun getUserProfileUrl(accountId: Int): String {
+ for (manager in roomInfoResponse.managerList) {
+ if (manager.id.toInt() == accountId) {
+ return manager.profileImage
+ }
+ }
+
+ for (speaker in roomInfoResponse.speakerList) {
+ if (speaker.id.toInt() == accountId) {
+ return speaker.profileImage
+ }
+ }
+
+ for (listener in roomInfoResponse.listenerList) {
+ if (listener.id.toInt() == accountId) {
+ return listener.profileImage
+ }
+ }
+
+ return ""
+ }
+
+ fun getManagerNickname(): String {
+ return roomInfoResponse.managerNickname
+ }
+
+ fun setSpeaker(roomId: Long, userId: Long, onSuccess: () -> Unit) {
+ compositeDisposable.add(
+ repository.setSpeaker(roomId, userId, "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success) {
+ onSuccess()
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun setListener(roomId: Long, userId: Long, onSuccess: () -> Unit) {
+ compositeDisposable.add(
+ repository.setListener(roomId, userId, "Bearer ${SharedPreferenceManager.token}")
+ .retry(3)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success) {
+ onSuccess()
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun getRoomInfo(roomId: Long, userId: Int = 0, onSuccess: (String) -> Unit = {}) {
+ compositeDisposable.add(
+ repository.getRoomInfo(roomId, "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success && it.data != null) {
+ roomInfoResponse = it.data
+ Logger.e("data: ${it.data}")
+ _roomInfoLiveData.postValue(roomInfoResponse)
+
+ getTotalDonationCan(roomId = roomId)
+
+ if (userId > 0) {
+ val nickname = getUserNickname(userId)
+ onSuccess(nickname)
+ }
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun isEqualToHostId(accountId: Int): Boolean {
+ return accountId == roomInfoResponse.managerId.toInt()
+ }
+
+ fun getMemberCan() {
+ compositeDisposable.add(
+ userRepository.getMemberInfo(token = "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success && it.data != null) {
+ SharedPreferenceManager.can = it.data.can
+ }
+ },
+ {
+ }
+ )
+ )
+ }
+
+ fun shareRoomLink(
+ roomId: Long,
+ isPrivateRoom: Boolean,
+ password: String?,
+ onSuccess: (String) -> Unit
+ ) {
+ _isLoading.value = true
+ Firebase.dynamicLinks.shortLinkAsync(ShortDynamicLink.Suffix.SHORT) {
+ link = Uri.parse("https://yozm.day/?room_id=$roomId")
+ domainUriPrefix = "https://yozm.page.link"
+ androidParameters { }
+ iosParameters("kr.co.vividnext.yozm") {
+ appStoreId = "1630284226"
+ }
+ }.addOnSuccessListener {
+ val uri = it.shortLink
+ if (uri != null) {
+ val message = if (isPrivateRoom) {
+ "${SharedPreferenceManager.nickname}님이 귀하를 " +
+ "소다라이브의 비공개라이브에 초대하였습니다.\n" +
+ "※ 라이브 참여: $uri\n" +
+ "(입장 비밀번호 : $password)"
+ } else {
+ "${SharedPreferenceManager.nickname}님이 귀하를 " +
+ "소다라이브의 공개라이브에 초대하였습니다.\n" +
+ "※ 라이브 참여: $uri"
+ }
+
+ onSuccess(message)
+ }
+ }.addOnFailureListener {
+ _toastLiveData.postValue("공유링크를 생성하지 못했습니다.\n다시 시도해 주세요.")
+ }.addOnCompleteListener {
+ _isLoading.value = false
+ }
+ }
+
+ fun creatorFollow(creatorId: Long, roomId: Long, isGetUserProfile: Boolean = false) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.creatorFollow(
+ creatorId,
+ "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success && it.data != null) {
+ getRoomInfo(roomId)
+
+ if (isGetUserProfile) {
+ getUserProfile(roomId = roomId, userId = creatorId) {}
+ }
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ _isLoading.value = false
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun creatorUnFollow(creatorId: Long, roomId: Long, isGetUserProfile: Boolean = false) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.creatorUnFollow(
+ creatorId,
+ "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success && it.data != null) {
+ getRoomInfo(roomId)
+
+ if (isGetUserProfile) {
+ getUserProfile(roomId = roomId, userId = creatorId) {}
+ }
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ _isLoading.value = false
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun toggleShowNotice() {
+ _isShowNotice.value = !isShowNotice.value!!
+ _isExpandNotice.value = false
+ }
+
+ fun toggleExpandNotice() {
+ _isExpandNotice.value = !isExpandNotice.value!!
+ }
+
+ fun toggleBackgroundImage() {
+ _isBgOn.value = !isBgOn.value!!
+ }
+
+ fun editLiveRoomInfo(
+ roomId: Long,
+ newTitle: String,
+ newContent: String,
+ newCoverImageUri: Uri? = null,
+ onSuccess: () -> Unit
+ ) {
+ val request = EditLiveRoomInfoRequest(
+ title = if (newTitle != roomInfoResponse.title) {
+ newTitle
+ } else {
+ null
+ },
+ notice = if (newContent != roomInfoResponse.notice) {
+ newContent
+ } else {
+ null
+ },
+ numberOfPeople = null,
+ beginDateTimeString = null,
+ timezone = null
+ )
+
+ val requestJson = if (request.title != null || request.notice != null) {
+ Gson().toJson(request)
+ } else {
+ null
+ }
+
+ val coverImage = if (newCoverImageUri != null) {
+ val file = File(getRealPathFromURI(newCoverImageUri!!))
+ MultipartBody.Part.createFormData(
+ "coverImage",
+ file.name,
+ file.asRequestBody("image/*".toMediaType())
+ )
+ } else {
+ null
+ }
+
+ if (coverImage == null && requestJson == null) {
+ _toastLiveData.value = "변경사항이 없습니다."
+ return
+ }
+
+ compositeDisposable.add(
+ repository.editLiveRoomInfo(
+ roomId = roomId,
+ coverImage = coverImage,
+ request = requestJson?.toRequestBody("text/plain".toMediaType()),
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success) {
+ getRoomInfo(roomId = roomId)
+ onSuccess()
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue(
+ "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요."
+ )
+ }
+ )
+ )
+ }
+
+ fun quitRoom(roomId: Long, onSuccess: () -> Unit) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.quitRoom(roomId = roomId, token = "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success) {
+ onSuccess()
+ _isLoading.value = false
+ } else {
+ _isLoading.value = false
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun kickOut(roomId: Long, userId: Long) {
+ compositeDisposable.add(
+ repository.kickOut(roomId, userId, token = "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe({}, {})
+ )
+ }
+
+ fun donation(roomId: Long, can: Int, message: String, onSuccess: () -> Unit) {
+ _isLoading.postValue(true)
+ compositeDisposable.add(
+ repository.donation(roomId, can, message, "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+ if (it.success) {
+ SharedPreferenceManager.can -= can
+ onSuccess()
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun refundDonation(roomId: Long) {
+ _isLoading.postValue(true)
+
+ compositeDisposable.add(
+ repository.refundDonation(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고객센터로 문의해주세요."
+ )
+ }
+ )
+ )
+ }
+
+ fun addDonationCan(can: Int) {
+ _totalDonationCan.postValue(totalDonationCan.value!! + can)
+ }
+
+ fun donationStatus(roomId: Long, onSuccess: (GetLiveRoomDonationStatusResponse) -> Unit) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.donationStatus(roomId, token = "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+ if (it.success && it.data != null) {
+ onSuccess(it.data)
+ } else {
+ _toastLiveData.postValue(
+ "후원현황을 가져오지 못했습니다\n다시 시도해 주세요."
+ )
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue(
+ "후원현황을 가져오지 못했습니다\n다시 시도해 주세요."
+ )
+ }
+ )
+ )
+ }
+
+ fun getUserRank(userId: Long): Int {
+ // 방장 -> -2
+ // 스탭 -> -3
+ // 나머지 -> 체크
+ return if (isEqualToHostId(userId.toInt())) {
+ -2
+ } else if (isEqualToManagerId(userId.toInt())) {
+ -3
+ } else {
+ roomInfoResponse.donationRankingTop3UserIds.indexOf(userId)
+ }
+ }
+
+ private fun getTotalDonationCan(roomId: Long) {
+ compositeDisposable.add(
+ repository.getTotalDonationCan(
+ roomId = roomId,
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.success && it.data != null) {
+ _totalDonationCan.postValue(it.data.totalDonationCan)
+ }
+ },
+ {
+ }
+ )
+ )
+ }
+
+ fun getUserProfile(roomId: Long, userId: Long, onSuccess: () -> Unit) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.getUserProfile(
+ roomId = roomId,
+ userId = userId,
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+ if (it.success && it.data != null) {
+ _userProfileLiveData.value = it.data!!
+ onSuccess()
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun setManager(roomId: Long, userId: Long, onSuccess: () -> Unit) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.setManager(roomId, userId, "Bearer ${SharedPreferenceManager.token}")
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+ if (it.success) {
+ getRoomInfo(roomId)
+ onSuccess()
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun isEqualToManagerId(accountId: Int): Boolean {
+ for (manager in roomInfoResponse.managerList) {
+ if (manager.id == accountId.toLong()) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ fun memberBlock(userId: Long, onSuccess: () -> Unit) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ userRepository.memberBlock(
+ userId = userId,
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+
+ if (it.success) {
+ _toastLiveData.postValue("차단하였습니다.")
+ onSuccess()
+ } else {
+ val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ _toastLiveData.postValue(message)
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun memberUnBlock(userId: Long) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ userRepository.memberUnBlock(
+ userId = userId,
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+
+ if (it.success) {
+ getUserProfile(roomId = roomInfoResponse.roomId, userId = userId) {}
+ _toastLiveData.postValue("차단이 해제 되었습니다.")
+ } else {
+ val message = it.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ _toastLiveData.postValue(message)
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun report(type: ReportType, userId: Long, reason: String = "프로필 신고") {
+ _isLoading.value = true
+
+ val request = ReportRequest(type, reason, reportedAccountId = userId)
+ compositeDisposable.add(
+ reportRepository.report(
+ request = request,
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "신고가 접수되었습니다."
+ )
+ }
+
+ _isLoading.value = false
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("신고가 접수되었습니다.")
+ }
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt
new file mode 100644
index 0000000..b701b92
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt
@@ -0,0 +1,8 @@
+package kr.co.vividnext.sodalive.live.room
+
+import com.google.gson.annotations.SerializedName
+
+data class SetManagerOrSpeakerOrAudienceRequest(
+ @SerializedName("roomId") val roomId: Long,
+ @SerializedName("accountId") val accountId: Long
+)
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
new file mode 100644
index 0000000..32c801d
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChat.kt
@@ -0,0 +1,336 @@
+package kr.co.vividnext.sodalive.live.room.chat
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.TextUtils
+import android.text.style.ForegroundColorSpan
+import android.view.View
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.view.setPadding
+import androidx.viewbinding.ViewBinding
+import coil.load
+import coil.transform.RoundedCornersTransformation
+import com.google.gson.annotations.SerializedName
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.common.CustomTypefaceSpan
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomChatBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDonationStatusChatBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomJoinChatBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.extensions.moneyFormat
+import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse
+
+enum class LiveRoomChatType {
+ @SerializedName("CHAT")
+ CHAT,
+
+ @SerializedName("DONATION_STATUS")
+ DONATION_STATUS,
+
+ @SerializedName("JOIN")
+ JOIN
+}
+
+abstract class LiveRoomChat {
+ open var type: LiveRoomChatType = LiveRoomChatType.CHAT
+ abstract fun bind(
+ context: Context,
+ binding: ViewBinding,
+ onClickProfile: ((Long) -> Unit)? = null
+ )
+}
+
+data class LiveRoomJoinChat(
+ val nickname: String
+) : LiveRoomChat() {
+ override var type = LiveRoomChatType.JOIN
+ override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) {
+ val str = "'$nickname'님이 입장하셨습니다."
+ val spStr = SpannableString(str)
+
+ spStr.setSpan(
+ ForegroundColorSpan(
+ ContextCompat.getColor(
+ context,
+ R.color.color_ffdc00
+ )
+ ),
+ str.indexOf("'") + 1,
+ str.indexOf("'님"),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+
+ spStr.setSpan(
+ CustomTypefaceSpan(
+ ResourcesCompat.getFont(
+ context,
+ R.font.gmarket_sans_bold
+ )
+ ),
+ str.indexOf("'"),
+ str.indexOf("'님"),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+
+ (binding as ItemLiveRoomJoinChatBinding).tvJoin.text = spStr
+ }
+}
+
+data class LiveRoomDonationStatusChat(
+ val response: GetLiveRoomDonationStatusResponse? = null,
+ val donationStatusString: String? = null
+) : LiveRoomChat() {
+ override var type = LiveRoomChatType.DONATION_STATUS
+ override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) {
+ if (donationStatusString != null) {
+ (binding as ItemLiveRoomDonationStatusChatBinding)
+ .tvDonationList
+ .text = donationStatusString
+ } else {
+ (binding as ItemLiveRoomDonationStatusChatBinding)
+ .tvDonationList
+ .text = getDonationString(context)
+ }
+ }
+
+ fun getDonationString(context: Context): String {
+ if (response != null) {
+ var donationStatusString: CharSequence = "[현재 라이브 후원현황]\n\n"
+ for (index in response.donationList.indices) {
+ val donation = response.donationList[index]
+ val spChars = SpannableString(
+ "${index + 1}. " +
+ if (donation.nickname.length > 10) {
+ "${donation.nickname.substring(0, 10)} : "
+ } else {
+ "${donation.nickname} : "
+ } + "(${donation.can.moneyFormat()} 캔)\n"
+ )
+ spChars.setSpan(
+ ForegroundColorSpan(
+ ContextCompat.getColor(
+ context,
+ R.color.color_fdca2f
+ )
+ ),
+ spChars.indexOf(": (") + 3,
+ spChars.indexOf(")"),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+
+ donationStatusString = TextUtils.concat(
+ donationStatusString, spChars
+ )
+ }
+
+ donationStatusString = TextUtils.concat(
+ donationStatusString,
+ "\n-------------------------\n\n"
+ )
+
+ donationStatusString = TextUtils.concat(
+ donationStatusString,
+ "후원인원 : ${response.totalCount} 명\n"
+ )
+
+ donationStatusString = TextUtils.concat(
+ donationStatusString,
+ "후원합계 : ${response.totalCan.moneyFormat()} 캔"
+ )
+
+ return donationStatusString.toString()
+ } else {
+ return ""
+ }
+ }
+}
+
+data class LiveRoomNormalChat(
+ @SerializedName("userId") val userId: Long,
+ @SerializedName("profileUrl") val profileUrl: String,
+ @SerializedName("nickname") val nickname: String,
+ @SerializedName("rank") val rank: Int,
+ @SerializedName("chat") val chat: String,
+) : LiveRoomChat() {
+ override fun bind(context: Context, binding: ViewBinding, onClickProfile: ((Long) -> Unit)?) {
+ val itemBinding = binding as ItemLiveRoomChatBinding
+ itemBinding.ivProfile.load(profileUrl) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(RoundedCornersTransformation(23.3f.dpToPx()))
+ }
+
+ itemBinding.ivProfile.setOnClickListener {
+ if (onClickProfile != null && userId != SharedPreferenceManager.userId) {
+ onClickProfile(userId)
+ }
+ }
+
+ itemBinding.tvChat.text = chat
+ itemBinding.tvNickname.text = nickname
+
+ itemBinding.ivBg.visibility = View.VISIBLE
+ itemBinding.tvCreatorOrManager.visibility = View.GONE
+
+ when (rank + 1) {
+ -2 -> {
+ itemBinding.ivBg.setImageResource(R.drawable.bg_circle_4999e3)
+ itemBinding.ivCrown.setImageResource(R.drawable.ic_badge_manager)
+ itemBinding.tvCreatorOrManager.setBackgroundResource(
+ R.drawable.bg_round_corner_2_4999e3
+ )
+ itemBinding.tvCreatorOrManager.text = "스탭"
+
+ itemBinding.ivCrown.visibility = View.VISIBLE
+ itemBinding.tvCreatorOrManager.visibility = View.VISIBLE
+ }
+
+ -1 -> {
+ itemBinding.ivBg.setImageResource(R.drawable.bg_circle_6f3dec_9970ff)
+ itemBinding.ivCrown.setImageResource(R.drawable.ic_crown)
+ itemBinding.ivCrown.visibility = View.VISIBLE
+ }
+
+ 1 -> {
+ itemBinding.ivBg.setImageResource(R.drawable.bg_circle_ffdc00_fdca2f)
+ itemBinding.ivCrown.setImageResource(R.drawable.ic_crown_1)
+ itemBinding.ivCrown.visibility = View.VISIBLE
+ }
+
+ 2 -> {
+ itemBinding.ivBg.setImageResource(R.drawable.bg_circle_9f9f9f_dcdcdc)
+ itemBinding.ivCrown.setImageResource(R.drawable.ic_crown_2)
+ itemBinding.ivCrown.visibility = View.VISIBLE
+ }
+
+ 3 -> {
+ itemBinding.ivBg.setImageResource(R.drawable.bg_circle_e5a578_c67e4a)
+ itemBinding.ivCrown.setImageResource(R.drawable.ic_crown_3)
+ itemBinding.ivCrown.visibility = View.VISIBLE
+ }
+
+ else -> {
+ itemBinding.ivBg.setImageResource(R.drawable.bg_circle_9f9f9f_bbbbbb)
+ itemBinding.ivCrown.visibility = View.GONE
+ }
+ }
+
+ val messageHorizontalPadding = 8.dpToPx().toInt()
+ val messageVerticalPadding = 5.3f.dpToPx().toInt()
+ itemBinding.llMessageBg.setPadding(
+ messageHorizontalPadding,
+ messageVerticalPadding,
+ messageHorizontalPadding,
+ messageVerticalPadding
+ )
+
+ if (SharedPreferenceManager.userId == userId) {
+ itemBinding.llMessageBg.setBackgroundResource(R.drawable.bg_round_corner_3_3_999970ff)
+ } else {
+ itemBinding.llMessageBg.setBackgroundResource(R.drawable.bg_round_corner_3_3_99000000)
+ }
+ itemBinding.ivCoin.visibility = View.GONE
+ itemBinding.tvDonationMessage.visibility = View.GONE
+ itemBinding.root.setBackgroundResource(0)
+ itemBinding.root.setPadding(0)
+ }
+}
+
+data class LiveRoomDonationChat(
+ @SerializedName("profileUrl") val profileUrl: String,
+ @SerializedName("nickname") val nickname: String,
+ @SerializedName("chat") val chat: String,
+ @SerializedName("coin") val coin: Int,
+ @SerializedName("donationMessage") val donationMessage: String,
+) : LiveRoomChat() {
+ @SuppressLint("SetTextI18n")
+ 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_fdca2f
+ )
+ ),
+ 0,
+ chat.indexOf("코인", 0, true) + 2,
+ 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.bg_placeholder)
+ transformations(RoundedCornersTransformation(23.3f.dpToPx()))
+ }
+ itemBinding.tvChat.text = spChat
+ itemBinding.tvNickname.text = spNickname
+ itemBinding.ivProfile.setOnClickListener {}
+
+ itemBinding.ivCoin.visibility = View.VISIBLE
+ itemBinding.ivBg.visibility = View.GONE
+ itemBinding.ivCrown.visibility = View.GONE
+ itemBinding.tvCreatorOrManager.visibility = View.GONE
+
+ if (donationMessage.isNotBlank()) {
+ itemBinding.tvDonationMessage.text = "\"$donationMessage\""
+ itemBinding.tvDonationMessage.visibility = View.VISIBLE
+ } else {
+ itemBinding.tvDonationMessage.visibility = View.GONE
+ }
+
+ itemBinding.llMessageBg.setPadding(0)
+ itemBinding.llMessageBg.background = null
+
+ itemBinding.root.setBackgroundResource(
+ when {
+ coin >= 100000 -> {
+ R.drawable.bg_round_corner_6_7_c25264
+ }
+
+ coin >= 50000 -> {
+ R.drawable.bg_round_corner_6_7_e6d85e37
+ }
+
+ coin >= 10000 -> {
+ R.drawable.bg_round_corner_6_7_e6d38c38
+ }
+
+ coin >= 5000 -> {
+ R.drawable.bg_round_corner_6_7_e659548f
+ }
+
+ coin >= 1000 -> {
+ R.drawable.bg_round_corner_6_7_e64d6aa4
+ }
+
+ coin >= 500 -> {
+ R.drawable.bg_round_corner_6_7_e62d7390
+ }
+
+ else -> {
+ R.drawable.bg_round_corner_6_7_e6548f7d
+ }
+ }
+ )
+ itemBinding.root.setPadding(33)
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatAdapter.kt
new file mode 100644
index 0000000..020952a
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatAdapter.kt
@@ -0,0 +1,99 @@
+package kr.co.vividnext.sodalive.live.room.chat
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomChatBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDonationStatusChatBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomJoinChatBinding
+
+class LiveRoomChatAdapter(
+ private val onClickProfile: (Long) -> Unit
+) : RecyclerView.Adapter() {
+
+ val items = mutableListOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveRoomChatViewHolder {
+ when (viewType) {
+ LiveRoomChatType.DONATION_STATUS.ordinal -> {
+ return LiveRoomDonationStatusChatViewHolder(
+ parent.context,
+ ItemLiveRoomDonationStatusChatBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ }
+
+ LiveRoomChatType.JOIN.ordinal -> {
+ return LiveRoomJoinChatViewHolder(
+ parent.context,
+ ItemLiveRoomJoinChatBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ }
+
+ else -> {
+ return LiveRoomNormalChatViewHolder(
+ parent.context,
+ ItemLiveRoomChatBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ }
+ }
+ }
+
+ override fun onBindViewHolder(holder: LiveRoomChatViewHolder, position: Int) {
+ holder.bind(items[position], onClickProfile)
+ }
+
+ override fun getItemCount() = items.count()
+
+ override fun getItemViewType(position: Int): Int {
+ return items[position].type.ordinal
+ }
+}
+
+abstract class LiveRoomChatViewHolder(binding: ViewBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ abstract fun bind(chat: LiveRoomChat, onClickProfile: ((Long) -> Unit)? = null)
+}
+
+class LiveRoomNormalChatViewHolder(
+ private val context: Context,
+ private val binding: ItemLiveRoomChatBinding
+) : LiveRoomChatViewHolder(binding) {
+ override fun bind(
+ chat: LiveRoomChat,
+ onClickProfile: ((Long) -> Unit)?
+ ) = chat.bind(context, binding, onClickProfile)
+}
+
+class LiveRoomDonationStatusChatViewHolder(
+ private val context: Context,
+ private val binding: ItemLiveRoomDonationStatusChatBinding
+) : LiveRoomChatViewHolder(binding) {
+ override fun bind(
+ chat: LiveRoomChat,
+ onClickProfile: ((Long) -> Unit)?
+ ) = chat.bind(context, binding)
+}
+
+class LiveRoomJoinChatViewHolder(
+ private val context: Context,
+ private val binding: ItemLiveRoomJoinChatBinding
+) : LiveRoomChatViewHolder(binding) {
+ override fun bind(
+ chat: LiveRoomChat,
+ onClickProfile: ((Long) -> Unit)?
+ ) = chat.bind(context, binding)
+}
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
new file mode 100644
index 0000000..26c32ac
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/chat/LiveRoomChatRawMessage.kt
@@ -0,0 +1,17 @@
+package kr.co.vividnext.sodalive.live.room.chat
+
+import com.google.gson.annotations.SerializedName
+
+data class LiveRoomChatRawMessage(
+ @SerializedName("type") val type: LiveRoomChatRawMessageType,
+ @SerializedName("message") val message: String,
+ @SerializedName("can") val can: Int,
+ @SerializedName("donationMessage") val donationMessage: String?
+)
+
+enum class LiveRoomChatRawMessageType {
+ @SerializedName("DONATION") DONATION,
+ @SerializedName("SET_MANAGER") SET_MANAGER,
+ @SerializedName("EDIT_ROOM_INFO") EDIT_ROOM_INFO,
+ @SerializedName("DONATION_STATUS") DONATION_STATUS
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/DeleteLiveRoomDonationMessage.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/DeleteLiveRoomDonationMessage.kt
new file mode 100644
index 0000000..717c063
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/DeleteLiveRoomDonationMessage.kt
@@ -0,0 +1,8 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import com.google.gson.annotations.SerializedName
+
+data class DeleteLiveRoomDonationMessage(
+ @SerializedName("roomId") val roomId: Long,
+ @SerializedName("messageUUID") val messageUUID: String
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationStatusResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationStatusResponse.kt
new file mode 100644
index 0000000..138859a
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationStatusResponse.kt
@@ -0,0 +1,16 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import com.google.gson.annotations.SerializedName
+
+data class GetLiveRoomDonationStatusResponse(
+ @SerializedName("donationList") val donationList: List,
+ @SerializedName("totalCount") val totalCount: Int,
+ @SerializedName("totalCan") val totalCan: Int
+)
+
+data class GetLiveRoomDonationItem(
+ @SerializedName("profileImage") val profileImage: String,
+ @SerializedName("nickname") val nickname: String,
+ @SerializedName("userId") val userId: Long,
+ @SerializedName("can") val can: Int
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt
new file mode 100644
index 0000000..8e9bd14
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt
@@ -0,0 +1,7 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import com.google.gson.annotations.SerializedName
+
+data class GetLiveRoomDonationTotalResponse(
+ @SerializedName("totalDonationCan") val totalDonationCan: Int
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationDialog.kt
new file mode 100644
index 0000000..4a9b464
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationDialog.kt
@@ -0,0 +1,108 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import coil.load
+import coil.transform.CircleCropTransformation
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.common.Constants
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.databinding.DialogLiveRoomDonationBinding
+import kr.co.vividnext.sodalive.extensions.moneyFormat
+import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
+
+class LiveRoomDonationDialog(
+ private val activity: AppCompatActivity,
+ layoutInflater: LayoutInflater,
+ onClickDonation: (Int, String) -> Unit
+) {
+
+ private val bottomSheetDialog: BottomSheetDialog = BottomSheetDialog(activity)
+ private val dialogView = DialogLiveRoomDonationBinding.inflate(layoutInflater)
+
+ init {
+ bottomSheetDialog.setContentView(dialogView.root)
+ bottomSheetDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+ bottomSheetDialog.setCancelable(false)
+
+ dialogView.tvCancel.setOnClickListener { bottomSheetDialog.dismiss() }
+ dialogView.tvDonation.setOnClickListener {
+ try {
+ val coin = dialogView.etDonationCoin.text.toString().toInt()
+ val message = dialogView.etDonationMessage.text.toString()
+
+ if (coin > 0) {
+ bottomSheetDialog.dismiss()
+ onClickDonation(coin, message)
+ } else {
+ Toast.makeText(
+ activity,
+ "1코인 이상 후원하실 수 있습니다.",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ } catch (e: NumberFormatException) {
+ Toast.makeText(
+ activity,
+ "1코인 이상 후원하실 수 있습니다.",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+
+ setupView()
+ }
+
+ fun show(width: Int) {
+ if (!bottomSheetDialog.isShowing) {
+ bottomSheetDialog.show()
+
+ val lp = WindowManager.LayoutParams()
+ lp.copyFrom(bottomSheetDialog.window?.attributes)
+ lp.width = width
+ lp.height = WindowManager.LayoutParams.WRAP_CONTENT
+
+ bottomSheetDialog.window?.attributes = lp
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun setupView() {
+ dialogView.tvCoin.text = SharedPreferenceManager.can.moneyFormat()
+ dialogView.tvPlus10.setOnClickListener { addCoin(10) }
+ dialogView.tvPlus100.setOnClickListener { addCoin(100) }
+ dialogView.tvPlus1000.setOnClickListener { addCoin(1000) }
+ dialogView.tvPlus10000.setOnClickListener { addCoin(10000) }
+
+ dialogView.ivProfile.load(SharedPreferenceManager.profileImage) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(CircleCropTransformation())
+ }
+
+ dialogView.tvCoin.setOnClickListener {
+ bottomSheetDialog.dismiss()
+
+ val intent = Intent(activity, CanChargeActivity::class.java)
+ intent.putExtra(Constants.EXTRA_PREV_LIVE_ROOM, true)
+ activity.startActivity(intent)
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun addCoin(coin: Int) {
+ try {
+ val currentCoin = dialogView.etDonationCoin.text.toString().toInt()
+ dialogView.etDonationCoin.setText((currentCoin + coin).toString())
+ } catch (e: NumberFormatException) {
+ dialogView.etDonationCoin.setText(coin.toString())
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt
new file mode 100644
index 0000000..f5c9fb5
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt
@@ -0,0 +1,10 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import com.google.gson.annotations.SerializedName
+
+data class LiveRoomDonationMessage(
+ @SerializedName("uuid") val uuid: String,
+ @SerializedName("nickname") val nickname: String,
+ @SerializedName("canMessage") val canMessage: String,
+ @SerializedName("donationMessage") val donationMessage: String
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageAdapter.kt
new file mode 100644
index 0000000..17c1338
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageAdapter.kt
@@ -0,0 +1,62 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDonationMessageBinding
+
+class LiveRoomDonationMessageAdapter(
+ private val onClickDeleteMessage: (String) -> Unit,
+ private val copyMessage: (String) -> Unit
+) : RecyclerView.Adapter() {
+
+ private var items = mutableListOf()
+
+ inner class ViewHolder(
+ private val binding: ItemLiveRoomDonationMessageBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+
+ @SuppressLint("SetTextI18n")
+ fun bind(item: LiveRoomDonationMessage) {
+ binding.tvNickname.text = "${item.nickname}님이"
+ binding.tvCoinMessage.text = item.canMessage
+ binding.tvDonationMessage.text = "\"${item.donationMessage}\""
+ binding.ivDelete.setOnClickListener { onClickDeleteMessage(item.uuid) }
+
+ binding.root.setOnClickListener { copyMessage(item.donationMessage) }
+ }
+ }
+
+ fun update(items: List) {
+ items.let {
+ val diffCallback = LiveRoomDonationMessageDiffUtilCallback(this.items, items)
+ val diffResult = DiffUtil.calculateDiff(diffCallback)
+
+ this.items.run {
+ clear()
+ addAll(it)
+ diffResult.dispatchUpdatesTo(this@LiveRoomDonationMessageAdapter)
+ }
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ ItemLiveRoomDonationMessageBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(items[position])
+ }
+
+ override fun getItemCount(): Int {
+ return items.size
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDialog.kt
new file mode 100644
index 0000000..3041a7f
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDialog.kt
@@ -0,0 +1,115 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import android.annotation.SuppressLint
+import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.Window
+import android.view.WindowManager
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.LiveData
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kr.co.vividnext.sodalive.databinding.DialogLiveRoomDonationMessageBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.extensions.moneyFormat
+
+class LiveRoomDonationMessageDialog(
+ layoutInflater: LayoutInflater,
+ private val activity: FragmentActivity,
+ private val donationMessageListLiveData: LiveData>,
+ private val donationMessageCountLiveData: LiveData,
+ private val getDonationMessageList: () -> Unit,
+ private val deleteDonationMessage: (String) -> Unit,
+ copyMessage: (String) -> Unit
+) {
+ private val alertDialog: AlertDialog
+ private val dialogView = DialogLiveRoomDonationMessageBinding.inflate(layoutInflater)
+ private val adapter = LiveRoomDonationMessageAdapter(
+ onClickDeleteMessage = { deleteDonationMessage(it) },
+ copyMessage = copyMessage
+ )
+
+ init {
+ val dialogBuilder = AlertDialog.Builder(activity)
+ dialogBuilder.setView(dialogView.root)
+
+ alertDialog = dialogBuilder.create()
+ alertDialog.setCancelable(false)
+ alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ alertDialog.setOnShowListener {
+ val lp = WindowManager.LayoutParams()
+ lp.copyFrom(alertDialog.window?.attributes)
+ lp.width = activity.resources.displayMetrics.widthPixels - (26.7f.dpToPx()).toInt()
+ lp.height = activity.resources.displayMetrics.heightPixels - (200f.dpToPx()).toInt()
+
+ alertDialog.window?.attributes = lp
+ }
+
+ dialogView.tvClose.setOnClickListener { alertDialog.dismiss() }
+ setupRecyclerView()
+ bindData()
+ }
+
+ private fun setupRecyclerView() {
+ dialogView.rvDonationMessage.isNestedScrollingEnabled = true
+ dialogView.rvDonationMessage.layoutManager = LinearLayoutManager(activity)
+ dialogView.rvDonationMessage.addItemDecoration(object : RecyclerView.ItemDecoration() {
+ override fun getItemOffsets(
+ outRect: Rect,
+ view: View,
+ parent: RecyclerView,
+ state: RecyclerView.State
+ ) {
+ super.getItemOffsets(outRect, view, parent, state)
+
+ when (parent.getChildAdapterPosition(view)) {
+ 0 -> {
+ outRect.top = 0.dpToPx().toInt()
+ outRect.bottom = 4.dpToPx().toInt()
+ }
+
+ adapter.itemCount - 1 -> {
+ outRect.top = 4.dpToPx().toInt()
+ outRect.bottom = 0.dpToPx().toInt()
+ }
+
+ else -> {
+ outRect.top = 4.dpToPx().toInt()
+ outRect.bottom = 4.dpToPx().toInt()
+ }
+ }
+ }
+ })
+ dialogView.rvDonationMessage.adapter = adapter
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun bindData() {
+ donationMessageListLiveData.observe(activity) {
+ if (it.isEmpty()) {
+ dialogView.rvDonationMessage.visibility = View.GONE
+ dialogView.tvNone.visibility = View.VISIBLE
+ } else {
+ dialogView.rvDonationMessage.visibility = View.VISIBLE
+ dialogView.tvNone.visibility = View.GONE
+ }
+
+ adapter.update(it)
+ }
+
+ donationMessageCountLiveData.observe(activity) {
+ dialogView.tvDonationMessageCount.text = "(${it.moneyFormat()})"
+ }
+ }
+
+ fun show() {
+ alertDialog.show()
+ getDonationMessageList()
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDiffUtilCallback.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDiffUtilCallback.kt
new file mode 100644
index 0000000..ff9e118
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageDiffUtilCallback.kt
@@ -0,0 +1,24 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import androidx.recyclerview.widget.DiffUtil
+
+class LiveRoomDonationMessageDiffUtilCallback(
+ private val oldData: List,
+ private val newData: List
+) : DiffUtil.Callback() {
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ val oldItem = oldData[oldItemPosition]
+ val newItem = newData[newItemPosition]
+
+ return oldItem.uuid == newItem.uuid
+ }
+
+ override fun getOldListSize(): Int = oldData.size
+
+ override fun getNewListSize(): Int = newData.size
+
+ override fun areContentsTheSame(
+ oldItemPosition: Int,
+ newItemPosition: Int
+ ) = oldData[oldItemPosition] == newData[newItemPosition]
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageViewModel.kt
new file mode 100644
index 0000000..ca07c57
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessageViewModel.kt
@@ -0,0 +1,96 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.orhanobut.logger.Logger
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
+import io.reactivex.rxjava3.schedulers.Schedulers
+import kr.co.vividnext.sodalive.base.BaseViewModel
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.live.LiveRepository
+
+class LiveRoomDonationMessageViewModel(private val repository: LiveRepository) : BaseViewModel() {
+ private val _donationMessageListLiveData = MutableLiveData>()
+ val donationMessageListLiveData: LiveData>
+ get() = _donationMessageListLiveData
+
+ private val _donationMessageCountLiveData = MutableLiveData(0)
+ val donationMessageCountLiveData: LiveData
+ get() = _donationMessageCountLiveData
+
+ private val _toastLiveData = MutableLiveData()
+ val toastLiveData: LiveData
+ get() = _toastLiveData
+
+ private var _isLoading = MutableLiveData(false)
+ val isLoading: LiveData
+ get() = _isLoading
+
+ fun getDonationMessageList(roomId: Long) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.getDonationMessageList(
+ roomId = roomId,
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+ if (it.success && it.data != null) {
+ _donationMessageListLiveData.postValue(it.data!!)
+ _donationMessageCountLiveData.postValue(it.data!!.size)
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+
+ fun deleteDonationMessage(roomId: Long, uuid: String) {
+ _isLoading.value = true
+ compositeDisposable.add(
+ repository.deleteDonationMessage(
+ roomId = roomId,
+ uuid = uuid,
+ token = "Bearer ${SharedPreferenceManager.token}"
+ )
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ {
+ _isLoading.value = false
+ if (it.success) {
+ getDonationMessageList(roomId)
+ } else {
+ if (it.message != null) {
+ _toastLiveData.postValue(it.message)
+ } else {
+ _toastLiveData.postValue(
+ "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
+ )
+ }
+ }
+ },
+ {
+ _isLoading.value = false
+ it.message?.let { message -> Logger.e(message) }
+ _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
+ }
+ )
+ )
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingAdapter.kt
new file mode 100644
index 0000000..26fe0ff
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingAdapter.kt
@@ -0,0 +1,146 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import coil.load
+import coil.transform.CircleCropTransformation
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomDonationRankingBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.extensions.moneyFormat
+
+class LiveRoomDonationRankingAdapter :
+ RecyclerView.Adapter() {
+ val items = mutableListOf()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
+ parent.context,
+ ItemLiveRoomDonationRankingBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(items[position], position)
+ }
+
+ override fun getItemCount() = items.count()
+
+ inner class ViewHolder(
+ private val context: Context,
+ private val binding: ItemLiveRoomDonationRankingBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ @SuppressLint("SetTextI18n")
+ fun bind(item: GetLiveRoomDonationItem, position: Int) {
+ binding.tvRank.text = "${position + 1}"
+ binding.tvNickname.text = item.nickname
+
+ binding.ivProfile.load(item.profileImage) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(CircleCropTransformation())
+ }
+
+ binding.tvDonationCan.text = item.can.moneyFormat()
+
+ val lp = binding.rlDonationRanking.layoutParams as FrameLayout.LayoutParams
+
+ when (position) {
+ 0 -> {
+ binding.ivBg.setImageResource(R.drawable.bg_circle_ffdc00_ffb600)
+ binding.ivBg.visibility = View.VISIBLE
+
+ binding.ivCrown.setImageResource(R.drawable.ic_crown_1)
+ binding.ivCrown.visibility = View.VISIBLE
+ binding.root.setBackgroundResource(
+ if (items.size == 1) {
+ R.drawable.bg_round_corner_4_7_2b2635
+ } else {
+ R.drawable.bg_top_round_corner_4_7_2b2635
+ }
+ )
+
+ lp.setMargins(
+ 0,
+ 20.dpToPx().toInt(),
+ 0,
+ if (items.size == 1) {
+ 20.dpToPx().toInt()
+ } else {
+ 13.3f.dpToPx().toInt()
+ }
+ )
+ binding.rlDonationRanking.layoutParams = lp
+ }
+
+ 1 -> {
+ binding.ivBg.setImageResource(R.drawable.bg_circle_ffffff_9f9f9f)
+ binding.ivBg.visibility = View.VISIBLE
+
+ binding.ivCrown.setImageResource(R.drawable.ic_crown_2)
+ binding.ivCrown.visibility = View.VISIBLE
+
+ if (items.size == 2) {
+ binding.root.setBackgroundResource(
+ R.drawable.bg_bottom_round_corner_4_7_2b2635
+ )
+ } else {
+ binding.root.setBackgroundColor(
+ ContextCompat.getColor(context, R.color.color_2b2635)
+ )
+ }
+
+ lp.setMargins(
+ 0,
+ 0,
+ 0,
+ if (items.size == 2) {
+ 20.dpToPx().toInt()
+ } else {
+ 13.3f.dpToPx().toInt()
+ }
+ )
+ binding.rlDonationRanking.layoutParams = lp
+ }
+
+ 2 -> {
+ binding.ivBg.setImageResource(R.drawable.bg_circle_e6a77a_c67e4a)
+ binding.ivBg.visibility = View.VISIBLE
+
+ binding.ivCrown.setImageResource(R.drawable.ic_crown_3)
+ binding.ivCrown.visibility = View.VISIBLE
+ binding.root.setBackgroundResource(
+ R.drawable.bg_bottom_round_corner_4_7_2b2635
+ )
+
+ lp.setMargins(
+ 0,
+ 0,
+ 0,
+ 20.dpToPx().toInt()
+ )
+ binding.rlDonationRanking.layoutParams = lp
+ }
+
+ else -> {
+ binding.ivBg.setImageResource(0)
+ binding.ivBg.visibility = View.GONE
+ binding.ivCrown.visibility = View.GONE
+ binding.root.setBackgroundResource(0)
+ binding.root.background = null
+
+ lp.setMargins(0, 0, 0, 0)
+ binding.rlDonationRanking.layoutParams = lp
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingDialog.kt
new file mode 100644
index 0000000..953581e
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRankingDialog.kt
@@ -0,0 +1,87 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import android.annotation.SuppressLint
+import android.graphics.Rect
+import android.view.LayoutInflater
+import android.view.View
+import androidx.fragment.app.FragmentActivity
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import kr.co.vividnext.sodalive.databinding.DialogLiveRoomDonationRankingBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.extensions.moneyFormat
+import kr.co.vividnext.sodalive.live.room.LiveRoomViewModel
+
+@SuppressLint("NotifyDataSetChanged")
+class LiveRoomDonationRankingDialog(
+ private val activity: FragmentActivity,
+ layoutInflater: LayoutInflater,
+ viewModel: LiveRoomViewModel,
+ roomId: Long
+) {
+ private val bottomSheetDialog: BottomSheetDialog = BottomSheetDialog(activity)
+ private val dialogView = DialogLiveRoomDonationRankingBinding.inflate(layoutInflater)
+ private val adapter = LiveRoomDonationRankingAdapter()
+
+ init {
+ bottomSheetDialog.setContentView(dialogView.root)
+ bottomSheetDialog.setCancelable(false)
+
+ dialogView.ivClose.setOnClickListener { bottomSheetDialog.dismiss() }
+ setupRecyclerView()
+
+ viewModel.donationStatus(roomId) {
+ adapter.items.clear()
+ adapter.items.addAll(it.donationList)
+ adapter.notifyDataSetChanged()
+ dialogView.tvTotalCoin.text = it.totalCan.moneyFormat()
+ dialogView.tvTotalCount.text = it.totalCount.moneyFormat()
+ }
+ }
+
+ private fun setupRecyclerView() {
+ dialogView.rvDonationRanking.layoutManager = LinearLayoutManager(
+ activity,
+ LinearLayoutManager.VERTICAL,
+ false
+ )
+
+ dialogView.rvDonationRanking.addItemDecoration(object : RecyclerView.ItemDecoration() {
+ override fun getItemOffsets(
+ outRect: Rect,
+ view: View,
+ parent: RecyclerView,
+ state: RecyclerView.State
+ ) {
+ super.getItemOffsets(outRect, view, parent, state)
+
+ outRect.left = 13.3f.dpToPx().toInt()
+ outRect.right = 13.3f.dpToPx().toInt()
+
+ when (parent.getChildAdapterPosition(view)) {
+ 0, 1, 2 -> {
+ outRect.top = 0
+ outRect.bottom = 0
+ }
+
+ 3 -> {
+ outRect.top = 20.dpToPx().toInt()
+ outRect.bottom = 6.7f.dpToPx().toInt()
+ }
+
+ else -> {
+ outRect.top = 6.7f.dpToPx().toInt()
+ outRect.bottom = 6.7f.dpToPx().toInt()
+ }
+ }
+ }
+ })
+
+ dialogView.rvDonationRanking.adapter = adapter
+ }
+
+ fun show() {
+ bottomSheetDialog.show()
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRequest.kt
new file mode 100644
index 0000000..3f58f20
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRequest.kt
@@ -0,0 +1,10 @@
+package kr.co.vividnext.sodalive.live.room.donation
+
+import com.google.gson.annotations.SerializedName
+
+data class LiveRoomDonationRequest(
+ @SerializedName("roomId") val roomId: Long,
+ @SerializedName("can") val can: Int,
+ @SerializedName("message") val message: String,
+ @SerializedName("container") val container: String
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt
new file mode 100644
index 0000000..ea5c2cf
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt
@@ -0,0 +1,26 @@
+package kr.co.vividnext.sodalive.live.room.info
+
+import com.google.gson.annotations.SerializedName
+
+data class GetRoomInfoResponse(
+ @SerializedName("roomId") val roomId: Long,
+ @SerializedName("title") val title: String,
+ @SerializedName("notice") val notice: String,
+ @SerializedName("coverImageUrl") val coverImageUrl: String,
+ @SerializedName("channelName") val channelName: String,
+ @SerializedName("rtcToken") val rtcToken: String,
+ @SerializedName("rtmToken") val rtmToken: String,
+ @SerializedName("managerId") val managerId: Long,
+ @SerializedName("managerNickname") val managerNickname: String,
+ @SerializedName("managerProfileUrl") val managerProfileUrl: String,
+ @SerializedName("isFollowingManager") val isFollowingManager: Boolean,
+ @SerializedName("participantsCount") val participantsCount: Int,
+ @SerializedName("totalAvailableParticipantsCount") val totalAvailableParticipantsCount: Int,
+ @SerializedName("speakerList") val speakerList: List,
+ @SerializedName("listenerList") val listenerList: List,
+ @SerializedName("managerList") val managerList: List,
+ @SerializedName("donationRankingTop3UserIds") val donationRankingTop3UserIds: List,
+ @SerializedName("isAvailableDonation") val isAvailableDonation: Boolean = false,
+ @SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
+ @SerializedName("password") val password: String? = null
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt
new file mode 100644
index 0000000..e04614c
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt
@@ -0,0 +1,19 @@
+package kr.co.vividnext.sodalive.live.room.info
+
+import com.google.gson.annotations.SerializedName
+
+data class LiveRoomMember(
+ @SerializedName("id") val id: Long,
+ @SerializedName("nickname") val nickname: String,
+ @SerializedName("profileImage") val profileImage: String,
+ @SerializedName("role") val role: LiveRoomMemberRole
+)
+
+enum class LiveRoomMemberRole {
+ @SerializedName("LISTENER")
+ LISTENER,
+ @SerializedName("SPEAKER")
+ SPEAKER,
+ @SerializedName("MANAGER")
+ MANAGER
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/kick_out/LiveRoomKickOutRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/kick_out/LiveRoomKickOutRequest.kt
new file mode 100644
index 0000000..f230d46
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/kick_out/LiveRoomKickOutRequest.kt
@@ -0,0 +1,8 @@
+package kr.co.vividnext.sodalive.live.room.kick_out
+
+import com.google.gson.annotations.SerializedName
+
+data class LiveRoomKickOutRequest(
+ @SerializedName("roomId") val roomId: Long,
+ @SerializedName("userId") val userId: Long
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt
new file mode 100644
index 0000000..3aee820
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/GetLiveRoomUserProfileResponse.kt
@@ -0,0 +1,20 @@
+package kr.co.vividnext.sodalive.live.room.profile
+
+import com.google.gson.annotations.SerializedName
+
+data class GetLiveRoomUserProfileResponse(
+ @SerializedName("userId") val userId: Long,
+ @SerializedName("nickname") val nickname: String,
+ @SerializedName("profileUrl") val profileUrl: String,
+ @SerializedName("gender") val gender: String,
+ @SerializedName("instagramUrl") val instagramUrl: String,
+ @SerializedName("youtubeUrl") val youtubeUrl: String,
+ @SerializedName("websiteUrl") val websiteUrl: String,
+ @SerializedName("blogUrl") val blogUrl: String,
+ @SerializedName("introduce") val introduce: String,
+ @SerializedName("tags") val tags: String,
+ @SerializedName("isSpeaker") val isSpeaker: Boolean?,
+ @SerializedName("isManager") val isManager: Boolean?,
+ @SerializedName("isFollowing") val isFollowing: Boolean?,
+ @SerializedName("isBlock") val isBlock: Boolean
+)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomMemberResponseDiffUtilCallback.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomMemberResponseDiffUtilCallback.kt
new file mode 100644
index 0000000..35169c4
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomMemberResponseDiffUtilCallback.kt
@@ -0,0 +1,27 @@
+package kr.co.vividnext.sodalive.live.room.profile
+
+import androidx.recyclerview.widget.DiffUtil
+import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember
+
+class LiveRoomMemberResponseDiffUtilCallback(
+ private val oldData: List,
+ private val newData: List
+) : DiffUtil.Callback() {
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ val oldItem = oldData[oldItemPosition]
+ val newItem = newData[newItemPosition]
+
+ return if (oldItem is LiveRoomMember && newItem is LiveRoomMember) {
+ oldItem.id == newItem.id
+ } else {
+ false
+ }
+ }
+
+ override fun getOldListSize(): Int = oldData.size
+
+ override fun getNewListSize(): Int = newData.size
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
+ oldData[oldItemPosition] == newData[newItemPosition]
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileAdapter.kt
new file mode 100644
index 0000000..491eee6
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileAdapter.kt
@@ -0,0 +1,248 @@
+package kr.co.vividnext.sodalive.live.room.profile
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomListProfileBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileHeaderBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileManagerBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileMasterBinding
+import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember
+
+class LiveRoomProfileAdapter(
+ private val isStaff: () -> Boolean,
+ private val onClickInviteSpeaker: (Long) -> Unit,
+ private val onClickChangeListener: (Long) -> Unit,
+ private val kickOut: (Long) -> Unit,
+ private val onClickProfile: (Long) -> Unit
+) : RecyclerView.Adapter() {
+
+ private val items = mutableListOf()
+ var managerId: Long = -1
+ var totalUserCount: Int = 0
+
+ @SuppressLint("NotifyDataSetChanged")
+ fun updateList(
+ speakers: List,
+ listeners: List,
+ managers: List
+ ) {
+ val items = mutableListOf()
+ speakers.forEach {
+ if (it.id == managerId) {
+ val item = LiveRoomProfileItemMaster(
+ nickname = it.nickname,
+ profileUrl = it.profileImage
+ )
+ item.id = it.id
+
+ items.add(0, item)
+ } else {
+ val item = LiveRoomProfileItemUser(
+ nickname = it.nickname,
+ profileUrl = it.profileImage,
+ role = it.role
+ )
+
+ item.id = it.id
+ items.add(item)
+ }
+ }
+
+ items.add(
+ 1,
+ LiveRoomProfileItemSpeakerTitle(
+ "스피커",
+ speakerCount = speakers.size - 1,
+ totalUserCount = totalUserCount
+ )
+ )
+
+ items.add(
+ 1,
+ LiveRoomProfileItemManagerTitle(
+ "스탭",
+ managerCount = managers.size
+ )
+ )
+
+ managers.forEachIndexed { index, manager ->
+ val item = LiveRoomProfileItemManager(
+ nickname = manager.nickname,
+ profileUrl = manager.profileImage
+ )
+
+ item.id = manager.id
+ items.add(index = index + 2, item)
+ }
+
+ items.add(LiveRoomProfileItemListenerTitle("리스너"))
+
+ listeners.forEach {
+ val item = LiveRoomProfileItemUser(
+ nickname = it.nickname,
+ profileUrl = it.profileImage,
+ role = it.role
+ )
+
+ item.id = it.id
+ items.add(item)
+ }
+
+ this.items.run {
+ clear()
+ addAll(items)
+ notifyDataSetChanged()
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveRoomProfileViewHolder {
+ when (viewType) {
+ LiveRoomProfileItemType.SPEAKER_TITLE.ordinal -> {
+ return LiveRoomProfileSpeakerTitleViewHolder(
+ ItemLiveRoomProfileHeaderBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ }
+
+ LiveRoomProfileItemType.MANAGER_TITLE.ordinal -> {
+ return LiveRoomProfileManagerTitleViewHolder(
+ ItemLiveRoomProfileHeaderBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ }
+
+ LiveRoomProfileItemType.LISTENER_TITLE.ordinal -> {
+ return LiveRoomProfileListenerTitleViewHolder(
+ ItemLiveRoomProfileHeaderBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ }
+
+ LiveRoomProfileItemType.MASTER.ordinal -> {
+ return LiveRoomProfileMasterViewHolder(
+ ItemLiveRoomProfileMasterBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ ),
+ managerId,
+ onClickProfile = onClickProfile
+ )
+ }
+
+ LiveRoomProfileItemType.MANAGER.ordinal -> {
+ return LiveRoomProfileManagerViewHolder(
+ ItemLiveRoomProfileManagerBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ ),
+ onClickProfile = onClickProfile
+ )
+ }
+
+ else -> {
+ return LiveRoomProfileUserViewHolder(
+ ItemLiveRoomListProfileBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ ),
+ isStaff = isStaff,
+ managerId = managerId,
+ onClickInviteSpeaker = onClickInviteSpeaker,
+ onClickChangeListener = onClickChangeListener,
+ kickOut = kickOut,
+ onClickProfile = onClickProfile
+ )
+ }
+ }
+ }
+
+ override fun onBindViewHolder(holder: LiveRoomProfileViewHolder, position: Int) {
+ holder.bind(items[position])
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return items[position].type.ordinal
+ }
+
+ override fun getItemCount(): Int = items.size
+}
+
+abstract class LiveRoomProfileViewHolder(binding: ViewBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ abstract fun bind(item: LiveRoomProfileItem)
+}
+
+class LiveRoomProfileSpeakerTitleViewHolder(
+ private val binding: ItemLiveRoomProfileHeaderBinding,
+) : LiveRoomProfileViewHolder(binding) {
+ override fun bind(item: LiveRoomProfileItem) = item.bind(binding)
+}
+
+class LiveRoomProfileListenerTitleViewHolder(
+ private val binding: ItemLiveRoomProfileHeaderBinding,
+) : LiveRoomProfileViewHolder(binding) {
+ override fun bind(item: LiveRoomProfileItem) = item.bind(binding)
+}
+
+class LiveRoomProfileManagerTitleViewHolder(
+ private val binding: ItemLiveRoomProfileHeaderBinding,
+) : LiveRoomProfileViewHolder(binding) {
+ override fun bind(item: LiveRoomProfileItem) = item.bind(binding)
+}
+
+class LiveRoomProfileMasterViewHolder(
+ private val binding: ItemLiveRoomProfileMasterBinding,
+ private val managerId: Long,
+ private val onClickProfile: (Long) -> Unit
+) : LiveRoomProfileViewHolder(binding) {
+ override fun bind(item: LiveRoomProfileItem) {
+ item.onClickProfile = onClickProfile
+ item.managerId = managerId
+ item.bind(binding)
+ }
+}
+
+class LiveRoomProfileManagerViewHolder(
+ private val binding: ItemLiveRoomProfileManagerBinding,
+ private val onClickProfile: (Long) -> Unit
+) : LiveRoomProfileViewHolder(binding) {
+ override fun bind(item: LiveRoomProfileItem) {
+ item.onClickProfile = onClickProfile
+ item.bind(binding)
+ }
+}
+
+class LiveRoomProfileUserViewHolder(
+ private val binding: ItemLiveRoomListProfileBinding,
+ private val isStaff: () -> Boolean,
+ private val managerId: Long,
+ private val onClickInviteSpeaker: (Long) -> Unit,
+ private val onClickChangeListener: (Long) -> Unit,
+ private val kickOut: (Long) -> Unit,
+ private val onClickProfile: (Long) -> Unit
+) : LiveRoomProfileViewHolder(binding) {
+ override fun bind(item: LiveRoomProfileItem) {
+ item.isStaff = isStaff
+ item.managerId = managerId
+ item.onClickInviteSpeaker = onClickInviteSpeaker
+ item.onClickChangeListener = onClickChangeListener
+ item.kickOut = kickOut
+ item.onClickProfile = onClickProfile
+ item.bind(binding)
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileDialog.kt
new file mode 100644
index 0000000..478ccc5
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileDialog.kt
@@ -0,0 +1,71 @@
+package kr.co.vividnext.sodalive.live.room.profile
+
+import android.annotation.SuppressLint
+import android.view.LayoutInflater
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.LiveData
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import kr.co.vividnext.sodalive.databinding.DialogLiveRoomProfileBinding
+import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse
+
+class LiveRoomProfileDialog(
+ layoutInflater: LayoutInflater,
+ private val activity: FragmentActivity,
+ private val roomInfoLiveData: LiveData,
+ isStaff: () -> Boolean,
+ onClickInviteSpeaker: (Long) -> Unit,
+ onClickChangeListener: (Long) -> Unit,
+ onClickKickOut: (Long) -> Unit,
+ onClickProfile: (Long) -> Unit
+) {
+ private val bottomSheetDialog: BottomSheetDialog = BottomSheetDialog(activity)
+ private val dialogView = DialogLiveRoomProfileBinding.inflate(layoutInflater)
+ private val adapter = LiveRoomProfileAdapter(
+ isStaff = isStaff,
+ onClickInviteSpeaker = onClickInviteSpeaker,
+ onClickChangeListener = onClickChangeListener,
+ kickOut = onClickKickOut,
+ onClickProfile = onClickProfile
+ )
+
+ init {
+ bottomSheetDialog.setContentView(dialogView.root)
+ bottomSheetDialog.setCancelable(false)
+
+ dialogView.ivClose.setOnClickListener { bottomSheetDialog.dismiss() }
+ setupRecyclerView()
+ bindData()
+ }
+
+ private fun setupRecyclerView() {
+ dialogView.rvPeoples.isNestedScrollingEnabled = true
+ dialogView.rvPeoples.layoutManager = LinearLayoutManager(
+ activity,
+ LinearLayoutManager.VERTICAL,
+ false
+ )
+
+ dialogView.rvPeoples.adapter = adapter
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun bindData() {
+ roomInfoLiveData.observe(activity) {
+ adapter.managerId = it.managerId
+ adapter.totalUserCount = it.totalAvailableParticipantsCount
+ dialogView.tvParticipate.text = "${it.participantsCount}"
+ dialogView.tvTotalPeoples.text = "/${it.totalAvailableParticipantsCount}"
+
+ adapter.updateList(
+ speakers = it.speakerList,
+ listeners = it.listenerList,
+ managers = it.managerList
+ )
+ }
+ }
+
+ fun show() {
+ bottomSheetDialog.show()
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileItem.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileItem.kt
new file mode 100644
index 0000000..efff22e
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileItem.kt
@@ -0,0 +1,201 @@
+package kr.co.vividnext.sodalive.live.room.profile
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+import coil.load
+import coil.transform.CircleCropTransformation
+import com.google.gson.annotations.SerializedName
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.common.SharedPreferenceManager
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomListProfileBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileHeaderBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileManagerBinding
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileMasterBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.live.room.info.LiveRoomMemberRole
+
+enum class LiveRoomProfileItemType {
+ @SerializedName("MASTER")
+ MASTER,
+
+ @SerializedName("MANAGER")
+ MANAGER,
+
+ @SerializedName("SPEAKER_TITLE")
+ SPEAKER_TITLE,
+
+ @SerializedName("LISTENER_TITLE")
+ LISTENER_TITLE,
+
+ @SerializedName("MANAGER_TITLE")
+ MANAGER_TITLE,
+
+ @SerializedName("USER")
+ USER
+}
+
+abstract class LiveRoomProfileItem {
+ open var id: Long = 0L
+ open var type: LiveRoomProfileItemType = LiveRoomProfileItemType.USER
+ open var isStaff: () -> Boolean = { false }
+ open var managerId = 0L
+ open var onClickInviteSpeaker: (Long) -> Unit = {}
+ open var onClickChangeListener: (Long) -> Unit = {}
+ open var kickOut: (Long) -> Unit = {}
+ open var onClickProfile: (Long) -> Unit = {}
+ abstract fun bind(binding: ViewBinding)
+}
+
+data class LiveRoomProfileItemSpeakerTitle(
+ val title: String,
+ val speakerCount: Int,
+ val totalUserCount: Int
+) : LiveRoomProfileItem() {
+ override var type = LiveRoomProfileItemType.SPEAKER_TITLE
+ override fun bind(binding: ViewBinding) {
+ val itemBinding = binding as ItemLiveRoomProfileHeaderBinding
+ itemBinding.tvTitle.text = title
+ itemBinding.tvSpeakerCount.text = "$speakerCount"
+ itemBinding.tvSpeakerTotalCount.text = if (totalUserCount > 9) {
+ "/9"
+ } else {
+ "/${totalUserCount - 1}"
+ }
+ itemBinding.tvSpeakerCount.visibility = View.VISIBLE
+ itemBinding.tvSpeakerTotalCount.visibility = View.VISIBLE
+
+ val lp = itemBinding.llRoot.layoutParams as RecyclerView.LayoutParams
+ lp.topMargin = 14f.dpToPx().toInt()
+ lp.bottomMargin = 14f.dpToPx().toInt()
+ itemBinding.llRoot.layoutParams = lp
+ }
+}
+
+data class LiveRoomProfileItemListenerTitle(
+ val title: String
+) : LiveRoomProfileItem() {
+ override var type = LiveRoomProfileItemType.LISTENER_TITLE
+ override fun bind(binding: ViewBinding) {
+ val itemBinding = binding as ItemLiveRoomProfileHeaderBinding
+ itemBinding.tvTitle.text = title
+ itemBinding.tvSpeakerCount.visibility = View.GONE
+ itemBinding.tvSpeakerTotalCount.visibility = View.GONE
+
+ val lp = itemBinding.llRoot.layoutParams as RecyclerView.LayoutParams
+ lp.topMargin = 20f.dpToPx().toInt()
+ lp.bottomMargin = 14f.dpToPx().toInt()
+ itemBinding.llRoot.layoutParams = lp
+ }
+}
+
+data class LiveRoomProfileItemManagerTitle(
+ val title: String,
+ val managerCount: Int
+) : LiveRoomProfileItem() {
+ override var type = LiveRoomProfileItemType.MANAGER_TITLE
+ override fun bind(binding: ViewBinding) {
+ val itemBinding = binding as ItemLiveRoomProfileHeaderBinding
+ itemBinding.tvTitle.text = title
+ itemBinding.tvSpeakerCount.text = "$managerCount"
+ itemBinding.tvSpeakerCount.visibility = View.VISIBLE
+ itemBinding.tvSpeakerTotalCount.visibility = View.GONE
+
+ val lp = itemBinding.llRoot.layoutParams as RecyclerView.LayoutParams
+ lp.topMargin = 14f.dpToPx().toInt()
+ lp.bottomMargin = 14f.dpToPx().toInt()
+ itemBinding.llRoot.layoutParams = lp
+ }
+}
+
+data class LiveRoomProfileItemMaster(
+ val nickname: String,
+ val profileUrl: String
+) : LiveRoomProfileItem() {
+ override var type = LiveRoomProfileItemType.MASTER
+ override fun bind(binding: ViewBinding) {
+ val itemBinding = binding as ItemLiveRoomProfileMasterBinding
+ itemBinding.tvNickname.text = nickname
+ itemBinding.ivProfile.load(profileUrl) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(CircleCropTransformation())
+ }
+ if (id != SharedPreferenceManager.userId) {
+ itemBinding.ivProfile.setOnClickListener { onClickProfile(id) }
+ }
+ }
+}
+
+data class LiveRoomProfileItemManager(
+ val nickname: String,
+ val profileUrl: String
+) : LiveRoomProfileItem() {
+ override var type = LiveRoomProfileItemType.MANAGER
+ override fun bind(binding: ViewBinding) {
+ val itemBinding = binding as ItemLiveRoomProfileManagerBinding
+ itemBinding.tvNickname.text = nickname
+ itemBinding.ivProfile.load(profileUrl) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(CircleCropTransformation())
+ }
+ if (id != SharedPreferenceManager.userId) {
+ itemBinding.ivProfile.setOnClickListener {
+ onClickProfile(id)
+ }
+ }
+ }
+}
+
+data class LiveRoomProfileItemUser(
+ val nickname: String,
+ val profileUrl: String,
+ val role: LiveRoomMemberRole
+) : LiveRoomProfileItem() {
+ override var type = LiveRoomProfileItemType.USER
+ override fun bind(binding: ViewBinding) {
+ val itemBinding = binding as ItemLiveRoomListProfileBinding
+ itemBinding.tvNickname.text = nickname
+ itemBinding.ivProfile.load(profileUrl) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(CircleCropTransformation())
+ }
+
+ if (id != SharedPreferenceManager.userId) {
+ itemBinding.ivProfile.setOnClickListener { onClickProfile(id) }
+ }
+
+ if (id == SharedPreferenceManager.userId && role == LiveRoomMemberRole.SPEAKER) {
+ itemBinding.llControlButtons.visibility = View.VISIBLE
+ itemBinding.tvChangeAudience.visibility = View.VISIBLE
+ itemBinding.tvInviteSpeaker.visibility = View.GONE
+ itemBinding.ivKickOut.visibility = View.GONE
+
+ itemBinding.tvChangeAudience.setOnClickListener { onClickChangeListener(id) }
+ itemBinding.tvInviteSpeaker.setOnClickListener { }
+ } else if (managerId == SharedPreferenceManager.userId || isStaff()) {
+ itemBinding.llControlButtons.visibility = View.VISIBLE
+ itemBinding.ivKickOut.visibility = View.VISIBLE
+
+ if (role == LiveRoomMemberRole.SPEAKER) {
+ itemBinding.tvChangeAudience.visibility = View.VISIBLE
+ itemBinding.tvInviteSpeaker.visibility = View.GONE
+
+ itemBinding.tvChangeAudience.setOnClickListener { onClickChangeListener(id) }
+ itemBinding.tvInviteSpeaker.setOnClickListener { }
+ } else {
+ itemBinding.tvChangeAudience.visibility = View.GONE
+ itemBinding.tvInviteSpeaker.visibility = View.VISIBLE
+
+ itemBinding.tvChangeAudience.setOnClickListener { }
+ itemBinding.tvInviteSpeaker.setOnClickListener { onClickInviteSpeaker(id) }
+ }
+
+ itemBinding.ivKickOut.setOnClickListener { kickOut(id) }
+ } else {
+ itemBinding.llControlButtons.visibility = View.GONE
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileListAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileListAdapter.kt
new file mode 100644
index 0000000..078d558
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomProfileListAdapter.kt
@@ -0,0 +1,86 @@
+package kr.co.vividnext.sodalive.live.room.profile
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import coil.load
+import coil.transform.RoundedCornersTransformation
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.databinding.ItemLiveRoomProfileBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember
+
+class LiveRoomProfileListAdapter : RecyclerView.Adapter() {
+ inner class ViewHolder(
+ private val binding: ItemLiveRoomProfileBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(item: LiveRoomMember) {
+ binding.ivProfile.load(item.profileImage) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(RoundedCornersTransformation(23.3f.dpToPx()))
+
+ if (activeSpeakers.contains(item.id.toInt())) {
+ binding.ivBg.visibility = View.VISIBLE
+ } else {
+ binding.ivBg.visibility = View.GONE
+ }
+
+ if (muteSpeakers.contains(item.id.toInt())) {
+ binding.ivMute.visibility = View.VISIBLE
+ } else {
+ binding.ivMute.visibility = View.GONE
+ }
+
+ val ivMuteLp = binding.ivMute.layoutParams
+ ivMuteLp.width = 51.7f.dpToPx().toInt()
+ ivMuteLp.height = 51.7f.dpToPx().toInt()
+ binding.ivMute.layoutParams = ivMuteLp
+
+ if (managerId == item.id) {
+ binding.ivCrown.visibility = View.VISIBLE
+ } else {
+ binding.ivCrown.visibility = View.GONE
+ }
+ }
+
+ binding.tvNickname.text = item.nickname
+ }
+ }
+
+ private val items = mutableListOf()
+ val activeSpeakers = mutableSetOf()
+ val muteSpeakers = mutableSetOf()
+ var managerId: Long = -1
+
+ fun updateList(items: List) {
+ items.let {
+ val diffCallback = LiveRoomMemberResponseDiffUtilCallback(this.items, items)
+ val diffResult = DiffUtil.calculateDiff(diffCallback)
+
+ this.items.run {
+ clear()
+ addAll(items)
+ diffResult.dispatchUpdatesTo(this@LiveRoomProfileListAdapter)
+ }
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ ItemLiveRoomProfileBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: LiveRoomProfileListAdapter.ViewHolder, position: Int) {
+ holder.bind(items[position])
+ }
+
+ override fun getItemCount() = items.count()
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomUserProfileDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomUserProfileDialog.kt
new file mode 100644
index 0000000..234898a
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/profile/LiveRoomUserProfileDialog.kt
@@ -0,0 +1,192 @@
+package kr.co.vividnext.sodalive.live.room.profile
+
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.Window
+import android.view.WindowManager
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.LiveData
+import coil.load
+import coil.transform.RoundedCornersTransformation
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.databinding.DialogLiveRoomUserProfileBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+
+class LiveRoomUserProfileDialog(
+ private val activity: FragmentActivity,
+ private val userProfileLiveData: LiveData,
+ layoutInflater: LayoutInflater,
+ private val isStaff: (Long) -> Boolean,
+ private val onClickSendMessage: (Long, String) -> Unit,
+ private val onClickSetManager: (Long) -> Unit,
+ private val onClickReleaseManager: (Long) -> Unit,
+ private val onClickFollow: (Long) -> Unit,
+ private val onClickUnFollow: (Long) -> Unit,
+ private val onClickInviteSpeaker: (Long) -> Unit,
+ private val onClickChangeListener: (Long) -> Unit,
+ private val onClickKickOut: (Long) -> Unit,
+ private val onClickPopupMenu: (Long, String, Boolean, View) -> Unit,
+) {
+ private val alertDialog: AlertDialog
+ private val dialogView = DialogLiveRoomUserProfileBinding.inflate(layoutInflater)
+
+ private var isIntroduceFold = true
+
+ init {
+ val dialogBuilder = AlertDialog.Builder(activity)
+ dialogBuilder.setView(dialogView.root)
+
+ alertDialog = dialogBuilder.create()
+ alertDialog.setCancelable(false)
+ alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ setupView()
+ bindUserProfile()
+ }
+
+ fun dismiss() {
+ alertDialog.dismiss()
+ }
+
+ fun show() {
+ alertDialog.show()
+
+ val lp = WindowManager.LayoutParams()
+ lp.copyFrom(alertDialog.window?.attributes)
+ lp.width = activity.resources.displayMetrics.widthPixels - (40.dpToPx()).toInt()
+ lp.height = activity.resources.displayMetrics.heightPixels - (37.3f.dpToPx()).toInt()
+
+ alertDialog.window?.attributes = lp
+ }
+
+ fun isShowing(): Boolean {
+ return alertDialog.isShowing
+ }
+
+ private fun setupView() {
+ val profileLP = dialogView.ivProfile.layoutParams
+ profileLP.width = activity.resources.displayMetrics.widthPixels - (66.7f.dpToPx()).toInt()
+ profileLP.height = activity.resources.displayMetrics.widthPixels - (66.7f.dpToPx()).toInt()
+ dialogView.ivProfile.layoutParams = profileLP
+ }
+
+ private fun bindUserProfile() {
+ userProfileLiveData.observe(activity) { userProfile ->
+ dialogView.ivProfile.load(userProfile.profileUrl) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(RoundedCornersTransformation(8.dpToPx()))
+ }
+
+ dialogView.tvNickname.text = userProfile.nickname
+ dialogView.tvTags.text = userProfile.tags
+ dialogView.tvGender.text = userProfile.gender
+ dialogView.tvIntroduce.text = userProfile.introduce
+
+ dialogView.ivClose.setOnClickListener { alertDialog.dismiss() }
+ dialogView.llSendMessage.setOnClickListener {
+ onClickSendMessage(
+ userProfile.userId,
+ userProfile.nickname
+ )
+ }
+
+ dialogView.tvIntroduce.setOnClickListener {
+ isIntroduceFold = !isIntroduceFold
+
+ if (isIntroduceFold) {
+ dialogView.tvIntroduce.maxLines = 2
+ } else {
+ dialogView.tvIntroduce.maxLines = Int.MAX_VALUE
+ }
+ }
+
+ if (userProfile.isSpeaker != null) {
+ dialogView.tvInviteSpeaker.visibility = View.VISIBLE
+
+ if (userProfile.isSpeaker) {
+ dialogView.tvInviteSpeaker.text = "리스너 변경"
+ dialogView.tvInviteSpeaker.setOnClickListener {
+ onClickChangeListener(userProfile.userId)
+ alertDialog.dismiss()
+ }
+ } else {
+ dialogView.tvInviteSpeaker.text = "스피커 초대"
+ dialogView.tvInviteSpeaker.setOnClickListener {
+ onClickInviteSpeaker(userProfile.userId)
+ alertDialog.dismiss()
+ }
+ }
+ } else {
+ dialogView.tvInviteSpeaker.visibility = View.GONE
+ dialogView.tvKickOut.visibility = View.GONE
+ }
+
+ if (userProfile.isManager != null) {
+ dialogView.tvSetManager.visibility = View.VISIBLE
+
+ if (userProfile.isManager) {
+ dialogView.tvSetManager.text = "스탭 해제"
+ dialogView.tvSetManager.setOnClickListener {
+ onClickReleaseManager(userProfile.userId)
+ alertDialog.dismiss()
+ }
+ } else {
+ dialogView.tvSetManager.text = "스탭 지정"
+ dialogView.tvSetManager.setOnClickListener {
+ onClickSetManager(userProfile.userId)
+ alertDialog.dismiss()
+ }
+ }
+ } else {
+ dialogView.tvSetManager.visibility = View.GONE
+ }
+
+ if (
+ (userProfile.isSpeaker != null && !isStaff(userProfile.userId)) ||
+ (userProfile.isSpeaker != null && userProfile.isManager != null)
+ ) {
+ dialogView.tvKickOut.visibility = View.VISIBLE
+ dialogView.tvKickOut.setOnClickListener {
+ onClickKickOut(userProfile.userId)
+ alertDialog.dismiss()
+ }
+ }
+
+ if (userProfile.isFollowing != null) {
+ dialogView.llFollow.visibility = View.VISIBLE
+
+ if (userProfile.isFollowing) {
+ dialogView.tvFollow.text = "팔로잉"
+ dialogView.ivFollow.setImageResource(R.drawable.ic_alarm_selected)
+ dialogView.llFollow.setBackgroundResource(
+ R.drawable.bg_round_corner_23_3_3e1b93_9970ff
+ )
+ dialogView.tvFollow.setOnClickListener { onClickUnFollow(userProfile.userId) }
+ } else {
+ dialogView.tvFollow.text = "팔로우"
+ dialogView.ivFollow.setImageResource(R.drawable.ic_alarm)
+ dialogView.llFollow.setBackgroundResource(
+ R.drawable.bg_round_corner_23_3_transparent_9970ff
+ )
+ dialogView.tvFollow.setOnClickListener { onClickFollow(userProfile.userId) }
+ }
+ } else {
+ dialogView.llFollow.visibility = View.GONE
+ }
+
+ dialogView.ivMenu.setOnClickListener {
+ onClickPopupMenu(
+ userProfile.userId,
+ userProfile.nickname,
+ userProfile.isBlock,
+ dialogView.ivMenu
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomInfoEditDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomInfoEditDialog.kt
new file mode 100644
index 0000000..5b19e88
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/update/LiveRoomInfoEditDialog.kt
@@ -0,0 +1,88 @@
+package kr.co.vividnext.sodalive.live.room.update
+
+import android.app.Activity
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.WindowManager
+import androidx.appcompat.app.AlertDialog
+import coil.load
+import coil.transform.RoundedCornersTransformation
+import kr.co.vividnext.sodalive.R
+import kr.co.vividnext.sodalive.databinding.DialogLiveRoomInfoUpdateBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+
+class LiveRoomInfoEditDialog(
+ activity: Activity,
+ layoutInflater: LayoutInflater,
+ onClickImagePicker: () -> Unit
+) {
+ private val alertDialog: AlertDialog
+ private val dialogView = DialogLiveRoomInfoUpdateBinding.inflate(layoutInflater)
+
+ private var coverImageUrl: String? = null
+ private var coverImageUri: Uri? = null
+
+ init {
+ val dialogBuilder = AlertDialog.Builder(activity)
+ dialogBuilder.setView(dialogView.root)
+
+ alertDialog = dialogBuilder.create()
+ alertDialog.setCancelable(false)
+ alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ dialogView.ivPhotoPicker.setOnClickListener { onClickImagePicker() }
+ dialogView.ivClose.setOnClickListener { alertDialog.dismiss() }
+ dialogView.tvCancel.setOnClickListener { alertDialog.dismiss() }
+ }
+
+ fun setRoomInfo(
+ currentTitle: String,
+ currentContent: String,
+ ) {
+ dialogView.etTitle.setText(currentTitle)
+ dialogView.etContent.setText(currentContent)
+ }
+
+ fun setCoverImageUri(coverImageUri: Uri) {
+ this.coverImageUri = coverImageUri
+ dialogView.ivCover.load(coverImageUri) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(RoundedCornersTransformation(13.3f.dpToPx()))
+ }
+ }
+
+ fun setCoverImageUrl(coverImageUrl: String) {
+ this.coverImageUrl = coverImageUrl
+ dialogView.ivCover.load(coverImageUrl) {
+ crossfade(true)
+ placeholder(R.drawable.bg_placeholder)
+ transformations(RoundedCornersTransformation(13.3f.dpToPx()))
+ }
+ }
+
+ fun setConfirmAction(confirmAction: (String, String, Uri?) -> Unit) {
+ dialogView.tvConfirm.setOnClickListener {
+ alertDialog.dismiss()
+
+ val newTitle = dialogView.etTitle.text.toString()
+ val newContent = dialogView.etContent.text.toString()
+ confirmAction(newTitle, newContent, coverImageUri)
+ coverImageUri = null
+ coverImageUrl = null
+ }
+ }
+
+ fun show(width: Int) {
+ alertDialog.show()
+
+ val lp = WindowManager.LayoutParams()
+ lp.copyFrom(alertDialog.window?.attributes)
+ lp.width = width - (26.7f.dpToPx()).toInt()
+ lp.height = WindowManager.LayoutParams.WRAP_CONTENT
+
+ alertDialog.window?.attributes = lp
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/ProfileReportDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/ProfileReportDialog.kt
new file mode 100644
index 0000000..8801afe
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/report/ProfileReportDialog.kt
@@ -0,0 +1,50 @@
+package kr.co.vividnext.sodalive.report
+
+import android.app.Activity
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import android.view.WindowManager
+import androidx.appcompat.app.AlertDialog
+import kr.co.vividnext.sodalive.databinding.DialogProfileReportBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+
+class ProfileReportDialog(
+ activity: Activity,
+ layoutInflater: LayoutInflater,
+ confirmButtonClick: () -> Unit
+) {
+
+ private val alertDialog: AlertDialog
+
+ val dialogView = DialogProfileReportBinding.inflate(layoutInflater)
+
+ init {
+ val dialogBuilder = AlertDialog.Builder(activity)
+ dialogBuilder.setView(dialogView.root)
+
+ alertDialog = dialogBuilder.create()
+ alertDialog.setCancelable(false)
+ alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ dialogView.tvCancel.setOnClickListener {
+ alertDialog.dismiss()
+ }
+
+ dialogView.tvReport.setOnClickListener {
+ alertDialog.dismiss()
+ confirmButtonClick()
+ }
+ }
+
+ fun show(width: Int) {
+ alertDialog.show()
+
+ val lp = WindowManager.LayoutParams()
+ lp.copyFrom(alertDialog.window?.attributes)
+ lp.width = width - (26.7f.dpToPx()).toInt()
+ lp.height = WindowManager.LayoutParams.WRAP_CONTENT
+
+ alertDialog.window?.attributes = lp
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/ReportApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportApi.kt
new file mode 100644
index 0000000..e6cfcaf
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportApi.kt
@@ -0,0 +1,15 @@
+package kr.co.vividnext.sodalive.report
+
+import io.reactivex.rxjava3.core.Single
+import kr.co.vividnext.sodalive.common.ApiResponse
+import retrofit2.http.Body
+import retrofit2.http.Header
+import retrofit2.http.POST
+
+interface ReportApi {
+ @POST("/report")
+ fun report(
+ @Body request: ReportRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRepository.kt
new file mode 100644
index 0000000..d1c3e3c
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRepository.kt
@@ -0,0 +1,5 @@
+package kr.co.vividnext.sodalive.report
+
+class ReportRepository(private val api: ReportApi) {
+ fun report(request: ReportRequest, token: String) = api.report(request, authHeader = token)
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt
new file mode 100644
index 0000000..24d8f13
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt
@@ -0,0 +1,19 @@
+package kr.co.vividnext.sodalive.report
+
+import com.google.gson.annotations.SerializedName
+
+data class ReportRequest(
+ @SerializedName("type") val type: ReportType,
+ @SerializedName("reason") val reason: String,
+ @SerializedName("reportedAccountId") val reportedAccountId: Long? = null,
+ @SerializedName("cheersId") val cheersId: Long? = null,
+ @SerializedName("audioContentId") val audioContentId: Long? = null,
+)
+
+enum class ReportType {
+ @SerializedName("REVIEW") REVIEW,
+ @SerializedName("PROFILE") PROFILE,
+ @SerializedName("USER") USER,
+ @SerializedName("CHEERS") CHEERS,
+ @SerializedName("AUDIO_CONTENT") AUDIO_CONTENT
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/report/UserReportDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/report/UserReportDialog.kt
new file mode 100644
index 0000000..6b1a475
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/report/UserReportDialog.kt
@@ -0,0 +1,58 @@
+package kr.co.vividnext.sodalive.report
+
+import android.app.Activity
+import android.graphics.Color
+import android.graphics.drawable.ColorDrawable
+import android.view.LayoutInflater
+import android.view.WindowManager
+import android.widget.RadioButton
+import androidx.appcompat.app.AlertDialog
+import kr.co.vividnext.sodalive.databinding.DialogUserReportBinding
+import kr.co.vividnext.sodalive.extensions.dpToPx
+
+class UserReportDialog(
+ activity: Activity,
+ layoutInflater: LayoutInflater,
+ confirmButtonClick: (String) -> Unit
+) {
+
+ private val alertDialog: AlertDialog
+
+ val dialogView = DialogUserReportBinding.inflate(layoutInflater)
+
+ var reason = ""
+
+ init {
+ val dialogBuilder = AlertDialog.Builder(activity)
+ dialogBuilder.setView(dialogView.root)
+
+ alertDialog = dialogBuilder.create()
+ alertDialog.setCancelable(false)
+ alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+
+ dialogView.tvCancel.setOnClickListener {
+ alertDialog.dismiss()
+ }
+
+ dialogView.tvReport.setOnClickListener {
+ alertDialog.dismiss()
+ confirmButtonClick(reason)
+ }
+
+ dialogView.radioGroup.setOnCheckedChangeListener { radioGroup, checkedId ->
+ val radioButton = radioGroup.findViewById(checkedId)
+ reason = radioButton.text.toString()
+ }
+ }
+
+ fun show(width: Int) {
+ alertDialog.show()
+
+ val lp = WindowManager.LayoutParams()
+ lp.copyFrom(alertDialog.window?.attributes)
+ lp.width = width - (26.7f.dpToPx()).toInt()
+ lp.height = WindowManager.LayoutParams.WRAP_CONTENT
+
+ alertDialog.window?.attributes = lp
+ }
+}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/CreatorFollowRequestRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/CreatorFollowRequestRequest.kt
new file mode 100644
index 0000000..e0d9359
--- /dev/null
+++ b/app/src/main/java/kr/co/vividnext/sodalive/user/CreatorFollowRequestRequest.kt
@@ -0,0 +1,5 @@
+package kr.co.vividnext.sodalive.user
+
+import com.google.gson.annotations.SerializedName
+
+data class CreatorFollowRequestRequest(@SerializedName("creatorId") val creatorId: Long)
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt
index 931eea1..b451080 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.user
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
+import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest
import kr.co.vividnext.sodalive.mypage.MyPageResponse
import kr.co.vividnext.sodalive.settings.notification.GetMemberInfoResponse
@@ -56,4 +57,28 @@ interface UserApi {
@Query("container") container: String = "aos",
@Header("Authorization") authHeader: String
): Single>
+
+ @POST("/member/block")
+ fun memberBlock(
+ @Body request: MemberBlockRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @POST("/member/unblock")
+ fun memberUnBlock(
+ @Body request: MemberBlockRequest,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @POST("/member/creator/follow")
+ fun creatorFollow(
+ request: Any,
+ @Header("Authorization") authHeader: String
+ ): Single>
+
+ @POST("/member/creator/unfollow")
+ fun creatorUnFollow(
+ request: Any,
+ @Header("Authorization") authHeader: String
+ ): Single>
}
diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt
index 1948cc7..0c1143c 100644
--- a/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt
+++ b/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.user
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.common.ApiResponse
+import kr.co.vividnext.sodalive.explorer.profile.MemberBlockRequest
import kr.co.vividnext.sodalive.main.PushTokenUpdateRequest
import kr.co.vividnext.sodalive.mypage.MyPageResponse
import kr.co.vividnext.sodalive.settings.notification.UpdateNotificationSettingRequest
@@ -35,4 +36,14 @@ class UserRepository(private val userApi: UserApi) {
fun getMyPage(token: String): Single> {
return userApi.getMyPage(authHeader = token)
}
+
+ fun memberBlock(
+ userId: Long,
+ token: String
+ ) = userApi.memberBlock(request = MemberBlockRequest(userId), authHeader = token)
+
+ fun memberUnBlock(
+ userId: Long,
+ token: String
+ ) = userApi.memberUnBlock(request = MemberBlockRequest(userId), authHeader = token)
}
diff --git a/app/src/main/res/drawable-xxhdpi/btn_follow.png b/app/src/main/res/drawable-xxhdpi/btn_follow.png
new file mode 100644
index 0000000..df22156
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_follow.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/btn_following.png b/app/src/main/res/drawable-xxhdpi/btn_following.png
new file mode 100644
index 0000000..e9507dd
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_following.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/btn_message_send.png b/app/src/main/res/drawable-xxhdpi/btn_message_send.png
new file mode 100644
index 0000000..5fc9f6c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_message_send.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_alarm.png b/app/src/main/res/drawable-xxhdpi/ic_alarm.png
new file mode 100644
index 0000000..d1b1e59
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_alarm.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_alarm_selected.png b/app/src/main/res/drawable-xxhdpi/ic_alarm_selected.png
new file mode 100644
index 0000000..7d99d63
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_alarm_selected.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_badge_manager.png b/app/src/main/res/drawable-xxhdpi/ic_badge_manager.png
new file mode 100644
index 0000000..0ecc7b3
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_badge_manager.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_bottom_white.png b/app/src/main/res/drawable-xxhdpi/ic_bottom_white.png
new file mode 100644
index 0000000..da528d9
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_bottom_white.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_crown.png b/app/src/main/res/drawable-xxhdpi/ic_crown.png
new file mode 100644
index 0000000..003ab3c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_crown.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_crown_1.png b/app/src/main/res/drawable-xxhdpi/ic_crown_1.png
new file mode 100644
index 0000000..3ad440c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_crown_1.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_crown_2.png b/app/src/main/res/drawable-xxhdpi/ic_crown_2.png
new file mode 100644
index 0000000..ee2874d
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_crown_2.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_crown_3.png b/app/src/main/res/drawable-xxhdpi/ic_crown_3.png
new file mode 100644
index 0000000..95838a4
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_crown_3.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_donation.png b/app/src/main/res/drawable-xxhdpi/ic_donation.png
new file mode 100644
index 0000000..3732210
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_donation.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_donation_message_list.png b/app/src/main/res/drawable-xxhdpi/ic_donation_message_list.png
new file mode 100644
index 0000000..f7e7728
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_donation_message_list.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_donation_status.png b/app/src/main/res/drawable-xxhdpi/ic_donation_status.png
new file mode 100644
index 0000000..1e1dab5
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_donation_status.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_donation_white.png b/app/src/main/res/drawable-xxhdpi/ic_donation_white.png
new file mode 100644
index 0000000..bdc3e78
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_donation_white.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_edit.png b/app/src/main/res/drawable-xxhdpi/ic_edit.png
new file mode 100644
index 0000000..2e1937d
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_edit.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_kick_out.png b/app/src/main/res/drawable-xxhdpi/ic_kick_out.png
new file mode 100644
index 0000000..0594439
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_kick_out.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_message_send.png b/app/src/main/res/drawable-xxhdpi/ic_message_send.png
new file mode 100644
index 0000000..3a797f8
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_message_send.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_mic_off.png b/app/src/main/res/drawable-xxhdpi/ic_mic_off.png
new file mode 100644
index 0000000..9bc69ff
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_mic_off.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_mic_on.png b/app/src/main/res/drawable-xxhdpi/ic_mic_on.png
new file mode 100644
index 0000000..31ed38e
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_mic_on.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_mute.png b/app/src/main/res/drawable-xxhdpi/ic_mute.png
new file mode 100644
index 0000000..9d88d4c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_mute.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_noti.png b/app/src/main/res/drawable-xxhdpi/ic_noti.png
new file mode 100644
index 0000000..e03f3c5
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_noti.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_notice_normal.png b/app/src/main/res/drawable-xxhdpi/ic_notice_normal.png
new file mode 100644
index 0000000..830880c
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notice_normal.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_notice_selected.png b/app/src/main/res/drawable-xxhdpi/ic_notice_selected.png
new file mode 100644
index 0000000..6820e27
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notice_selected.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_request_speak.png b/app/src/main/res/drawable-xxhdpi/ic_request_speak.png
new file mode 100644
index 0000000..52a3b13
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_request_speak.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_seemore_vertical.png b/app/src/main/res/drawable-xxhdpi/ic_seemore_vertical.png
new file mode 100644
index 0000000..bc34f59
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_seemore_vertical.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_share.png b/app/src/main/res/drawable-xxhdpi/ic_share.png
new file mode 100644
index 0000000..5181ef2
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_share.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_speaker_off.png b/app/src/main/res/drawable-xxhdpi/ic_speaker_off.png
new file mode 100644
index 0000000..61e0381
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_speaker_off.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_speaker_on.png b/app/src/main/res/drawable-xxhdpi/ic_speaker_on.png
new file mode 100644
index 0000000..be48c2d
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_speaker_on.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/img_noti_mute.png b/app/src/main/res/drawable-xxhdpi/img_noti_mute.png
new file mode 100644
index 0000000..8bb4056
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/img_noti_mute.png differ
diff --git a/app/src/main/res/drawable/bg_bottom_round_corner_10_222222.xml b/app/src/main/res/drawable/bg_bottom_round_corner_10_222222.xml
new file mode 100644
index 0000000..5e84bf0
--- /dev/null
+++ b/app/src/main/res/drawable/bg_bottom_round_corner_10_222222.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_bottom_round_corner_4_7_2b2635.xml b/app/src/main/res/drawable/bg_bottom_round_corner_4_7_2b2635.xml
new file mode 100644
index 0000000..8d1d989
--- /dev/null
+++ b/app/src/main/res/drawable/bg_bottom_round_corner_4_7_2b2635.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_circle_4999e3.xml b/app/src/main/res/drawable/bg_circle_4999e3.xml
new file mode 100644
index 0000000..79dd10a
--- /dev/null
+++ b/app/src/main/res/drawable/bg_circle_4999e3.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_circle_6f3dec_9970ff.xml b/app/src/main/res/drawable/bg_circle_6f3dec_9970ff.xml
new file mode 100644
index 0000000..bc0b8e4
--- /dev/null
+++ b/app/src/main/res/drawable/bg_circle_6f3dec_9970ff.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_circle_9f9f9f_bbbbbb.xml b/app/src/main/res/drawable/bg_circle_9f9f9f_bbbbbb.xml
new file mode 100644
index 0000000..21c613d
--- /dev/null
+++ b/app/src/main/res/drawable/bg_circle_9f9f9f_bbbbbb.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_circle_9f9f9f_dcdcdc.xml b/app/src/main/res/drawable/bg_circle_9f9f9f_dcdcdc.xml
new file mode 100644
index 0000000..1e0edf9
--- /dev/null
+++ b/app/src/main/res/drawable/bg_circle_9f9f9f_dcdcdc.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_circle_e5a578_c67e4a.xml b/app/src/main/res/drawable/bg_circle_e5a578_c67e4a.xml
new file mode 100644
index 0000000..37e6bbe
--- /dev/null
+++ b/app/src/main/res/drawable/bg_circle_e5a578_c67e4a.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_circle_e6a77a_c67e4a.xml b/app/src/main/res/drawable/bg_circle_e6a77a_c67e4a.xml
new file mode 100644
index 0000000..9826cab
--- /dev/null
+++ b/app/src/main/res/drawable/bg_circle_e6a77a_c67e4a.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_circle_ffdc00_fdca2f.xml b/app/src/main/res/drawable/bg_circle_ffdc00_fdca2f.xml
new file mode 100644
index 0000000..f0936ac
--- /dev/null
+++ b/app/src/main/res/drawable/bg_circle_ffdc00_fdca2f.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_circle_ffdc00_ffb600.xml b/app/src/main/res/drawable/bg_circle_ffdc00_ffb600.xml
new file mode 100644
index 0000000..1f383f6
--- /dev/null
+++ b/app/src/main/res/drawable/bg_circle_ffdc00_ffb600.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_circle_ffffff_9f9f9f.xml b/app/src/main/res/drawable/bg_circle_ffffff_9f9f9f.xml
new file mode 100644
index 0000000..aaf3104
--- /dev/null
+++ b/app/src/main/res/drawable/bg_circle_ffffff_9f9f9f.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_10_232323_eeeeee.xml b/app/src/main/res/drawable/bg_round_corner_10_232323_eeeeee.xml
new file mode 100644
index 0000000..f8a0947
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_10_232323_eeeeee.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_10_99525252.xml b/app/src/main/res/drawable/bg_round_corner_10_99525252.xml
new file mode 100644
index 0000000..db8b84d
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_10_99525252.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_33ffffff_ffffff.xml b/app/src/main/res/drawable/bg_round_corner_13_3_33ffffff_ffffff.xml
new file mode 100644
index 0000000..e06556a
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_13_3_33ffffff_ffffff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_ffffff.xml b/app/src/main/res/drawable/bg_round_corner_13_3_ffffff.xml
new file mode 100644
index 0000000..8fb6ac3
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_13_3_ffffff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_transparent_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_9970ff.xml
new file mode 100644
index 0000000..9a637ff
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_9970ff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_transparent_bbbbbb.xml b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_bbbbbb.xml
new file mode 100644
index 0000000..0b0de7d
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_bbbbbb.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_13_3_transparent_ff5c49.xml b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_ff5c49.xml
new file mode 100644
index 0000000..0056003
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_13_3_transparent_ff5c49.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_15_transparent_bbbbbb.xml b/app/src/main/res/drawable/bg_round_corner_15_transparent_bbbbbb.xml
new file mode 100644
index 0000000..47b782e
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_15_transparent_bbbbbb.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_16_7_cc555555.xml b/app/src/main/res/drawable/bg_round_corner_16_7_cc555555.xml
new file mode 100644
index 0000000..7e189aa
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_16_7_cc555555.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_23_3_3e1b93_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_23_3_3e1b93_9970ff.xml
new file mode 100644
index 0000000..a23adaa
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_23_3_3e1b93_9970ff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_23_3_555555.xml b/app/src/main/res/drawable/bg_round_corner_23_3_555555.xml
new file mode 100644
index 0000000..d77e4d2
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_23_3_555555.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_23_3_transparent_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_23_3_transparent_9970ff.xml
new file mode 100644
index 0000000..647cb8d
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_23_3_transparent_9970ff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_2_4999e3.xml b/app/src/main/res/drawable/bg_round_corner_2_4999e3.xml
new file mode 100644
index 0000000..8cd3ccc
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_2_4999e3.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_3_3_99000000.xml b/app/src/main/res/drawable/bg_round_corner_3_3_99000000.xml
new file mode 100644
index 0000000..b5f2288
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_3_3_99000000.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_3_3_999970ff.xml b/app/src/main/res/drawable/bg_round_corner_3_3_999970ff.xml
new file mode 100644
index 0000000..540f1da
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_3_3_999970ff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_46_7_transparent_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_46_7_transparent_9970ff.xml
new file mode 100644
index 0000000..70de629
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_46_7_transparent_9970ff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_4_7_3d2a6c.xml b/app/src/main/res/drawable/bg_round_corner_4_7_3d2a6c.xml
new file mode 100644
index 0000000..5331f73
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_4_7_3d2a6c.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_333333.xml b/app/src/main/res/drawable/bg_round_corner_5_3_333333.xml
new file mode 100644
index 0000000..e5e61ab
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_5_3_333333.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_5_3_transparent_909090.xml b/app/src/main/res/drawable/bg_round_corner_5_3_transparent_909090.xml
new file mode 100644
index 0000000..79162be
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_5_3_transparent_909090.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_303030.xml b/app/src/main/res/drawable/bg_round_corner_6_7_303030.xml
new file mode 100644
index 0000000..d3e7bc5
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_303030.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_339970ff_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_6_7_339970ff_9970ff.xml
new file mode 100644
index 0000000..9066562
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_339970ff_9970ff.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_88333333.xml b/app/src/main/res/drawable/bg_round_corner_6_7_88333333.xml
new file mode 100644
index 0000000..33a5f01
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_88333333.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_c25264.xml b/app/src/main/res/drawable/bg_round_corner_6_7_c25264.xml
new file mode 100644
index 0000000..0c6c674
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_c25264.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e62d7390.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e62d7390.xml
new file mode 100644
index 0000000..bd801e8
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_e62d7390.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e64d6aa4.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e64d6aa4.xml
new file mode 100644
index 0000000..c8dc3b5
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_e64d6aa4.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e6548f7d.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e6548f7d.xml
new file mode 100644
index 0000000..e5a67df
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_e6548f7d.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e659548f.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e659548f.xml
new file mode 100644
index 0000000..2a8089c
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_e659548f.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e6d38c38.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e6d38c38.xml
new file mode 100644
index 0000000..a7fe57d
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_e6d38c38.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_e6d85e37.xml b/app/src/main/res/drawable/bg_round_corner_6_7_e6d85e37.xml
new file mode 100644
index 0000000..fc00014
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_6_7_e6d85e37.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_8_222222.xml b/app/src/main/res/drawable/bg_round_corner_8_222222.xml
new file mode 100644
index 0000000..a00cbf1
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_8_222222.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_round_corner_8_2b2635.xml b/app/src/main/res/drawable/bg_round_corner_8_2b2635.xml
new file mode 100644
index 0000000..aeb43de
--- /dev/null
+++ b/app/src/main/res/drawable/bg_round_corner_8_2b2635.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_top_round_corner_10_222222.xml b/app/src/main/res/drawable/bg_top_round_corner_10_222222.xml
new file mode 100644
index 0000000..28a06f3
--- /dev/null
+++ b/app/src/main/res/drawable/bg_top_round_corner_10_222222.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_top_round_corner_4_7_2b2635.xml b/app/src/main/res/drawable/bg_top_round_corner_4_7_2b2635.xml
new file mode 100644
index 0000000..c834f26
--- /dev/null
+++ b/app/src/main/res/drawable/bg_top_round_corner_4_7_2b2635.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_live_room.xml b/app/src/main/res/layout/activity_live_room.xml
new file mode 100644
index 0000000..38fe6f8
--- /dev/null
+++ b/app/src/main/res/layout/activity_live_room.xml
@@ -0,0 +1,496 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_live_room.xml b/app/src/main/res/layout/dialog_live_room.xml
new file mode 100644
index 0000000..e3c912e
--- /dev/null
+++ b/app/src/main/res/layout/dialog_live_room.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_live_room_donation.xml b/app/src/main/res/layout/dialog_live_room_donation.xml
new file mode 100644
index 0000000..b509bf8
--- /dev/null
+++ b/app/src/main/res/layout/dialog_live_room_donation.xml
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_live_room_donation_message.xml b/app/src/main/res/layout/dialog_live_room_donation_message.xml
new file mode 100644
index 0000000..09ed448
--- /dev/null
+++ b/app/src/main/res/layout/dialog_live_room_donation_message.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_live_room_donation_ranking.xml b/app/src/main/res/layout/dialog_live_room_donation_ranking.xml
new file mode 100644
index 0000000..8f30cb8
--- /dev/null
+++ b/app/src/main/res/layout/dialog_live_room_donation_ranking.xml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_live_room_info_update.xml b/app/src/main/res/layout/dialog_live_room_info_update.xml
new file mode 100644
index 0000000..f2ef649
--- /dev/null
+++ b/app/src/main/res/layout/dialog_live_room_info_update.xml
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_live_room_profile.xml b/app/src/main/res/layout/dialog_live_room_profile.xml
new file mode 100644
index 0000000..813f583
--- /dev/null
+++ b/app/src/main/res/layout/dialog_live_room_profile.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_live_room_user_profile.xml b/app/src/main/res/layout/dialog_live_room_user_profile.xml
new file mode 100644
index 0000000..0e0d997
--- /dev/null
+++ b/app/src/main/res/layout/dialog_live_room_user_profile.xml
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_profile_report.xml b/app/src/main/res/layout/dialog_profile_report.xml
new file mode 100644
index 0000000..508e0dc
--- /dev/null
+++ b/app/src/main/res/layout/dialog_profile_report.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_user_report.xml b/app/src/main/res/layout/dialog_user_report.xml
new file mode 100644
index 0000000..8a476ae
--- /dev/null
+++ b/app/src/main/res/layout/dialog_user_report.xml
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_room_chat.xml b/app/src/main/res/layout/item_live_room_chat.xml
new file mode 100644
index 0000000..3bd9b08
--- /dev/null
+++ b/app/src/main/res/layout/item_live_room_chat.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_room_donation_message.xml b/app/src/main/res/layout/item_live_room_donation_message.xml
new file mode 100644
index 0000000..e621120
--- /dev/null
+++ b/app/src/main/res/layout/item_live_room_donation_message.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_room_donation_ranking.xml b/app/src/main/res/layout/item_live_room_donation_ranking.xml
new file mode 100644
index 0000000..7a8099b
--- /dev/null
+++ b/app/src/main/res/layout/item_live_room_donation_ranking.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_room_donation_status_chat.xml b/app/src/main/res/layout/item_live_room_donation_status_chat.xml
new file mode 100644
index 0000000..7cca94f
--- /dev/null
+++ b/app/src/main/res/layout/item_live_room_donation_status_chat.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_room_join_chat.xml b/app/src/main/res/layout/item_live_room_join_chat.xml
new file mode 100644
index 0000000..fbf9540
--- /dev/null
+++ b/app/src/main/res/layout/item_live_room_join_chat.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_room_list_profile.xml b/app/src/main/res/layout/item_live_room_list_profile.xml
new file mode 100644
index 0000000..71030f4
--- /dev/null
+++ b/app/src/main/res/layout/item_live_room_list_profile.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_room_profile.xml b/app/src/main/res/layout/item_live_room_profile.xml
new file mode 100644
index 0000000..759e211
--- /dev/null
+++ b/app/src/main/res/layout/item_live_room_profile.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_room_profile_header.xml b/app/src/main/res/layout/item_live_room_profile_header.xml
new file mode 100644
index 0000000..292782d
--- /dev/null
+++ b/app/src/main/res/layout/item_live_room_profile_header.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_room_profile_manager.xml b/app/src/main/res/layout/item_live_room_profile_manager.xml
new file mode 100644
index 0000000..8bfe131
--- /dev/null
+++ b/app/src/main/res/layout/item_live_room_profile_manager.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_live_room_profile_master.xml b/app/src/main/res/layout/item_live_room_profile_master.xml
new file mode 100644
index 0000000..7dcb243
--- /dev/null
+++ b/app/src/main/res/layout/item_live_room_profile_master.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/audio_content_detail_creator_menu.xml b/app/src/main/res/menu/audio_content_detail_creator_menu.xml
new file mode 100644
index 0000000..e7ec1c2
--- /dev/null
+++ b/app/src/main/res/menu/audio_content_detail_creator_menu.xml
@@ -0,0 +1,13 @@
+
+
diff --git a/app/src/main/res/menu/audio_content_detail_user_menu.xml b/app/src/main/res/menu/audio_content_detail_user_menu.xml
new file mode 100644
index 0000000..ebe6795
--- /dev/null
+++ b/app/src/main/res/menu/audio_content_detail_user_menu.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/app/src/main/res/menu/review_option_menu.xml b/app/src/main/res/menu/review_option_menu.xml
new file mode 100644
index 0000000..2c0296c
--- /dev/null
+++ b/app/src/main/res/menu/review_option_menu.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/app/src/main/res/menu/user_profile_option_menu.xml b/app/src/main/res/menu/user_profile_option_menu.xml
new file mode 100644
index 0000000..529a98c
--- /dev/null
+++ b/app/src/main/res/menu/user_profile_option_menu.xml
@@ -0,0 +1,18 @@
+
+
diff --git a/app/src/main/res/menu/user_profile_option_menu_2.xml b/app/src/main/res/menu/user_profile_option_menu_2.xml
new file mode 100644
index 0000000..2747bf1
--- /dev/null
+++ b/app/src/main/res/menu/user_profile_option_menu_2.xml
@@ -0,0 +1,18 @@
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 1d8a96e..516b49d 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -31,11 +31,40 @@
#DD4500
#1F1734
#333333
+ #A285EB
+ #3D2A6C
+ #FFDC00
+ #4999E3
+ #C25264
#B3909090
#88909090
#339970FF
#7FE2E2E2
#4D9970FF
- #A285EB
+ #44000000
+ #26909090
+ #99525252
+ #CC555555
+ #999970ff
+ #E6548F7D
+ #E62D7390
+ #E64D6AA4
+ #E659548F
+ #E6D38C38
+ #E6D85E37
+ #33FFFFFF
+ #303030
+ #555555
+ #3E1B93
+ #88333333
+ #1B1B1B
+ #6F3DEC
+ #9F9F9F
+ #DCDCDC
+ #C67E4A
+ #E5A578
+ #E6A77A
+ #FFB600
+ #99000000