라이브 방 추가

This commit is contained in:
klaus 2023-08-01 07:04:16 +09:00
parent 8a094adc4f
commit c2618669c8
151 changed files with 7972 additions and 16 deletions

View File

@ -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'
}

View File

@ -39,6 +39,7 @@
<activity android:name=".live.room.create.LiveRoomCreateActivity" />
<activity android:name=".live.room.update.LiveRoomEditActivity" />
<activity android:name=".live.reservation.complete.LiveReservationCompleteActivity" />
<activity android:name=".live.room.LiveRoomActivity" />
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"

View File

@ -0,0 +1,205 @@
package kr.co.vividnext.sodalive.agora
import android.content.Context
import com.orhanobut.logger.Logger
import io.agora.rtc2.Constants
import io.agora.rtc2.IRtcEngineEventHandler
import io.agora.rtc2.RtcEngine
import io.agora.rtm.ErrorInfo
import io.agora.rtm.ResultCallback
import io.agora.rtm.RtmChannel
import io.agora.rtm.RtmChannelListener
import io.agora.rtm.RtmClient
import io.agora.rtm.RtmClientListener
import io.agora.rtm.SendMessageOptions
import kr.co.vividnext.sodalive.BuildConfig
import kr.co.vividnext.sodalive.live.room.LiveRoomRequestType
import kotlin.concurrent.thread
class Agora(
private val context: Context,
private val rtcEventHandler: IRtcEngineEventHandler,
private val rtmClientListener: RtmClientListener
) {
// RTM client instance
private var rtmClient: RtmClient? = null
// RTM channel instance
private var rtmChannel: RtmChannel? = null
private var rtcEngine: RtcEngine? = null
init {
initAgoraEngine()
}
private fun initAgoraEngine() {
try {
rtcEngine = RtcEngine.create(
context,
BuildConfig.AGORA_APP_ID,
rtcEventHandler
)
rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
rtcEngine!!.setAudioProfile(
Constants.AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO,
Constants.AUDIO_SCENARIO_GAME_STREAMING
)
rtcEngine!!.enableAudio()
rtcEngine!!.enableAudioVolumeIndication(500, 3, true)
rtmClient = RtmClient.createInstance(
context,
BuildConfig.AGORA_APP_ID,
rtmClientListener
)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun deInitAgoraEngine() {
if (rtcEngine != null) {
rtcEngine!!.leaveChannel()
thread {
RtcEngine.destroy()
rtcEngine = null
}
}
rtmChannel?.leave(null)
rtmChannel?.release()
rtmClient?.logout(null)
}
fun inputChat(message: String) {
val rtmMessage = rtmClient!!.createMessage()
rtmMessage.text = message
rtmChannel!!.sendMessage(
rtmMessage,
object : ResultCallback<Void?> {
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<Void> {
override fun onSuccess(p0: Void?) {
rtmChannel!!.join(object : ResultCallback<Void> {
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<Void?> {
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<Void?> {
override fun onSuccess(aVoid: Void?) {
onSuccess()
}
override fun onFailure(errorInfo: ErrorInfo) {
}
}
)
}
fun rtmChannelIsNull(): Boolean {
return rtmChannel == null
}
fun getConnectionState(): Int {
return rtcEngine!!.connectionState
}
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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(

View File

@ -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)

View File

@ -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<ApiResponse<Any>>
@GET("/live/room/info/{id}")
fun getRoomInfo(
@Path("id") id: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetRoomInfoResponse>>
@GET("/live/room/donation-message")
fun getDonationMessageList(
@Query("roomId") roomId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<List<LiveRoomDonationMessage>>>
@HTTP(method = "DELETE", path = "/live/room/donation-message", hasBody = true)
fun deleteDonationMessage(
@Body request: DeleteLiveRoomDonationMessage,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@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<ApiResponse<GetLiveRoomUserProfileResponse>>
@GET("/live/room/{id}/donation-total")
fun getDonationTotal(
@Path("id") id: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetLiveRoomDonationTotalResponse>>
@PUT("/live/room/info/set/speaker")
fun setSpeaker(
@Body request: SetManagerOrSpeakerOrAudienceRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/live/room/info/set/listener")
fun setListener(
@Body request: SetManagerOrSpeakerOrAudienceRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/live/room/kick-out")
fun kickOut(
@Body request: LiveRoomKickOutRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/live/room/donation")
fun donation(
@Body request: LiveRoomDonationRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/live/room/donation/refund/{id}")
fun refundDonation(
@Path("id") id: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/live/room/quit")
fun quitRoom(
@Query("id") roomId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@PUT("/live/room/info/set/manager")
fun setManager(
@Body request: SetManagerOrSpeakerOrAudienceRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/live/room/{id}/donation-list")
fun donationStatus(
@Path("id") id: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetLiveRoomDonationStatusResponse>>
}

View File

@ -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>(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>(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>(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>(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>(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>(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
)
}
}
}
}

View File

@ -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<ApiResponse<Any>> {
return api.setSpeaker(
request = SetManagerOrSpeakerOrAudienceRequest(roomId, accountId = userId),
authHeader = token
)
}
fun setListener(roomId: Long, userId: Long, token: String): Single<ApiResponse<Any>> {
return api.setListener(
request = SetManagerOrSpeakerOrAudienceRequest(roomId, accountId = userId),
authHeader = token
)
}
fun kickOut(roomId: Long, userId: Long, token: String): Single<ApiResponse<Any>> {
return api.kickOut(
request = LiveRoomKickOutRequest(roomId, userId),
authHeader = token
)
}
fun donation(
roomId: Long,
can: Int,
message: String,
token: String
): Single<ApiResponse<Any>> {
return api.donation(
request = LiveRoomDonationRequest(
roomId = roomId,
can = can,
message = message,
container = "aos"
),
authHeader = token
)
}
fun refundDonation(roomId: Long, token: String): Single<ApiResponse<Any>> {
return api.refundDonation(
id = roomId,
authHeader = token
)
}
fun quitRoom(roomId: Long, token: String): Single<ApiResponse<Any>> {
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)
}

View File

@ -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(

View File

@ -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
)

File diff suppressed because it is too large Load Diff

View File

@ -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
}
}

View File

@ -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,
}

View File

@ -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<GetRoomInfoResponse>()
val roomInfoLiveData: LiveData<GetRoomInfoResponse>
get() = _roomInfoLiveData
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _isShowNotice = MutableLiveData(true)
val isShowNotice: LiveData<Boolean>
get() = _isShowNotice
private val _isExpandNotice = MutableLiveData(false)
val isExpandNotice: LiveData<Boolean>
get() = _isExpandNotice
private val _totalDonationCan = MutableLiveData(0)
val totalDonationCan: LiveData<Int>
get() = _totalDonationCan
private val _userProfileLiveData = MutableLiveData<GetLiveRoomUserProfileResponse>()
val userProfileLiveData: LiveData<GetLiveRoomUserProfileResponse>
get() = _userProfileLiveData
lateinit var roomInfoResponse: GetRoomInfoResponse
fun isRoomInfoInitialized() = this::roomInfoResponse.isInitialized
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private var _isBgOn = MutableLiveData(true)
val isBgOn: LiveData<Boolean>
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("신고가 접수되었습니다.")
}
)
)
}
}

View File

@ -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
)

View File

@ -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)
}
}

View File

@ -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<LiveRoomChatViewHolder>() {
val items = mutableListOf<LiveRoomChat>()
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)
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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<GetLiveRoomDonationItem>,
@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
)

View File

@ -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
)

View File

@ -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())
}
}
}

View File

@ -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
)

View File

@ -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<LiveRoomDonationMessageAdapter.ViewHolder>() {
private var items = mutableListOf<LiveRoomDonationMessage>()
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<LiveRoomDonationMessage>) {
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
}
}

View File

@ -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<List<LiveRoomDonationMessage>>,
private val donationMessageCountLiveData: LiveData<Int>,
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()
}
}

View File

@ -0,0 +1,24 @@
package kr.co.vividnext.sodalive.live.room.donation
import androidx.recyclerview.widget.DiffUtil
class LiveRoomDonationMessageDiffUtilCallback(
private val oldData: List<LiveRoomDonationMessage>,
private val newData: List<LiveRoomDonationMessage>
) : 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]
}

View File

@ -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<List<LiveRoomDonationMessage>>()
val donationMessageListLiveData: LiveData<List<LiveRoomDonationMessage>>
get() = _donationMessageListLiveData
private val _donationMessageCountLiveData = MutableLiveData(0)
val donationMessageCountLiveData: LiveData<Int>
get() = _donationMessageCountLiveData
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private var _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
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("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
}
)
)
}
}

View File

@ -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<LiveRoomDonationRankingAdapter.ViewHolder>() {
val items = mutableListOf<GetLiveRoomDonationItem>()
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
}
}
}
}
}

View File

@ -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()
}
}

View File

@ -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
)

View File

@ -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<LiveRoomMember>,
@SerializedName("listenerList") val listenerList: List<LiveRoomMember>,
@SerializedName("managerList") val managerList: List<LiveRoomMember>,
@SerializedName("donationRankingTop3UserIds") val donationRankingTop3UserIds: List<Long>,
@SerializedName("isAvailableDonation") val isAvailableDonation: Boolean = false,
@SerializedName("isPrivateRoom") val isPrivateRoom: Boolean,
@SerializedName("password") val password: String? = null
)

View File

@ -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
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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<Any>,
private val newData: List<Any>
) : 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]
}

View File

@ -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<LiveRoomProfileViewHolder>() {
private val items = mutableListOf<LiveRoomProfileItem>()
var managerId: Long = -1
var totalUserCount: Int = 0
@SuppressLint("NotifyDataSetChanged")
fun updateList(
speakers: List<LiveRoomMember>,
listeners: List<LiveRoomMember>,
managers: List<LiveRoomMember>
) {
val items = mutableListOf<LiveRoomProfileItem>()
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)
}
}

View File

@ -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<GetRoomInfoResponse>,
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()
}
}

View File

@ -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
}
}
}

View File

@ -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<LiveRoomProfileListAdapter.ViewHolder>() {
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<LiveRoomMember>()
val activeSpeakers = mutableSetOf<Int>()
val muteSpeakers = mutableSetOf<Int>()
var managerId: Long = -1
fun updateList(items: List<LiveRoomMember>) {
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()
}

View File

@ -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<GetLiveRoomUserProfileResponse>,
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
)
}
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<ApiResponse<Any>>
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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<RadioButton>(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
}
}

View File

@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.user
import com.google.gson.annotations.SerializedName
data class CreatorFollowRequestRequest(@SerializedName("creatorId") val creatorId: Long)

View File

@ -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<ApiResponse<MyPageResponse>>
@POST("/member/block")
fun memberBlock(
@Body request: MemberBlockRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/member/unblock")
fun memberUnBlock(
@Body request: MemberBlockRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/member/creator/follow")
fun creatorFollow(
request: Any,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/member/creator/unfollow")
fun creatorUnFollow(
request: Any,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
}

View File

@ -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<ApiResponse<MyPageResponse>> {
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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_222222" />
<corners
android:bottomLeftRadius="10dp"
android:bottomRightRadius="10dp" />
<stroke
android:width="1dp"
android:color="@color/color_222222" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_2b2635" />
<corners
android:bottomLeftRadius="4.7dp"
android:bottomRightRadius="4.7dp" />
<stroke
android:width="1dp"
android:color="@color/color_2b2635" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:endColor="@color/color_4999e3"
android:gradientRadius="50%"
android:startColor="@color/color_4999e3"
android:type="radial" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:gradientRadius="50%"
android:endColor="@color/color_6f3dec"
android:startColor="@color/color_9970ff"
android:type="radial" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:gradientRadius="50%"
android:endColor="@color/color_bbbbbb"
android:startColor="@color/color_9f9f9f"
android:type="radial" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:gradientRadius="50%"
android:endColor="@color/color_dcdcdc"
android:startColor="@color/color_9f9f9f"
android:type="radial" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:gradientRadius="50%"
android:endColor="@color/color_c67e4a"
android:startColor="@color/color_e5a578"
android:type="radial" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:gradientRadius="50%"
android:endColor="@color/color_c67e4a"
android:startColor="@color/color_e6a77a"
android:type="radial" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:gradientRadius="50%"
android:endColor="@color/color_fdca2f"
android:startColor="@color/color_ffdc00"
android:type="radial" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:gradientRadius="50%"
android:endColor="@color/color_ffb600"
android:startColor="@color/color_ffdc00"
android:type="radial" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<gradient
android:gradientRadius="50%"
android:endColor="@color/white"
android:startColor="@color/color_9f9f9f"
android:type="radial" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_232323" />
<corners android:radius="10dp" />
<stroke
android:width="1dp"
android:color="@color/color_eeeeee" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_99525252" />
<corners android:radius="10dp" />
<stroke
android:width="1dp"
android:color="@color/color_99525252" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/color_33ffffff" />
<corners android:radius="13.3dp" />
<stroke
android:width="1dp"
android:color="@color/white" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/white" />
<corners android:radius="13.3dp" />
<stroke
android:width="1dp"
android:color="@color/white" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<corners android:radius="13.3dp" />
<stroke
android:width="1dp"
android:color="@color/color_9970ff" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<corners android:radius="13.3dp" />
<stroke
android:width="1dp"
android:color="@color/color_bbbbbb" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<corners android:radius="13.3dp" />
<stroke
android:width="1dp"
android:color="@color/color_ff5c49" />
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/transparent" />
<corners android:radius="15dp" />
<stroke
android:width="1dp"
android:color="@color/color_bbbbbb" />
</shape>

Some files were not shown because too many files have changed in this diff Show More