feat(profile): 채널 후원 영역과 전체보기 흐름을 추가한다

This commit is contained in:
2026-02-25 20:57:30 +09:00
parent 5b83ae69dd
commit 092fc67b0b
27 changed files with 1049 additions and 5 deletions

View File

@@ -134,6 +134,7 @@
android:windowSoftInputMode="stateAlwaysHidden|adjustPan" />
<activity android:name=".explorer.profile.UserProfileActivity" />
<activity android:name=".explorer.profile.donation.UserProfileDonationAllViewActivity" />
<activity android:name=".explorer.profile.channel_donation.UserProfileChannelDonationAllViewActivity" />
<activity android:name=".explorer.profile.fantalk.UserProfileFantalkAllViewActivity" />
<activity android:name=".explorer.profile.follow.UserFollowerListActivity" />
<activity android:name=".explorer.profile.creator_community.all.CreatorCommunityAllActivity" />

View File

@@ -71,6 +71,7 @@ import kr.co.vividnext.sodalive.common.ApiBuilder
import kr.co.vividnext.sodalive.explorer.ExplorerApi
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.explorer.ExplorerViewModel
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.UserProfileChannelDonationAllViewModel
import kr.co.vividnext.sodalive.explorer.profile.UserProfileViewModel
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityApi
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
@@ -317,6 +318,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
viewModel { LiveRoomDonationMessageViewModel(get()) }
viewModel { ExplorerViewModel(get()) }
viewModel { UserProfileViewModel(get(), get(), get()) }
viewModel { UserProfileChannelDonationAllViewModel(get()) }
viewModel { UserFollowerListViewModel(get(), get()) }
viewModel { TextMessageViewModel(get()) }
viewModel { TextMessageWriteViewModel(get()) }

View File

@@ -8,6 +8,8 @@ import kr.co.vividnext.sodalive.explorer.profile.GetCreatorProfileResponse
import kr.co.vividnext.sodalive.explorer.profile.detail.GetCreatorDetailResponse
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.GetChannelDonationListResponse
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.PostChannelDonationRequest
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
import kr.co.vividnext.sodalive.explorer.profile.follow.GetFollowerListResponse
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
@@ -80,6 +82,18 @@ interface ExplorerApi {
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@POST("/explorer/profile/channel-donation")
fun postChannelDonation(
@Body request: PostChannelDonationRequest,
@Header("Authorization") authHeader: String
): Single<ApiResponse<Any>>
@GET("/explorer/profile/channel-donation")
fun getChannelDonationList(
@Query("creatorId") creatorId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<GetChannelDonationListResponse>>
@GET("/explorer/profile/{id}/follower-list")
fun getFollowerList(
@Path("id") userId: Long,

View File

@@ -7,6 +7,8 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.explorer.profile.GetCheersResponse
import kr.co.vividnext.sodalive.explorer.profile.cheers.PostWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.GetChannelDonationListResponse
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.PostChannelDonationRequest
import kr.co.vividnext.sodalive.explorer.profile.donation.GetDonationAllResponse
import java.util.TimeZone
@@ -69,6 +71,22 @@ class ExplorerRepository(
authHeader = token
)
fun postChannelDonation(
request: PostChannelDonationRequest,
token: String
) = api.postChannelDonation(
request = request,
authHeader = token
)
fun getChannelDonationList(
creatorId: Long,
token: String
): Single<ApiResponse<GetChannelDonationListResponse>> = api.getChannelDonationList(
creatorId = creatorId,
authHeader = token
)
fun getFollowerList(
userId: Long,
page: Int,

View File

@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.explorer.profile
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.GetChannelDonationListItem
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
@Keep
@@ -25,10 +26,10 @@ data class GetCreatorProfileResponse(
val notice: String,
@SerializedName("communityPostList")
val communityPostList: List<GetCommunityPostListResponse>,
@SerializedName("channelDonationList")
val channelDonationList: List<GetChannelDonationListItem>,
@SerializedName("cheers")
val cheers: GetCheersResponse,
@SerializedName("activitySummary")
val activitySummary: GetCreatorActivitySummary,
@SerializedName("seriesList")
val seriesList: List<GetSeriesListResponse.SeriesListItem>,
@SerializedName("isBlock")

View File

@@ -46,6 +46,9 @@ import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.databinding.ActivityUserProfileBinding
import kr.co.vividnext.sodalive.databinding.ItemCreatorCommunityBinding
import kr.co.vividnext.sodalive.dialog.MemberProfileDialog
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.GetChannelDonationListItem
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.UserProfileChannelDonationAdapter
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.UserProfileChannelDonationAllViewActivity
import kr.co.vividnext.sodalive.explorer.profile.cheers.UserProfileCheersAdapter
import kr.co.vividnext.sodalive.explorer.profile.creator_community.GetCommunityPostListResponse
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
@@ -65,6 +68,7 @@ import kr.co.vividnext.sodalive.live.reservation.complete.LiveReservationComplet
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog
import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationDialog
import kr.co.vividnext.sodalive.live.room.menu.MenuConfigActivity
import kr.co.vividnext.sodalive.live.roulette.config.RouletteConfigActivity
import kr.co.vividnext.sodalive.report.CheersReportDialog
@@ -92,6 +96,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
private lateinit var audioContentAdapter: AudioContentAdapter
private lateinit var seriesAdapter: UserProfileSeriesListAdapter
private lateinit var donationAdapter: UserProfileDonationAdapter
private lateinit var channelDonationAdapter: UserProfileChannelDonationAdapter
private lateinit var cheersAdapter: UserProfileCheersAdapter
private val handler = Handler(Looper.getMainLooper())
@@ -137,6 +142,7 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
setupLiveView()
setupDonationView()
setupChannelDonationView()
setupFanTalkView()
setupSeriesListView()
setupAudioContentListView()
@@ -338,6 +344,68 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
setupCheersView()
}
private fun setupChannelDonationView() {
binding.layoutUserProfileChannelDonation.tvAll.setOnClickListener {
val intent = Intent(applicationContext, UserProfileChannelDonationAllViewActivity::class.java)
intent.putExtra(Constants.EXTRA_USER_ID, userId)
startActivity(intent)
}
binding.layoutUserProfileChannelDonation.llChannelDonation.setOnClickListener {
val dialog = LiveRoomDonationDialog(
this,
LayoutInflater.from(this),
isLiveDonation = true,
messageMaxLength = 100
) { can, message, isSecret ->
viewModel.postChannelDonation(
creatorId = userId,
can = can,
isSecret = isSecret,
message = message
) {
viewModel.getCreatorProfile(userId)
}
}
dialog.show(screenWidth - 26.7f.dpToPx().toInt())
}
val recyclerView = binding.layoutUserProfileChannelDonation.rvChannelDonation
channelDonationAdapter = UserProfileChannelDonationAdapter()
recyclerView.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.HORIZONTAL,
false
)
recyclerView.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.left = 13.3f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
channelDonationAdapter.itemCount - 1 -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 13.3f.dpToPx().toInt()
}
else -> {
outRect.left = 6.7f.dpToPx().toInt()
outRect.right = 6.7f.dpToPx().toInt()
}
}
}
})
recyclerView.adapter = channelDonationAdapter
}
private fun setupCheersView() {
binding.layoutUserProfileFanTalk.ivSend.setOnClickListener {
hideKeyboard {
@@ -616,9 +684,12 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
it.totalContentCount,
it.ownedContentCount
)
setChannelDonationList(it.channelDonationList)
setLiveRoomList(it.liveRoomList)
setUserDonationRanking(it.userDonationRanking)
setCommunityPostList(it.communityPostList)
} else {
binding.layoutUserProfileChannelDonation.root.visibility = View.GONE
}
}
}
@@ -968,6 +1039,21 @@ class UserProfileActivity : BaseActivity<ActivityUserProfileBinding>(
}
}
@SuppressLint("NotifyDataSetChanged")
private fun setChannelDonationList(channelDonationItems: List<GetChannelDonationListItem>) {
binding.layoutUserProfileChannelDonation.root.visibility = View.VISIBLE
binding.layoutUserProfileChannelDonation.tvNoChannelDonation.visibility =
if (channelDonationItems.isEmpty()) View.VISIBLE else View.GONE
binding.layoutUserProfileChannelDonation.rvChannelDonation.visibility =
if (channelDonationItems.isEmpty()) View.GONE else View.VISIBLE
binding.layoutUserProfileChannelDonation.tvAll.visibility =
if (channelDonationItems.isEmpty()) View.GONE else View.VISIBLE
channelDonationAdapter.items.clear()
channelDonationAdapter.items.addAll(channelDonationItems)
channelDonationAdapter.notifyDataSetChanged()
}
private fun setCommunityPost(
layout: ItemCreatorCommunityBinding,
item: GetCommunityPostListResponse,

View File

@@ -12,6 +12,8 @@ import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.common.Utils
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
import kr.co.vividnext.sodalive.explorer.profile.cheers.PutModifyCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.GetChannelDonationListResponse
import kr.co.vividnext.sodalive.explorer.profile.channel_donation.PostChannelDonationRequest
import kr.co.vividnext.sodalive.explorer.profile.detail.GetCreatorDetailResponse
import kr.co.vividnext.sodalive.report.ReportRepository
import kr.co.vividnext.sodalive.report.ReportRequest
@@ -35,6 +37,10 @@ class UserProfileViewModel(
val creatorProfileLiveData: LiveData<GetCreatorProfileResponse>
get() = _creatorProfileLiveData
private val _channelDonationLiveData = MutableLiveData<GetChannelDonationListResponse>()
val channelDonationLiveData: LiveData<GetChannelDonationListResponse>
get() = _channelDonationLiveData
fun cheersReport(cheersId: Long, reason: String) {
_isLoading.value = true
@@ -356,6 +362,89 @@ class UserProfileViewModel(
onSuccess("보이스온 ${nickname}님의 채널입니다.\n$shareUrl")
}
fun getChannelDonationList(creatorId: Long) {
compositeDisposable.add(
repository.getChannelDonationList(
creatorId = creatorId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
if (it.success && it.data != null) {
_channelDonationLiveData.postValue(it.data)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
},
{
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
)
}
fun postChannelDonation(
creatorId: Long,
can: Int,
isSecret: Boolean,
message: String,
onSuccess: () -> Unit
) {
_isLoading.value = true
compositeDisposable.add(
repository.postChannelDonation(
request = PostChannelDonationRequest(
creatorId = creatorId,
can = can,
isSecret = isSecret,
message = message
),
token = "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(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
)
}
fun userBlock(userId: Long) {
_isLoading.value = true
compositeDisposable.add(

View File

@@ -0,0 +1,93 @@
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
import android.content.Context
import kr.co.vividnext.sodalive.R
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
fun GetChannelDonationListItem.relativeTimeText(context: Context): String {
val pastMillis = parseServerUtcToMillis(createdAt)
?: return context.getString(R.string.character_comment_time_just_now)
val nowMillis = System.currentTimeMillis()
var diff = nowMillis - pastMillis
if (diff < 0) diff = 0
val minute = 60_000L
val hour = 60 * minute
val day = 24 * hour
if (diff < minute) {
return context.getString(R.string.character_comment_time_just_now)
}
if (diff < hour) {
val minutes = (diff / minute).toInt()
return context.getString(R.string.character_comment_time_minutes, minutes)
}
if (diff < day) {
val hours = (diff / hour).toInt()
return context.getString(R.string.character_comment_time_hours, hours)
}
if (diff < 30 * day) {
val days = (diff / day).toInt()
return context.getString(R.string.character_comment_time_days, days)
}
val tz = TimeZone.getDefault()
val calNow = Calendar.getInstance(tz, Locale.getDefault())
val calPast = Calendar.getInstance(tz, Locale.getDefault())
calPast.timeInMillis = pastMillis
var years = calNow.get(Calendar.YEAR) - calPast.get(Calendar.YEAR)
val nowMonth = calNow.get(Calendar.MONTH)
val pastMonth = calPast.get(Calendar.MONTH)
val nowDay = calNow.get(Calendar.DAY_OF_MONTH)
val pastDay = calPast.get(Calendar.DAY_OF_MONTH)
if (nowMonth < pastMonth || (nowMonth == pastMonth && nowDay < pastDay)) {
years -= 1
}
if (years < 1) {
var months = (calNow.get(Calendar.YEAR) - calPast.get(Calendar.YEAR)) * 12 + (nowMonth - pastMonth)
if (nowDay < pastDay) months -= 1
if (months < 1) months = 1
return context.getString(R.string.character_comment_time_months, months)
}
return context.getString(R.string.character_comment_time_years, years)
}
private fun parseServerUtcToMillis(dateUtc: String?): Long? {
if (dateUtc.isNullOrBlank()) return null
val value = dateUtc.trim()
if (value.all { it.isDigit() }) {
return try { value.toLong() } catch (_: NumberFormatException) { null }
}
val patterns = listOf(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
"yyyy-MM-dd'T'HH:mm:ss'Z'",
"yyyy-MM-dd'T'HH:mm:ss",
"yyyy-MM-dd HH:mm:ss",
"yyyy/MM/dd HH:mm:ss"
)
for (pattern in patterns) {
try {
val sdf = SimpleDateFormat(pattern, Locale.US)
sdf.timeZone = TimeZone.getTimeZone("UTC")
val parsed: Date? = sdf.parse(value)
if (parsed != null) return parsed.time
} catch (_: ParseException) { }
}
return null
}

View File

@@ -0,0 +1,56 @@
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
import android.content.Context
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import kr.co.vividnext.sodalive.R
@DrawableRes
fun GetChannelDonationListItem.chatDonationBackgroundRes(): Int {
if (isSecret) return R.drawable.bg_round_corner_6_7_cc59548f
return when {
can >= 10000 -> R.drawable.bg_round_corner_6_7_ccc25264
can >= 5000 -> R.drawable.bg_round_corner_6_7_ccd85e37
can >= 1000 -> R.drawable.bg_round_corner_6_7_ccd38c38
can >= 500 -> R.drawable.bg_round_corner_6_7_cc59548f
can >= 100 -> R.drawable.bg_round_corner_6_7_cc4d6aa4
can >= 50 -> R.drawable.bg_round_corner_6_7_cc2d7390
else -> R.drawable.bg_round_corner_6_7_cc548f7d
}
}
fun GetChannelDonationListItem.channelDonationContentText(
context: Context,
maxLength: Int? = null
) =
run {
val contentText = if (maxLength != null && message.length > maxLength) {
"${message.take(maxLength)}..."
} else {
message
}
val canRange = Regex("[0-9,]+\\s*캔").find(contentText)?.range
SpannableString(contentText).apply {
setSpan(
ForegroundColorSpan(
ContextCompat.getColor(context, R.color.color_cfd8dc)
),
0,
contentText.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
if (canRange != null) {
setSpan(
ForegroundColorSpan(
ContextCompat.getColor(context, R.color.color_fdca2f)
),
canRange.first,
canRange.last + 1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}

View File

@@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class GetChannelDonationListResponse(
@SerializedName("totalCount") val totalCount: Int,
@SerializedName("items") val items: List<GetChannelDonationListItem>
)
@Keep
data class GetChannelDonationListItem(
@SerializedName("id") val id: Long,
@SerializedName("memberId") val memberId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileUrl") val profileUrl: String,
@SerializedName("can") val can: Int,
@SerializedName("isSecret") val isSecret: Boolean,
@SerializedName("message") val message: String,
@SerializedName("createdAt") val createdAt: String
)

View File

@@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class PostChannelDonationRequest(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("can") val can: Int,
@SerializedName("isSecret") val isSecret: Boolean = false,
@SerializedName("message") val message: String = "",
@SerializedName("container") val container: String = "aos"
)

View File

@@ -0,0 +1,51 @@
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemUserProfileChannelDonationBinding
class UserProfileChannelDonationAdapter : RecyclerView.Adapter<UserProfileChannelDonationAdapter.ViewHolder>() {
val items = mutableListOf<GetChannelDonationListItem>()
class ViewHolder(
private val binding: ItemUserProfileChannelDonationBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetChannelDonationListItem) {
binding.ivProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvNickname.text = item.nickname
binding.tvDate.text = item.relativeTimeText(binding.root.context)
binding.tvContent.text = item.channelDonationContentText(
context = binding.root.context,
maxLength = 30
)
binding.root.setBackgroundResource(item.chatDonationBackgroundRes())
binding.root.setOnClickListener(null)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemUserProfileChannelDonationBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
}

View File

@@ -0,0 +1,61 @@
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.databinding.ItemUserProfileChannelDonationAllBinding
class UserProfileChannelDonationAllAdapter : RecyclerView.Adapter<UserProfileChannelDonationAllAdapter.ViewHolder>() {
val items = mutableListOf<GetChannelDonationListItem>()
private val expandedItemIds = mutableSetOf<Long>()
inner class ViewHolder(
private val binding: ItemUserProfileChannelDonationAllBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: GetChannelDonationListItem) {
binding.ivProfile.load(item.profileUrl) {
crossfade(true)
placeholder(R.drawable.ic_place_holder)
transformations(CircleCropTransformation())
}
binding.tvNickname.text = item.nickname
binding.tvDate.text = item.relativeTimeText(binding.root.context)
val isExpanded = expandedItemIds.contains(item.id)
binding.tvContent.text = item.channelDonationContentText(
context = binding.root.context,
maxLength = if (isExpanded) null else 30
)
binding.root.setBackgroundResource(item.chatDonationBackgroundRes())
binding.tvContent.setOnClickListener {
if (!isExpanded && item.message.length > 30) {
expandedItemIds.add(item.id)
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
notifyItemChanged(position)
}
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemUserProfileChannelDonationAllBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
}

View File

@@ -0,0 +1,95 @@
package kr.co.vividnext.sodalive.explorer.profile.channel_donation
import android.annotation.SuppressLint
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.databinding.ActivityUserProfileChannelDonationAllBinding
import kr.co.vividnext.sodalive.extensions.dpToPx
import org.koin.android.ext.android.inject
class UserProfileChannelDonationAllViewActivity : BaseActivity<ActivityUserProfileChannelDonationAllBinding>(
ActivityUserProfileChannelDonationAllBinding::inflate
) {
private val viewModel: UserProfileChannelDonationAllViewModel by inject()
private lateinit var loadingDialog: LoadingDialog
private lateinit var adapter: UserProfileChannelDonationAllAdapter
private var userId: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
userId = intent.getLongExtra(Constants.EXTRA_USER_ID, 0)
super.onCreate(savedInstanceState)
if (userId > 0) {
bindData()
viewModel.getChannelDonationList(userId)
} else {
Toast.makeText(
applicationContext,
getString(R.string.error_invalid_request),
Toast.LENGTH_LONG
).show()
finish()
}
}
override fun setupView() {
loadingDialog = LoadingDialog(this, layoutInflater)
binding.toolbar.tvBack.setText(R.string.screen_user_profile_channel_donation_all_title)
binding.toolbar.tvBack.setOnClickListener { finish() }
val recyclerView = binding.rvChannelDonation
adapter = UserProfileChannelDonationAllAdapter()
recyclerView.layoutManager = LinearLayoutManager(
applicationContext,
LinearLayoutManager.VERTICAL,
false
)
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
outRect.bottom = 10.dpToPx().toInt()
}
})
recyclerView.adapter = adapter
}
@SuppressLint("NotifyDataSetChanged")
private fun bindData() {
viewModel.toastLiveData.observe(this) {
it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() }
}
viewModel.isLoading.observe(this) {
if (it) {
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
}
}
viewModel.channelDonationLiveData.observe(this) {
binding.tvTotalCount.text = it.totalCount.toString()
adapter.items.clear()
adapter.items.addAll(it.items)
adapter.notifyDataSetChanged()
binding.tvNoChannelDonation.visibility = if (it.items.isEmpty()) View.VISIBLE else View.GONE
binding.rvChannelDonation.visibility = if (it.items.isEmpty()) View.GONE else View.VISIBLE
}
}
}

View File

@@ -0,0 +1,66 @@
package kr.co.vividnext.sodalive.explorer.profile.channel_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.R
import kr.co.vividnext.sodalive.base.BaseViewModel
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.SodaLiveApplicationHolder
import kr.co.vividnext.sodalive.explorer.ExplorerRepository
class UserProfileChannelDonationAllViewModel(
private val repository: ExplorerRepository
) : BaseViewModel() {
private val _toastLiveData = MutableLiveData<String?>()
val toastLiveData: LiveData<String?>
get() = _toastLiveData
private val _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean>
get() = _isLoading
private val _channelDonationLiveData = MutableLiveData<GetChannelDonationListResponse>()
val channelDonationLiveData: LiveData<GetChannelDonationListResponse>
get() = _channelDonationLiveData
fun getChannelDonationList(creatorId: Long) {
_isLoading.value = true
compositeDisposable.add(
repository.getChannelDonationList(
creatorId = creatorId,
token = "Bearer ${SharedPreferenceManager.token}"
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
_isLoading.value = false
if (it.success && it.data != null) {
_channelDonationLiveData.postValue(it.data)
} else {
if (it.message != null) {
_toastLiveData.postValue(it.message)
} else {
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
}
},
{
_isLoading.value = false
it.message?.let { message -> Logger.e(message) }
_toastLiveData.postValue(
SodaLiveApplicationHolder.get()
.getString(R.string.common_error_unknown)
)
}
)
)
}
}

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.text.InputFilter
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
@@ -26,6 +27,7 @@ class LiveRoomDonationDialog(
private val activity: AppCompatActivity,
layoutInflater: LayoutInflater,
isLiveDonation: Boolean = false,
messageMaxLength: Int = 1000,
onClickDonation: (Int, String, Boolean) -> Unit
) {
@@ -47,11 +49,17 @@ class LiveRoomDonationDialog(
bottomSheetBehavior.skipCollapsed = true
}
dialogView.etDonationMessage.filters = arrayOf(InputFilter.LengthFilter(messageMaxLength))
dialogView.etDonationMessage.hint = activity.getString(
R.string.screen_live_room_donation_message_hint_format,
messageMaxLength
)
dialogView.tvCancel.setOnClickListener { bottomSheetDialog.dismiss() }
dialogView.tvDonation.setOnClickListener {
try {
val can = dialogView.etDonationCan.text.toString().toInt()
val message = dialogView.etDonationMessage.text.toString().prefix(1000)
val message = dialogView.etDonationMessage.text.toString().prefix(messageMaxLength)
if (isLiveDonation) {
val isSecret = dialogView.tvSecret.isSelected
@@ -99,9 +107,15 @@ class LiveRoomDonationDialog(
val isSelected = dialogView.tvSecret.isSelected
dialogView.tvSecret.isSelected = !isSelected
dialogView.etDonationMessage.hint = if (!isSelected) {
activity.getString(R.string.screen_live_room_secret_mission_hint)
activity.getString(
R.string.screen_live_room_secret_mission_hint_format,
messageMaxLength
)
} else {
activity.getString(R.string.screen_live_room_donation_message_hint)
activity.getString(
R.string.screen_live_room_donation_message_hint_format,
messageMaxLength
)
}
dialogView.etDonationCan.hint = if (!isSelected) {
activity.getString(R.string.screen_live_room_secret_mission_input_min)

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_525252" />
<corners android:radius="16dp" />
<stroke
android:width="1dp"
android:color="@color/color_525252" />
</shape>

View File

@@ -277,6 +277,15 @@
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/layout_user_profile_channel_donation"
layout="@layout/layout_user_profile_channel_donation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="48dp"
android:visibility="gone" />
<include
android:id="@+id/layout_user_profile_live"
layout="@layout/layout_user_profile_live"

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
android:id="@+id/toolbar"
layout="@layout/detail_toolbar" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="20dp"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/medium"
android:text="@string/screen_user_profile_donation_total_label"
android:textColor="@color/color_eeeeee"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_total_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6.7dp"
android:fontFamily="@font/medium"
android:textColor="@color/color_80d8ff"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/medium"
android:text="@string/screen_user_profile_donation_total_unit"
android:textColor="@color/color_777777"
android:textSize="14sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="8dp"
android:background="@color/color_595959" />
<TextView
android:id="@+id/tv_no_channel_donation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="32dp"
android:fontFamily="@font/light"
android:gravity="center"
android:text="@string/screen_user_profile_channel_donation_empty"
android:textColor="@color/color_bbbbbb"
android:textSize="14sp"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_channel_donation"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="13.3dp" />
</LinearLayout>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_10_232323"
android:orientation="vertical"
android:padding="14dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_profile"
android:layout_width="36dp"
android:layout_height="36dp"
android:contentDescription="@null" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tv_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/bold"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:fontFamily="@font/medium"
android:textColor="#78909C"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:fontFamily="@font/medium"
android:textColor="@color/color_cfd8dc"
android:textSize="16sp" />
</LinearLayout>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_round_corner_10_232323"
android:orientation="vertical"
android:padding="14dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_profile"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@null" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tv_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/bold"
android:maxLines="1"
android:textColor="@color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:fontFamily="@font/medium"
android:textColor="#78909C"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:fontFamily="@font/medium"
android:textColor="@color/color_cfd8dc"
android:textSize="16sp" />
</LinearLayout>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/bold"
android:text="@string/screen_user_profile_channel_donation_title"
android:textColor="@color/white"
android:textSize="26sp" />
<TextView
android:id="@+id/tv_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:fontFamily="@font/light"
android:gravity="center"
android:text="@string/view_all"
android:textColor="#78909C"
android:textSize="14sp" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_channel_donation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:clipToPadding="false" />
<TextView
android:id="@+id/tv_no_channel_donation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:fontFamily="@font/light"
android:gravity="center"
android:text="@string/screen_user_profile_channel_donation_empty"
android:textColor="@color/color_bbbbbb"
android:textSize="14sp"
android:visibility="gone" />
<LinearLayout
android:id="@+id/ll_channel_donation"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="16dp"
android:background="@drawable/bg_round_corner_16_525252"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:drawablePadding="4dp"
android:fontFamily="@font/bold"
android:gravity="center"
android:text="@string/screen_user_profile_channel_donation_button"
android:textColor="@color/white"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_donation_white" />
</LinearLayout>
</LinearLayout>

View File

@@ -477,6 +477,8 @@
<string name="screen_live_room_listener_label">Listener</string>
<string name="screen_live_room_secret_mission_hint">Enter a secret mission (up to 1000 chars)</string>
<string name="screen_live_room_donation_message_hint">Enter a message to send (up to 1000 chars)</string>
<string name="screen_live_room_secret_mission_hint_format">Enter a secret mission (up to %1$d chars)</string>
<string name="screen_live_room_donation_message_hint_format">Enter a message to send (up to %1$d chars)</string>
<string name="screen_live_room_secret_mission_input_min">Enter 10 or more cans</string>
<string name="screen_live_room_donation_input_min">Enter at least 1 can</string>
<string name="screen_live_room_free">Free</string>
@@ -794,6 +796,10 @@
<string name="screen_user_profile_audio_ratio_detail">%1$s / %2$s items</string>
<string name="screen_user_profile_fan_talk_title">Fan Talk</string>
<string name="screen_user_profile_fantalk_all_title">View all Fan Talk</string>
<string name="screen_user_profile_channel_donation_title">Channel Donation</string>
<string name="screen_user_profile_channel_donation_button">Donate to Channel</string>
<string name="screen_user_profile_channel_donation_all_title">View all Channel Donations</string>
<string name="screen_user_profile_channel_donation_empty">No channel donations yet.</string>
<string name="screen_user_profile_cheer_label">Cheer</string>
<string name="screen_user_profile_cheer_hint">Leave a cheer comment!</string>
<string name="screen_user_profile_cheer_reply_hint">Leave a reply to this cheer!</string>

View File

@@ -476,6 +476,8 @@
<string name="screen_live_room_listener_label">リスナー</string>
<string name="screen_live_room_secret_mission_hint">シークレットミッションを入力最大1000文字</string>
<string name="screen_live_room_donation_message_hint">一緒に送るメッセージを入力最大1000文字</string>
<string name="screen_live_room_secret_mission_hint_format">シークレットミッションを入力(最大%1$d文字</string>
<string name="screen_live_room_donation_message_hint_format">一緒に送るメッセージを入力(最大%1$d文字</string>
<string name="screen_live_room_secret_mission_input_min">10CAN以上入力してください</string>
<string name="screen_live_room_donation_input_min">1CAN以上入力してください</string>
<string name="screen_live_room_free">無料</string>
@@ -794,6 +796,10 @@
<string name="screen_user_profile_audio_ratio_detail">%1$s / %2$s個</string>
<string name="screen_user_profile_fan_talk_title">ファンTalk</string>
<string name="screen_user_profile_fantalk_all_title">ファンTalkをすべて見る</string>
<string name="screen_user_profile_channel_donation_title">チャンネルギフト</string>
<string name="screen_user_profile_channel_donation_button">チャンネルにギフトする</string>
<string name="screen_user_profile_channel_donation_all_title">チャンネルギフトをすべて見る</string>
<string name="screen_user_profile_channel_donation_empty">チャンネルギフトがありません。</string>
<string name="screen_user_profile_cheer_label">応援</string>
<string name="screen_user_profile_cheer_hint">応援コメントを残してみましょう!</string>
<string name="screen_user_profile_cheer_reply_hint">応援コメントに返信してみましょう!</string>

View File

@@ -137,4 +137,5 @@
<color name="color_b0bec5">#B0BEC5</color>
<color name="color_7c7c80">#7C7C80</color>
<color name="color_37474f">#37474F</color>
<color name="color_cfd8dc">#CFD8DC</color>
</resources>

View File

@@ -476,6 +476,8 @@
<string name="screen_live_room_listener_label">리스너</string>
<string name="screen_live_room_secret_mission_hint">비밀 미션을 입력하세요(최대 1000자)</string>
<string name="screen_live_room_donation_message_hint">함께 보낼 메시지 입력(최대 1000자)</string>
<string name="screen_live_room_secret_mission_hint_format">비밀 미션을 입력하세요(최대 %1$d자)</string>
<string name="screen_live_room_donation_message_hint_format">함께 보낼 메시지 입력(최대 %1$d자)</string>
<string name="screen_live_room_secret_mission_input_min">10캔 이상 입력하세요</string>
<string name="screen_live_room_donation_input_min">1캔 이상 입력하세요</string>
<string name="screen_live_room_free">무료</string>
@@ -793,6 +795,10 @@
<string name="screen_user_profile_audio_ratio_detail">%1$s / %2$s개</string>
<string name="screen_user_profile_fan_talk_title">팬 Talk</string>
<string name="screen_user_profile_fantalk_all_title">팬 Talk 전체보기</string>
<string name="screen_user_profile_channel_donation_title">채널 후원</string>
<string name="screen_user_profile_channel_donation_button">채널 후원하기</string>
<string name="screen_user_profile_channel_donation_all_title">채널 후원 전체보기</string>
<string name="screen_user_profile_channel_donation_empty">채널 후원이 없습니다.</string>
<string name="screen_user_profile_cheer_label">응원</string>
<string name="screen_user_profile_cheer_hint">응원댓글을 남겨보세요!</string>
<string name="screen_user_profile_cheer_reply_hint">응원댓글에 답글을 남겨보세요!</string>