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