Compare commits
83 Commits
926e7eabd3
...
79a6ecdc0b
| Author | SHA1 | Date | |
|---|---|---|---|
| 79a6ecdc0b | |||
| 33f42e62b6 | |||
| 1b660fe542 | |||
| d0539b00b0 | |||
| e089ef6f8a | |||
| a8a70df304 | |||
| a207a8f840 | |||
| 6c85f81f98 | |||
| 6a88019851 | |||
| 379263ec87 | |||
| f3d610633c | |||
| 335db22680 | |||
| b71bb43dd6 | |||
| dace1416c5 | |||
| 1baf3ac7e0 | |||
| f3a4578a66 | |||
| b745c3d675 | |||
| f993d343a9 | |||
| 1ba7846f4c | |||
| da26b65a73 | |||
| 04dcb3f09f | |||
| 6916459c9e | |||
| 643fb9fa8e | |||
| f9160ccb02 | |||
| e2b166de11 | |||
| 89f3511864 | |||
| 3271c9e142 | |||
| 1f1069b05d | |||
| a325a1bbe9 | |||
| 255e6dc67d | |||
| b2685959ad | |||
| 536effab40 | |||
| 81537b1e96 | |||
| e691f78e2e | |||
| 6125b9f7d1 | |||
| d10373d265 | |||
| 5c1eb6204a | |||
| c3884ea917 | |||
| bc3fa9546b | |||
| ea66b6da1e | |||
| c9e47214ea | |||
| c04d603ac0 | |||
| 9e524d626a | |||
| e0dc0c2cb7 | |||
| cbd4f04f1b | |||
| eeb777fa01 | |||
| 9272db913c | |||
| ff4c0104c5 | |||
| 31ce0629d2 | |||
| 6ab7878221 | |||
| ddc18386fb | |||
| 809ed5ce23 | |||
| 8e086c4888 | |||
| bc6443e5bd | |||
| 8d205690b3 | |||
| 25af312d91 | |||
| b020b8096b | |||
| 87f6bec138 | |||
| 34266abd90 | |||
| e8e8a63d6d | |||
| d61867af4e | |||
| daeb0f75a2 | |||
| 6c497ed1d2 | |||
| 7b975ec54d | |||
| e3d81792bf | |||
| 5b9b7eafa7 | |||
| 5489c86cdc | |||
| b1f605fda4 | |||
| f9030ffd13 | |||
| f74f5a534d | |||
| 3f3bacebd9 | |||
| 2041036530 | |||
| 81a2b541a0 | |||
| e6cb94179b | |||
| 658b4f0a9d | |||
| 05cbbdfddc | |||
| 3c66ea986f | |||
| d1a0cf27a7 | |||
| 1aef4b1d89 | |||
| c745481ab2 | |||
| 7e281a15a5 | |||
| c5e94bbc92 | |||
| 61fe8a9fee |
@@ -114,6 +114,18 @@
|
||||
<activity android:name=".v2.main.MainV2Activity" />
|
||||
<activity android:name=".v2.main.content.overview.ContentOverviewActivity" />
|
||||
<activity android:name=".v2.creator.channel.CreatorChannelActivity" />
|
||||
<activity
|
||||
android:name=".v2.creator.channel.community.detail.CreatorChannelCommunityDetailActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity
|
||||
android:name=".v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity
|
||||
android:name=".v2.creator.channel.fantalk.write.CreatorChannelFanTalkWriteActivity"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
<activity
|
||||
android:name=".v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity android:name=".v2.live.onair.HomeOnAirLiveActivity" />
|
||||
<activity
|
||||
android:name=".v2.main.chat.dm.DmChatRoomActivity"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("ktlint:package-name", "ktlint:standard:package-name")
|
||||
|
||||
package kr.co.vividnext.sodalive.audio_content
|
||||
|
||||
import android.app.NotificationChannel
|
||||
@@ -471,9 +473,17 @@ class AudioContentPlayService :
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
val intent = Intent(this, MainV2Activity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
val intent = Intent(this, MainV2Activity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
putExtra(
|
||||
MainV2Activity.EXTRA_AUDIO_NOTIFICATION_ROUTE,
|
||||
MainV2Activity.ROUTE_AUDIO_DETAIL
|
||||
)
|
||||
contentId?.let {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, it)
|
||||
}
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("ktlint:package-name", "ktlint:standard:package-name")
|
||||
|
||||
package kr.co.vividnext.sodalive.audio_content.player
|
||||
|
||||
import android.app.PendingIntent
|
||||
@@ -155,6 +157,10 @@ class AudioContentPlayerService : MediaSessionService() {
|
||||
private fun initMediaSession() {
|
||||
val contextIntent = Intent(applicationContext, MainV2Activity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
putExtra(
|
||||
MainV2Activity.EXTRA_AUDIO_NOTIFICATION_ROUTE,
|
||||
MainV2Activity.ROUTE_AUDIO_PLAYER
|
||||
)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
applicationContext,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.audio_content.playlist.modify
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kr.co.vividnext.sodalive.audio_content.playlist.create.PlaylistContentIdAndOrder
|
||||
|
||||
@Keep
|
||||
data class UpdatePlaylistRequest(
|
||||
@SerializedName("title") val title: String? = null,
|
||||
@SerializedName("desc") val desc: String? = null,
|
||||
|
||||
@@ -5,7 +5,6 @@ import kr.co.vividnext.sodalive.audio_content.series.GetSeriesListResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.series.main.by_genre.GetSeriesGenreListResponse
|
||||
import kr.co.vividnext.sodalive.audio_content.series.main.home.SeriesHomeResponse
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Query
|
||||
@@ -23,7 +22,7 @@ interface SeriesMainApi {
|
||||
|
||||
@GET("/audio-content/series/main/day-of-week")
|
||||
fun getDayOfWeekSeriesList(
|
||||
@Query("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek,
|
||||
@Query("dayOfWeek") dayOfWeek: String,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Header("Authorization") authHeader: String
|
||||
|
||||
@@ -19,7 +19,7 @@ class SeriesMainRepository(
|
||||
size: Int,
|
||||
token: String
|
||||
) = api.getDayOfWeekSeriesList(
|
||||
dayOfWeek = dayOfWeek,
|
||||
dayOfWeek = dayOfWeek.queryValue,
|
||||
page = page - 1,
|
||||
size = size,
|
||||
authHeader = token
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package kr.co.vividnext.sodalive.chat.original
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class OriginalWorkListResponse(
|
||||
val totalCount: Long,
|
||||
val content: List<OriginalWorkListItemResponse>
|
||||
@SerializedName("totalCount") val totalCount: Long,
|
||||
@SerializedName("content") val content: List<OriginalWorkListItemResponse>
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class OriginalWorkListItemResponse(
|
||||
val id: Long,
|
||||
val imageUrl: String?,
|
||||
val title: String,
|
||||
val contentType: String
|
||||
@SerializedName("id") val id: Long,
|
||||
@SerializedName("imageUrl") val imageUrl: String?,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("contentType") val contentType: String
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package kr.co.vividnext.sodalive.common
|
||||
|
||||
import android.content.Context
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import java.text.ParseException
|
||||
import java.text.ParsePosition
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
@@ -10,8 +10,9 @@ import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
fun formatUtcRelativeTimeText(context: Context, utcText: String?): String {
|
||||
val pastMillis = parseServerUtcToMillis(utcText)
|
||||
val parsedTime = parseServerUtcToMillis(utcText)
|
||||
?: return context.getString(R.string.character_comment_time_just_now)
|
||||
val pastMillis = parsedTime.millis
|
||||
|
||||
val nowMillis = System.currentTimeMillis()
|
||||
var diff = nowMillis - pastMillis
|
||||
@@ -21,6 +22,10 @@ fun formatUtcRelativeTimeText(context: Context, utcText: String?): String {
|
||||
val hour = 60 * minute
|
||||
val day = 24 * hour
|
||||
|
||||
if (parsedTime.isIsoText && diff < RECENT_ISO_TIME_GRACE_MILLIS) {
|
||||
return context.getString(R.string.character_comment_time_just_now)
|
||||
}
|
||||
|
||||
if (diff < minute) {
|
||||
return context.getString(R.string.character_comment_time_just_now)
|
||||
}
|
||||
@@ -75,19 +80,25 @@ class AndroidUtcRelativeTimeTextFormatter(context: Context) : UtcRelativeTimeTex
|
||||
override fun format(utcText: String?): String = formatUtcRelativeTimeText(applicationContext, utcText)
|
||||
}
|
||||
|
||||
private fun parseServerUtcToMillis(utcText: String?): Long? {
|
||||
private fun parseServerUtcToMillis(utcText: String?): ParsedServerTime? {
|
||||
if (utcText.isNullOrBlank()) return null
|
||||
|
||||
val value = utcText.trim()
|
||||
val value = utcText.trim().normalizeIsoFraction()
|
||||
if (value.all { it.isDigit() }) {
|
||||
return try {
|
||||
value.toLong()
|
||||
val number = value.toLong()
|
||||
ParsedServerTime(
|
||||
millis = if (value.length <= EPOCH_SECONDS_MAX_LENGTH) number * 1000 else number,
|
||||
isIsoText = false
|
||||
)
|
||||
} catch (_: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val patterns = listOf(
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
|
||||
"yyyy-MM-dd'T'HH:mm:ssXXX",
|
||||
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
|
||||
"yyyy-MM-dd'T'HH:mm:ss'Z'",
|
||||
"yyyy-MM-dd'T'HH:mm:ss",
|
||||
@@ -99,11 +110,26 @@ private fun parseServerUtcToMillis(utcText: String?): Long? {
|
||||
try {
|
||||
val dateFormat = SimpleDateFormat(pattern, Locale.US)
|
||||
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val parsed: Date? = dateFormat.parse(value)
|
||||
if (parsed != null) return parsed.time
|
||||
} catch (_: ParseException) {
|
||||
val position = ParsePosition(0)
|
||||
val parsed: Date? = dateFormat.parse(value, position)
|
||||
if (parsed != null && position.index == value.length) {
|
||||
return ParsedServerTime(millis = parsed.time, isIsoText = true)
|
||||
}
|
||||
} catch (_: RuntimeException) {
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun String.normalizeIsoFraction(): String {
|
||||
return replace(Regex("""\.(\d{3})\d+"""), ".$1")
|
||||
}
|
||||
|
||||
private data class ParsedServerTime(
|
||||
val millis: Long,
|
||||
val isIsoText: Boolean
|
||||
)
|
||||
|
||||
private const val EPOCH_SECONDS_MAX_LENGTH = 10
|
||||
private const val RECENT_ISO_TIME_GRACE_MILLIS = 10 * 60_000L
|
||||
|
||||
@@ -181,10 +181,14 @@ import kr.co.vividnext.sodalive.user.signup.SignUpViewModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioViewModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityViewModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityDetailViewModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyViewModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelApi
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkViewModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailViewModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.write.CreatorChannelFanTalkWriteViewModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesViewModel
|
||||
import kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveViewModel
|
||||
@@ -452,7 +456,11 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
||||
viewModel { CreatorChannelAudioViewModel(get()) }
|
||||
viewModel { CreatorChannelSeriesViewModel(get()) }
|
||||
viewModel { CreatorChannelCommunityViewModel(get(), get()) }
|
||||
viewModel { CreatorChannelCommunityDetailViewModel(get(), get(), get()) }
|
||||
viewModel { CreatorChannelCommunityReplyViewModel(get(), get(), get()) }
|
||||
viewModel { CreatorChannelFanTalkViewModel(get(), get()) }
|
||||
viewModel { CreatorChannelFanTalkDetailViewModel(get(), get()) }
|
||||
viewModel { CreatorChannelFanTalkWriteViewModel(get()) }
|
||||
viewModel { CreatorChannelDonationViewModel(get(), get()) }
|
||||
viewModel { PushNotificationListViewModel(get()) }
|
||||
viewModel { CharacterTabViewModel(get()) }
|
||||
|
||||
@@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.explorer.profile.channel_donation.PostChannelDon
|
||||
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
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
@@ -73,13 +74,13 @@ interface ExplorerApi {
|
||||
fun writeCheers(
|
||||
@Body request: PostWriteCheersRequest,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
): Single<ApiResponse<CreatorChannelFanTalkReplyResponse>>
|
||||
|
||||
@PUT("/explorer/profile/cheers")
|
||||
fun modifyCheers(
|
||||
@Body request: PutModifyCheersRequest,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<Any>>
|
||||
): Single<ApiResponse<CreatorChannelFanTalkReplyResponse>>
|
||||
|
||||
@POST("/explorer/profile/channel-donation")
|
||||
fun postChannelDonation(
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
package kr.co.vividnext.sodalive.home
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* 콘텐츠 랭킹 정렬 기준
|
||||
*/
|
||||
enum class ContentRankingSortType {
|
||||
@Keep
|
||||
enum class ContentRankingSortType(val queryValue: String) {
|
||||
// 매출
|
||||
REVENUE,
|
||||
@SerializedName("REVENUE")
|
||||
REVENUE("REVENUE"),
|
||||
|
||||
// 판매량
|
||||
SALES_COUNT,
|
||||
@SerializedName("SALES_COUNT")
|
||||
SALES_COUNT("SALES_COUNT"),
|
||||
|
||||
// 댓글 수
|
||||
COMMENT_COUNT,
|
||||
@SerializedName("COMMENT_COUNT")
|
||||
COMMENT_COUNT("COMMENT_COUNT"),
|
||||
|
||||
// 좋아요 수
|
||||
LIKE_COUNT
|
||||
@SerializedName("LIKE_COUNT")
|
||||
LIKE_COUNT("LIKE_COUNT")
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ interface HomeApi {
|
||||
|
||||
@GET("/api/home/day-of-week-series")
|
||||
fun getDayOfWeekSeriesList(
|
||||
@Query("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek,
|
||||
@Query("dayOfWeek") dayOfWeek: String,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetSeriesListResponse.SeriesListItem>>>
|
||||
|
||||
@@ -34,7 +34,7 @@ interface HomeApi {
|
||||
|
||||
@GET("/api/home/content-ranking")
|
||||
fun getContentRankingBySort(
|
||||
@Query("sort") sort: ContentRankingSortType,
|
||||
@Query("sort") sort: String,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<List<GetAudioContentRankingItem>>>
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@ class HomeRepository(private val api: HomeApi) {
|
||||
)
|
||||
|
||||
fun getDayOfWeekSeriesList(
|
||||
dayOfWeek: SeriesPublishedDaysOfWeek, token: String
|
||||
dayOfWeek: SeriesPublishedDaysOfWeek,
|
||||
token: String
|
||||
) = api.getDayOfWeekSeriesList(
|
||||
dayOfWeek = dayOfWeek,
|
||||
dayOfWeek = dayOfWeek.queryValue,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
@@ -28,7 +29,7 @@ class HomeRepository(private val api: HomeApi) {
|
||||
sortType: ContentRankingSortType,
|
||||
token: String
|
||||
) = api.getContentRankingBySort(
|
||||
sort = sortType,
|
||||
sort = sortType.queryValue,
|
||||
authHeader = token
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
package kr.co.vividnext.sodalive.home
|
||||
|
||||
enum class SeriesPublishedDaysOfWeek {
|
||||
SUN, MON, TUE, WED, THU, FRI, SAT, RANDOM
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
enum class SeriesPublishedDaysOfWeek(val queryValue: String) {
|
||||
@SerializedName("SUN")
|
||||
SUN("SUN"),
|
||||
|
||||
@SerializedName("MON")
|
||||
MON("MON"),
|
||||
|
||||
@SerializedName("TUE")
|
||||
TUE("TUE"),
|
||||
|
||||
@SerializedName("WED")
|
||||
WED("WED"),
|
||||
|
||||
@SerializedName("THU")
|
||||
THU("THU"),
|
||||
|
||||
@SerializedName("FRI")
|
||||
FRI("FRI"),
|
||||
|
||||
@SerializedName("SAT")
|
||||
SAT("SAT"),
|
||||
|
||||
@SerializedName("RANDOM")
|
||||
RANDOM("RANDOM")
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.mypage.auth
|
||||
|
||||
data class AuthResponse(val gender: Int)
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class AuthResponse(
|
||||
@SerializedName("gender") val gender: Int
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ data class PayverseChargeResponse(
|
||||
@SerializedName("payloadJson") val payloadJson: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class PayverseVerifyRequest(
|
||||
@SerializedName("transactionId") val transactionId: String,
|
||||
@SerializedName("orderId") val orderId: String
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
package kr.co.vividnext.sodalive.v2.common.data
|
||||
|
||||
enum class ContentSort {
|
||||
LATEST,
|
||||
POPULAR,
|
||||
OWNED,
|
||||
PRICE_HIGH,
|
||||
PRICE_LOW
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
enum class ContentSort(val queryValue: String) {
|
||||
@SerializedName("LATEST")
|
||||
LATEST("LATEST"),
|
||||
|
||||
@SerializedName("POPULAR")
|
||||
POPULAR("POPULAR"),
|
||||
|
||||
@SerializedName("OWNED")
|
||||
OWNED("OWNED"),
|
||||
|
||||
@SerializedName("PRICE_HIGH")
|
||||
PRICE_HIGH("PRICE_HIGH"),
|
||||
|
||||
@SerializedName("PRICE_LOW")
|
||||
PRICE_LOW("PRICE_LOW")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package kr.co.vividnext.sodalive.v2.components.modal
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import kr.co.vividnext.sodalive.databinding.DialogV2ModalBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
|
||||
class V2ModalDialog(
|
||||
activity: Activity,
|
||||
layoutInflater: LayoutInflater,
|
||||
title: String,
|
||||
desc: String,
|
||||
confirmButtonTitle: String,
|
||||
confirmButtonClick: () -> Unit,
|
||||
cancelButtonTitle: String = "",
|
||||
cancelButtonClick: (() -> Unit)? = null
|
||||
) {
|
||||
|
||||
private val alertDialog: AlertDialog
|
||||
private var isButtonClicked = false
|
||||
|
||||
val dialogView = DialogV2ModalBinding.inflate(layoutInflater)
|
||||
|
||||
init {
|
||||
val dialogBuilder = AlertDialog.Builder(activity)
|
||||
dialogBuilder.setView(dialogView.root)
|
||||
|
||||
alertDialog = dialogBuilder.create()
|
||||
alertDialog.setCancelable(false)
|
||||
alertDialog.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
|
||||
|
||||
dialogView.tvTitle.text = title
|
||||
dialogView.tvDesc.text = desc
|
||||
|
||||
dialogView.tvCancel.text = cancelButtonTitle
|
||||
dialogView.tvCancel.setOnClickListener {
|
||||
dismissAndRun {
|
||||
cancelButtonClick?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
dialogView.tvConfirm.text = confirmButtonTitle
|
||||
dialogView.tvConfirm.setOnClickListener {
|
||||
dismissAndRun(confirmButtonClick)
|
||||
}
|
||||
|
||||
dialogView.tvCancel.visibility = if (cancelButtonTitle.isNotBlank()) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
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?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
|
||||
alertDialog.window?.attributes = lp
|
||||
}
|
||||
|
||||
private fun dismissAndRun(action: () -> Unit) {
|
||||
if (isButtonClicked) return
|
||||
isButtonClicked = true
|
||||
alertDialog.dismiss()
|
||||
action()
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,12 @@ import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.MeasureSpec
|
||||
import android.view.animation.Interpolator
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
@@ -31,7 +29,6 @@ import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
@@ -53,18 +50,25 @@ import kr.co.vividnext.sodalive.mypage.MyPageViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.auth.Auth
|
||||
import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
|
||||
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
|
||||
import kr.co.vividnext.sodalive.report.ProfileReportDialog
|
||||
import kr.co.vividnext.sodalive.report.UserReportDialog
|
||||
import kr.co.vividnext.sodalive.settings.ContentSettingsActivity
|
||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||
import kr.co.vividnext.sodalive.user.login.LoginActivity
|
||||
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.CreatorChannelAudioFragment
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragment
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityDetailActivity
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragment
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailActivity
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.toFanTalkDetailPayload
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.write.CreatorChannelFanTalkWriteActivity
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragment
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragment
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel
|
||||
@@ -74,8 +78,6 @@ import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTitleBarS
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.series.CreatorChannelSeriesFragment
|
||||
import kr.co.vividnext.sodalive.v2.main.MainV2Activity
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity
|
||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||
import kr.co.vividnext.sodalive.user.login.LoginActivity
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.koin.android.ext.android.inject
|
||||
@@ -103,6 +105,7 @@ class CreatorChannelActivity :
|
||||
private var isOwnerFabExpanded: Boolean = false
|
||||
private var isOwnerFabAnimating: Boolean = false
|
||||
private var isDonationFloatingButtonVisible: Boolean = false
|
||||
private var isFanTalkFixedPlusVisible: Boolean = false
|
||||
private lateinit var loadingDialog: LoadingDialog
|
||||
private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() }
|
||||
private val liveCoordinator: CreatorChannelLiveCoordinator by lazy {
|
||||
@@ -130,6 +133,29 @@ class CreatorChannelActivity :
|
||||
refreshCreatorChannelCommunity()
|
||||
}
|
||||
}
|
||||
private val communityDetailLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
refreshCreatorChannelCommunity()
|
||||
}
|
||||
}
|
||||
private val fanTalkWriteLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
refreshCreatorChannelFanTalk()
|
||||
homeActionDelegate?.refreshHome()
|
||||
}
|
||||
}
|
||||
private val fanTalkDetailLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
refreshCreatorChannelFanTalk()
|
||||
homeActionDelegate?.refreshHome()
|
||||
}
|
||||
}
|
||||
private val liveRoomCreateLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
@@ -192,6 +218,7 @@ class CreatorChannelActivity :
|
||||
binding.btnCreatorChannelDonationWrite.setOnClickListener {
|
||||
findDonationFragment()?.onCreatorChannelDonationFloatingButtonClicked()
|
||||
}
|
||||
binding.btnCreatorChannelFantalkFixedPlus.setOnClickListener { openFanTalkWrite() }
|
||||
binding.btnCreatorChannelOwnerCta.setOnClickListener { onOwnerCtaClicked() }
|
||||
binding.tvChatButton.setOnClickListener {
|
||||
currentHeader?.characterId?.let { characterId -> homeActionDelegate?.createChatRoom(characterId) }
|
||||
@@ -372,14 +399,18 @@ class CreatorChannelActivity :
|
||||
|
||||
private fun showUserBlockDialog() {
|
||||
val nickname = currentHeader?.nickname.orEmpty()
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.screen_live_room_block_title))
|
||||
.setMessage(getString(R.string.screen_live_room_block_message_user, nickname))
|
||||
.setPositiveButton(getString(R.string.screen_live_room_block_confirm)) { _, _ ->
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.screen_live_room_block_title),
|
||||
desc = getString(R.string.screen_live_room_block_message_user, nickname),
|
||||
confirmButtonTitle = getString(R.string.screen_live_room_block_confirm),
|
||||
confirmButtonClick = {
|
||||
homeActionDelegate?.blockUser()
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { _, _ -> }
|
||||
.show()
|
||||
},
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {}
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun showUserReportDialog() {
|
||||
@@ -389,9 +420,18 @@ class CreatorChannelActivity :
|
||||
}
|
||||
|
||||
private fun showProfileReportDialog() {
|
||||
ProfileReportDialog(this, layoutInflater) {
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.dialog_profile_report_title),
|
||||
desc = getString(R.string.dialog_profile_report_notice),
|
||||
confirmButtonTitle = getString(R.string.report_button),
|
||||
confirmButtonClick = {
|
||||
homeActionDelegate?.reportProfile()
|
||||
}.show(resources.displayMetrics.widthPixels)
|
||||
},
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {}
|
||||
).show(resources.displayMetrics.widthPixels)
|
||||
}
|
||||
|
||||
private fun setupTabsAndPager() {
|
||||
@@ -410,6 +450,7 @@ class CreatorChannelActivity :
|
||||
}
|
||||
updateOwnerFabVisibility()
|
||||
updateDonationFloatingButtonVisibility()
|
||||
updateFanTalkFixedPlusVisibility()
|
||||
updateOwnerCtaVisibility()
|
||||
updateCreatorChannelTabViewportHeight()
|
||||
updateViewPagerHeight()
|
||||
@@ -445,6 +486,7 @@ class CreatorChannelActivity :
|
||||
bindTitleBar(header)
|
||||
updateOwnerFabVisibility()
|
||||
updateDonationFloatingButtonVisibility()
|
||||
updateFanTalkFixedPlusVisibility()
|
||||
updateOwnerCtaVisibility()
|
||||
if (binding.viewPager.currentItem == CreatorChannelTab.Audio.ordinal) {
|
||||
binding.viewPager.post {
|
||||
@@ -504,6 +546,11 @@ class CreatorChannelActivity :
|
||||
updateViewPagerHeight()
|
||||
}
|
||||
|
||||
override fun onCreatorChannelHomeTabRequested(tab: CreatorChannelTab) {
|
||||
selectCreatorChannelTab(tab)
|
||||
scrollCreatorChannelToTop()
|
||||
}
|
||||
|
||||
override fun onCreatorChannelLiveContentChanged() {
|
||||
updateCreatorChannelTabViewportHeight()
|
||||
updateViewPagerHeight {
|
||||
@@ -545,12 +592,44 @@ class CreatorChannelActivity :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatorChannelCommunityPostClicked(postId: Long) {
|
||||
if (postId <= 0L) return
|
||||
if (SharedPreferenceManager.token.isBlank()) {
|
||||
showLoginActivity()
|
||||
return
|
||||
}
|
||||
|
||||
communityDetailLauncher.launch(CreatorChannelCommunityDetailActivity.newIntent(this, postId))
|
||||
}
|
||||
|
||||
override fun onCreatorChannelFanTalkContentChanged() {
|
||||
updateViewPagerHeight {
|
||||
postCheckCreatorChannelCurrentTabNeedsMore()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatorChannelFanTalkWriteClicked() {
|
||||
openFanTalkWrite()
|
||||
}
|
||||
|
||||
override fun onCreatorChannelFanTalkItemClicked(item: CreatorChannelFanTalkUiModel) {
|
||||
if (currentHeader?.isOwner != true) return
|
||||
if (creatorId <= 0L || item.fanTalkId <= 0L) return
|
||||
if (SharedPreferenceManager.token.isBlank()) {
|
||||
showLoginActivity()
|
||||
return
|
||||
}
|
||||
|
||||
fanTalkDetailLauncher.launch(
|
||||
CreatorChannelFanTalkDetailActivity.newIntent(this, creatorId, item.toFanTalkDetailPayload())
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreatorChannelFanTalkFloatingButtonVisibilityChanged(isVisible: Boolean) {
|
||||
isFanTalkFixedPlusVisible = isVisible
|
||||
updateFanTalkFixedPlusVisibility()
|
||||
}
|
||||
|
||||
override fun onCreatorChannelDonationContentChanged() {
|
||||
updateViewPagerHeight {
|
||||
postCheckCreatorChannelCurrentTabNeedsMore()
|
||||
@@ -582,7 +661,7 @@ class CreatorChannelActivity :
|
||||
}
|
||||
|
||||
override fun onCreatorChannelFanTalkDeleteClicked(fanTalkId: Long) {
|
||||
SodaDialog(
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.screen_user_profile_cheer_delete_title),
|
||||
@@ -618,7 +697,7 @@ class CreatorChannelActivity :
|
||||
}
|
||||
|
||||
private fun showCreatorChannelCommunityDeleteDialog(item: CreatorChannelCommunityPostUiModel) {
|
||||
SodaDialog(
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.screen_creator_community_delete_title),
|
||||
@@ -822,6 +901,21 @@ class CreatorChannelActivity :
|
||||
binding.btnCreatorChannelDonationWrite.isVisible = shouldShowDonationFloatingButton
|
||||
}
|
||||
|
||||
private fun updateFanTalkFixedPlusVisibility() {
|
||||
val shouldShowFanTalkFixedPlus =
|
||||
isFanTalkFixedPlusVisible && binding.viewPager.currentItem == CreatorChannelTab.FanTalk.ordinal
|
||||
binding.btnCreatorChannelFantalkFixedPlus.isVisible = shouldShowFanTalkFixedPlus
|
||||
}
|
||||
|
||||
private fun openFanTalkWrite() {
|
||||
if (creatorId <= 0L) return
|
||||
if (SharedPreferenceManager.token.isBlank()) {
|
||||
showLoginActivity()
|
||||
return
|
||||
}
|
||||
fanTalkWriteLauncher.launch(CreatorChannelFanTalkWriteActivity.newIntent(this, creatorId))
|
||||
}
|
||||
|
||||
private fun onOwnerFabCommunityClicked() {
|
||||
collapseOwnerFab(animate = false)
|
||||
communityWriteLauncher.launch(Intent(this, CreatorCommunityWriteActivity::class.java))
|
||||
@@ -940,7 +1034,7 @@ class CreatorChannelActivity :
|
||||
if (isAdult) {
|
||||
val isKoreanCountry = SharedPreferenceManager.countryCode.ifBlank { "KR" } == "KR"
|
||||
if (isKoreanCountry && !SharedPreferenceManager.isAuth) {
|
||||
SodaDialog(
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.auth_title),
|
||||
@@ -948,8 +1042,7 @@ class CreatorChannelActivity :
|
||||
confirmButtonTitle = getString(R.string.auth_go),
|
||||
confirmButtonClick = { startAuthFlow() },
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {},
|
||||
descGravity = Gravity.CENTER
|
||||
cancelButtonClick = {}
|
||||
).show(screenWidth)
|
||||
return
|
||||
}
|
||||
@@ -1096,6 +1189,14 @@ class CreatorChannelActivity :
|
||||
)
|
||||
}
|
||||
|
||||
private fun selectCreatorChannelTab(tab: CreatorChannelTab) {
|
||||
binding.viewPager.currentItem = tab.ordinal
|
||||
}
|
||||
|
||||
private fun scrollCreatorChannelToTop() {
|
||||
binding.nestedScrollView.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.btnCreatorChannelOwnerCta.isEnabled = true
|
||||
|
||||
@@ -12,6 +12,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleRe
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelHomeSectionAdapter
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
@@ -25,7 +26,9 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
|
||||
onScheduleClick = ::onScheduleClicked,
|
||||
onAudioContentClick = ::onAudioContentClicked,
|
||||
onSeriesClick = ::onSeriesClicked,
|
||||
onDonationClick = ::onDonationClicked
|
||||
onDonationClick = ::onDonationClicked,
|
||||
onSectionChevronClick = ::onSectionChevronClicked,
|
||||
onCommunityClick = ::onCommunityClicked
|
||||
)
|
||||
private val creatorId: Long by lazy { arguments?.getLong(ARG_CREATOR_ID) ?: 0L }
|
||||
private val host: Host
|
||||
@@ -123,6 +126,14 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
|
||||
host.onCreatorChannelDonationClicked()
|
||||
}
|
||||
|
||||
private fun onSectionChevronClicked(tab: CreatorChannelTab) {
|
||||
host.onCreatorChannelHomeTabRequested(tab)
|
||||
}
|
||||
|
||||
private fun onCommunityClicked(postId: Long) {
|
||||
host.onCreatorChannelCommunityPostClicked(postId)
|
||||
}
|
||||
|
||||
private fun onCurrentLiveClicked(live: CreatorChannelLiveResponse) {
|
||||
host.onCreatorChannelCurrentLiveClicked(live)
|
||||
}
|
||||
@@ -137,6 +148,8 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
|
||||
fun onCreatorChannelHomeActionDelegateReady(delegate: HomeActionDelegate?)
|
||||
fun onCreatorChannelHomeContentChanged()
|
||||
fun onCreatorChannelDonationClicked()
|
||||
fun onCreatorChannelHomeTabRequested(tab: CreatorChannelTab)
|
||||
fun onCreatorChannelCommunityPostClicked(postId: Long)
|
||||
fun onCreatorChannelCurrentLiveClicked(live: CreatorChannelLiveResponse)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||
@@ -27,7 +28,7 @@ import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
class CreatorChannelLiveCoordinator(
|
||||
private val activity: CreatorChannelActivity,
|
||||
private val activity: FragmentActivity,
|
||||
private val layoutInflater: LayoutInflater,
|
||||
private val fragmentManager: FragmentManager,
|
||||
private val liveViewModel: LiveViewModel,
|
||||
@@ -65,11 +66,38 @@ class CreatorChannelLiveCoordinator(
|
||||
}
|
||||
|
||||
liveViewModel.getRoomDetail(roomId) {
|
||||
if (!it.channelName.isNullOrBlank()) {
|
||||
if (it.manager.id == SharedPreferenceManager.userId) {
|
||||
enterLiveRoom(roomId, it, onEnterRoomSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
fun enterLiveRoom(roomId: Long, roomDetail: GetRoomDetailResponse) {
|
||||
activity.startService(
|
||||
Intent(activity, AudioContentPlayService::class.java).apply {
|
||||
action = AudioContentPlayService.MusicAction.STOP.name
|
||||
}
|
||||
)
|
||||
activity.startService(
|
||||
Intent(activity, AudioContentPlayerService::class.java).apply {
|
||||
action = "STOP_SERVICE"
|
||||
}
|
||||
)
|
||||
|
||||
val onEnterRoomSuccess = {
|
||||
activity.runOnUiThread { openLiveRoom(roomId) }
|
||||
}
|
||||
enterLiveRoom(roomId, roomDetail, onEnterRoomSuccess)
|
||||
}
|
||||
|
||||
private fun enterLiveRoom(
|
||||
roomId: Long,
|
||||
roomDetail: GetRoomDetailResponse,
|
||||
onEnterRoomSuccess: () -> Unit
|
||||
) {
|
||||
if (!roomDetail.channelName.isNullOrBlank()) {
|
||||
if (roomDetail.manager.id == SharedPreferenceManager.userId) {
|
||||
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
|
||||
} else if (it.price == 0 || it.isPaid) {
|
||||
if (it.isPrivateRoom) {
|
||||
} else if (roomDetail.price == 0 || roomDetail.isPaid) {
|
||||
if (roomDetail.isPrivateRoom) {
|
||||
LiveRoomPasswordDialog(
|
||||
activity = activity,
|
||||
layoutInflater = layoutInflater,
|
||||
@@ -88,9 +116,9 @@ class CreatorChannelLiveCoordinator(
|
||||
} else {
|
||||
showPaidLiveEntryDialog(
|
||||
roomId = roomId,
|
||||
beginDateTimeUtc = it.beginDateTimeUtc,
|
||||
price = it.price,
|
||||
isPrivateRoom = it.isPrivateRoom,
|
||||
beginDateTimeUtc = roomDetail.beginDateTimeUtc,
|
||||
price = roomDetail.price,
|
||||
isPrivateRoom = roomDetail.isPrivateRoom,
|
||||
onEnterRoomSuccess = onEnterRoomSuccess
|
||||
)
|
||||
}
|
||||
@@ -98,7 +126,6 @@ class CreatorChannelLiveCoordinator(
|
||||
showLiveRoomDetail(roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reservationRoom(roomId: Long) {
|
||||
liveViewModel.getRoomDetail(roomId) {
|
||||
|
||||
@@ -28,11 +28,14 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
|
||||
|
||||
private val viewModel: CreatorChannelCommunityViewModel by viewModel()
|
||||
private val listAdapter = CreatorChannelCommunityListAdapter(
|
||||
onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) },
|
||||
onPlayClick = { item -> toggleCommunityAudio(item) },
|
||||
onOwnerMoreClick = { item -> host.onCreatorChannelCommunityOwnerMoreClicked(item) },
|
||||
isPlayingContent = { postId -> mediaPlayerManager?.isPlayingContent(postId) == true }
|
||||
)
|
||||
private val gridAdapter = CreatorChannelCommunityGridAdapter()
|
||||
private val gridAdapter = CreatorChannelCommunityGridAdapter(
|
||||
onPostClick = { item -> host.onCreatorChannelCommunityPostClicked(item.postId) }
|
||||
)
|
||||
private var mediaPlayerManager: CreatorCommunityMediaPlayerManager? = null
|
||||
private var currentContentState: CreatorChannelCommunityUiState.Content? = null
|
||||
private var lastContentLayoutKey: CreatorChannelCommunityContentLayoutKey? = null
|
||||
@@ -225,6 +228,7 @@ class CreatorChannelCommunityFragment : BaseFragment<FragmentCreatorChannelCommu
|
||||
interface Host {
|
||||
fun isCreatorChannelOwner(): Boolean
|
||||
fun onCreatorChannelCommunityContentChanged()
|
||||
fun onCreatorChannelCommunityPostClicked(postId: Long)
|
||||
fun onCreatorChannelCommunityOwnerMoreClicked(item: CreatorChannelCommunityPostUiModel)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import coil.transform.CircleCropTransformation
|
||||
import com.bumptech.glide.Glide
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelCommunityDetailBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityContentItem
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.CreatorChannelCommunityReplyActivity
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class CreatorChannelCommunityDetailActivity : BaseActivity<ActivityCreatorChannelCommunityDetailBinding>(
|
||||
ActivityCreatorChannelCommunityDetailBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: CreatorChannelCommunityDetailViewModel by viewModel()
|
||||
private val replyLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK && postId > 0L) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
viewModel.loadDetail(postId)
|
||||
}
|
||||
}
|
||||
private val commentAdapter = CreatorChannelCommunityCommentAdapter(
|
||||
onReplyClick = { comment -> replyLauncher.launch(CreatorChannelCommunityReplyActivity.newIntent(this, postId, comment)) },
|
||||
onModifyClick = { comment -> viewModel.startCommentEdit(comment.commentId, comment.comment) },
|
||||
onDeleteClick = { comment -> showDeleteCommentDialog(comment.commentId) }
|
||||
)
|
||||
private lateinit var mediaPlayerManager: CreatorCommunityMediaPlayerManager
|
||||
private var postId: Long = 0L
|
||||
private var renderedEditingCommentId: Long? = null
|
||||
|
||||
override fun setupView() {
|
||||
mediaPlayerManager = CreatorCommunityMediaPlayerManager(this) { }
|
||||
postId = intent.getLongExtra(EXTRA_POST_ID, 0L)
|
||||
if (postId <= 0L) {
|
||||
Toast.makeText(this, R.string.creator_channel_community_invalid_post, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
binding.btnCreatorChannelCommunityDetailBack.setOnClickListener { finish() }
|
||||
binding.rvCreatorChannelCommunityDetailComments.layoutManager = LinearLayoutManager(this)
|
||||
binding.rvCreatorChannelCommunityDetailComments.adapter = commentAdapter
|
||||
binding.scrollCreatorChannelCommunityDetail.setOnScrollChangeListener { scrollView, _, scrollY, _, oldScrollY ->
|
||||
val contentHeight = binding.scrollCreatorChannelCommunityDetail.getChildAt(0)?.measuredHeight ?: 0
|
||||
val isScrollingDown = scrollY > oldScrollY
|
||||
val isAtBottom = scrollY >= contentHeight - scrollView.measuredHeight
|
||||
if (isScrollingDown && isAtBottom) {
|
||||
viewModel.loadMoreComments()
|
||||
}
|
||||
}
|
||||
binding.etCreatorChannelCommunityDetailComment.movementMethod = ScrollingMovementMethod()
|
||||
binding.etCreatorChannelCommunityDetailComment.doAfterTextChanged { editable ->
|
||||
viewModel.updateCommentInput(editable?.toString().orEmpty())
|
||||
}
|
||||
binding.btnCreatorChannelCommunityDetailSend.setOnClickListener { viewModel.submitComment() }
|
||||
binding.btnCreatorChannelCommunityDetailEditCancel.setOnClickListener { viewModel.cancelCommentEdit() }
|
||||
binding.ivCreatorChannelCommunityDetailHeart.setOnClickListener { viewModel.toggleLike() }
|
||||
|
||||
viewModel.detailStateLiveData.observe(this) { state -> render(state) }
|
||||
viewModel.commentWrittenEventLiveData.observe(this) { written ->
|
||||
if (written == true) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
hideCommentKeyboard()
|
||||
viewModel.consumeCommentWrittenEvent()
|
||||
}
|
||||
}
|
||||
viewModel.postChangedEventLiveData.observe(this) { changed ->
|
||||
if (changed == true) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
val current = viewModel.detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
|
||||
if (current?.editingCommentId != null) {
|
||||
hideCommentKeyboard()
|
||||
}
|
||||
viewModel.consumePostChangedEvent()
|
||||
}
|
||||
}
|
||||
viewModel.toastLiveData.observe(this) { event ->
|
||||
event.consume()?.let { toast ->
|
||||
val message = toast.message ?: toast.resId?.let(::getString)
|
||||
message?.let(::showToast)
|
||||
}
|
||||
}
|
||||
viewModel.loadDetail(postId)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mediaPlayerManager.pauseContent()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaPlayerManager.stopContent()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun render(state: CreatorChannelCommunityDetailUiState) {
|
||||
when (state) {
|
||||
CreatorChannelCommunityDetailUiState.Loading -> Unit
|
||||
is CreatorChannelCommunityDetailUiState.Error -> {
|
||||
Toast.makeText(
|
||||
this,
|
||||
state.message ?: getString(R.string.creator_channel_community_error_message),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
finish()
|
||||
}
|
||||
is CreatorChannelCommunityDetailUiState.Content -> renderContent(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderContent(content: CreatorChannelCommunityDetailUiState.Content) = with(binding) {
|
||||
val post = content.post
|
||||
ivCreatorChannelCommunityDetailProfile.loadUrl(post.creatorProfileUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
tvCreatorChannelCommunityDetailNickname.text = post.creatorNickname
|
||||
tvCreatorChannelCommunityDetailTime.text = post.createdAtText
|
||||
tvCreatorChannelCommunityDetailBody.text = post.content
|
||||
tvCreatorChannelCommunityDetailLikeCount.text = post.likeCount.moneyFormat()
|
||||
tvCreatorChannelCommunityDetailCommentCount.text = post.commentCount.moneyFormat()
|
||||
tvCreatorChannelCommunityDetailCommentHeader.text = getString(
|
||||
R.string.creator_channel_community_comment_header,
|
||||
post.commentCount.moneyFormat()
|
||||
)
|
||||
ivCreatorChannelCommunityDetailHeart.setImageResource(
|
||||
if (post.isLiked) R.drawable.ic_feed_community_heart_fill else R.drawable.ic_feed_community_heart
|
||||
)
|
||||
layoutCreatorChannelCommunityDetailPrice.isVisible = post.isLocked
|
||||
tvCreatorChannelCommunityDetailPrice.text = post.price.moneyFormat()
|
||||
tvCreatorChannelCommunityDetailPurchased.isVisible = post.existOrdered && post.price > 0
|
||||
layoutCreatorChannelCommunityDetailImageContainer.isVisible = post.imageUrl != null || post.showPaywall
|
||||
layoutCreatorChannelCommunityDetailPaywall.isVisible = post.showPaywall
|
||||
tvCreatorChannelCommunityDetailPaywallPrice.text = post.price.moneyFormat()
|
||||
if (post.imageUrl != null) {
|
||||
Glide.with(ivCreatorChannelCommunityDetailImage)
|
||||
.load(post.imageUrl)
|
||||
.placeholder(R.drawable.ic_place_holder)
|
||||
.into(ivCreatorChannelCommunityDetailImage)
|
||||
} else {
|
||||
Glide.with(ivCreatorChannelCommunityDetailImage).clear(ivCreatorChannelCommunityDetailImage)
|
||||
}
|
||||
tvCreatorChannelCommunityDetailAudio.isVisible = post.audioUrl != null
|
||||
tvCreatorChannelCommunityDetailAudio.text = getString(R.string.creator_channel_tab_audio)
|
||||
tvCreatorChannelCommunityDetailAudio.setOnClickListener {
|
||||
toggleAudio(post)
|
||||
}
|
||||
rvCreatorChannelCommunityDetailComments.isVisible = content.isCommentAvailable
|
||||
tvCreatorChannelCommunityDetailCommentHeader.isVisible = content.isCommentAvailable
|
||||
layoutCreatorChannelCommunityDetailInputBar.isVisible = content.isCommentInputVisible
|
||||
ivCreatorChannelCommunityDetailComment.isVisible = content.isCommentAvailable
|
||||
tvCreatorChannelCommunityDetailCommentCount.isVisible = content.isCommentAvailable
|
||||
commentAdapter.submitItems(content.comments)
|
||||
if (etCreatorChannelCommunityDetailComment.text.toString() != content.commentInput) {
|
||||
etCreatorChannelCommunityDetailComment.setText(content.commentInput)
|
||||
etCreatorChannelCommunityDetailComment.setSelection(content.commentInput.length)
|
||||
}
|
||||
btnCreatorChannelCommunityDetailEditCancel.isVisible = content.editingCommentId != null
|
||||
if (content.editingCommentId != null && content.editingCommentId != renderedEditingCommentId) {
|
||||
showCommentKeyboard()
|
||||
}
|
||||
renderedEditingCommentId = content.editingCommentId
|
||||
updateSendButton(content.commentInput.trim().isNotEmpty() && !content.isSendingComment)
|
||||
}
|
||||
|
||||
private fun updateSendButton(enabled: Boolean) = with(binding.btnCreatorChannelCommunityDetailSend) {
|
||||
isEnabled = enabled
|
||||
setBackgroundResource(
|
||||
if (enabled) {
|
||||
R.drawable.bg_creator_channel_community_send_enabled
|
||||
} else {
|
||||
R.drawable.bg_creator_channel_community_send_disabled
|
||||
}
|
||||
)
|
||||
setImageResource(if (enabled) R.drawable.ic_new_arrow_up_white else R.drawable.ic_new_arrow_up_gray)
|
||||
}
|
||||
|
||||
private fun toggleAudio(post: CreatorChannelCommunityPostDetailUiModel) {
|
||||
val audioUrl = post.audioUrl ?: return
|
||||
mediaPlayerManager.toggleContent(CreatorCommunityContentItem(post.postId, audioUrl))
|
||||
}
|
||||
|
||||
private fun showDeleteCommentDialog(commentId: Long) {
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.confirm_delete_title),
|
||||
desc = getString(R.string.confirm_delete_message),
|
||||
confirmButtonTitle = getString(R.string.confirm),
|
||||
confirmButtonClick = { viewModel.deleteComment(commentId) },
|
||||
cancelButtonTitle = getString(R.string.cancel)
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun showCommentKeyboard() {
|
||||
val editText = binding.etCreatorChannelCommunityDetailComment
|
||||
editText.requestFocus()
|
||||
editText.post {
|
||||
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideCommentKeyboard() {
|
||||
val editText = binding.etCreatorChannelCommunityDetailComment
|
||||
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(editText.windowToken, 0)
|
||||
editText.clearFocus()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_POST_ID = "extra_post_id"
|
||||
|
||||
fun newIntent(context: Context, postId: Long): Intent {
|
||||
return Intent(context, CreatorChannelCommunityDetailActivity::class.java)
|
||||
.putExtra(EXTRA_POST_ID, postId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentsResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityPostDetailResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityReplyResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
||||
|
||||
class CreatorChannelCommunityDetailViewModel(
|
||||
private val repository: CreatorChannelRepository,
|
||||
private val legacyRepository: CreatorCommunityRepository,
|
||||
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _detailStateLiveData = MutableLiveData<CreatorChannelCommunityDetailUiState>()
|
||||
val detailStateLiveData: LiveData<CreatorChannelCommunityDetailUiState>
|
||||
get() = _detailStateLiveData
|
||||
|
||||
private val _commentWrittenEventLiveData = MutableLiveData<Boolean>()
|
||||
val commentWrittenEventLiveData: LiveData<Boolean>
|
||||
get() = _commentWrittenEventLiveData
|
||||
|
||||
private val _postChangedEventLiveData = MutableLiveData<Boolean>()
|
||||
val postChangedEventLiveData: LiveData<Boolean>
|
||||
get() = _postChangedEventLiveData
|
||||
|
||||
private val _toastLiveData = MutableLiveData<CreatorChannelEvent<ToastMessage>>()
|
||||
val toastLiveData: LiveData<CreatorChannelEvent<ToastMessage>>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var postId: Long = 0L
|
||||
private var detailRequestGeneration = 0
|
||||
private var commentRequestGeneration = 0
|
||||
private var isLoadingComments = false
|
||||
private var isTogglingLike = false
|
||||
private var isSendingComment = false
|
||||
private var isModifyingComment = false
|
||||
|
||||
fun loadDetail(postId: Long) {
|
||||
if (postId <= 0) return
|
||||
this.postId = postId
|
||||
val generation = ++detailRequestGeneration
|
||||
commentRequestGeneration++
|
||||
isLoadingComments = false
|
||||
_detailStateLiveData.value = CreatorChannelCommunityDetailUiState.Loading
|
||||
compositeDisposable.add(
|
||||
repository.getCommunityPostDetail(postId, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
if (generation == detailRequestGeneration) {
|
||||
handleDetailResponse(response)
|
||||
}
|
||||
},
|
||||
{
|
||||
if (generation == detailRequestGeneration) {
|
||||
_detailStateLiveData.value = CreatorChannelCommunityDetailUiState.Error(it.message)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun loadMoreComments() {
|
||||
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||
if (!content.isCommentAvailable || !content.hasNextComment || isLoadingComments) return
|
||||
|
||||
loadComments(page = content.commentPage + 1, append = true)
|
||||
}
|
||||
|
||||
fun toggleLike() {
|
||||
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||
if (isTogglingLike) return
|
||||
|
||||
val previousPost = content.post
|
||||
val nextPost = previousPost.copy(
|
||||
isLiked = !previousPost.isLiked,
|
||||
likeCount = (previousPost.likeCount + if (previousPost.isLiked) -1 else 1).coerceAtLeast(0)
|
||||
)
|
||||
isTogglingLike = true
|
||||
_detailStateLiveData.value = content.copy(post = nextPost)
|
||||
compositeDisposable.add(
|
||||
legacyRepository.communityPostLike(postId, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isTogglingLike = false
|
||||
if (response.success) {
|
||||
_postChangedEventLiveData.value = true
|
||||
} else {
|
||||
val current = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
|
||||
?: return@subscribe
|
||||
_detailStateLiveData.value = current.copy(post = previousPost)
|
||||
}
|
||||
},
|
||||
{
|
||||
isTogglingLike = false
|
||||
val current = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
|
||||
?: return@subscribe
|
||||
_detailStateLiveData.value = current.copy(post = previousPost)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun updateCommentInput(input: String) {
|
||||
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||
if (!content.isCommentAvailable) return
|
||||
|
||||
_detailStateLiveData.value = content.copy(commentInput = input)
|
||||
}
|
||||
|
||||
fun submitComment() {
|
||||
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||
val comment = content.commentInput.trim()
|
||||
val editingCommentId = content.editingCommentId
|
||||
if (editingCommentId != null) {
|
||||
submitCommentEdit(content, editingCommentId, comment)
|
||||
return
|
||||
}
|
||||
if (!content.isCommentAvailable || comment.isBlank() || isSendingComment) return
|
||||
|
||||
isSendingComment = true
|
||||
_detailStateLiveData.value = content.copy(isSendingComment = true)
|
||||
compositeDisposable.add(
|
||||
legacyRepository.registerComment(
|
||||
postId = postId,
|
||||
comment = comment,
|
||||
parentId = null,
|
||||
isSecret = false,
|
||||
token = authToken()
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isSendingComment = false
|
||||
val current = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
|
||||
?: return@subscribe
|
||||
if (response.success) {
|
||||
_commentWrittenEventLiveData.value = true
|
||||
_detailStateLiveData.value = current.copy(commentInput = "", isSendingComment = false)
|
||||
loadDetail(postId)
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
_detailStateLiveData.value = current.copy(isSendingComment = false)
|
||||
}
|
||||
},
|
||||
{
|
||||
isSendingComment = false
|
||||
val current = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
|
||||
?: return@subscribe
|
||||
showMutationFailureToast()
|
||||
_detailStateLiveData.value = current.copy(isSendingComment = false)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun consumeCommentWrittenEvent() {
|
||||
_commentWrittenEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun consumePostChangedEvent() {
|
||||
_postChangedEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun startCommentEdit(commentId: Long, comment: String) {
|
||||
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||
if (commentId <= 0) return
|
||||
|
||||
_detailStateLiveData.value = content.copy(
|
||||
commentInput = comment,
|
||||
editingCommentId = commentId,
|
||||
editingOriginalComment = comment
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelCommentEdit() {
|
||||
val content = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||
_detailStateLiveData.value = content.copy(
|
||||
commentInput = "",
|
||||
editingCommentId = null,
|
||||
editingOriginalComment = ""
|
||||
)
|
||||
}
|
||||
|
||||
fun modifyComment(commentId: Long, comment: String) {
|
||||
val trimmedComment = comment.trim()
|
||||
if (commentId <= 0 || isModifyingComment) return
|
||||
if (trimmedComment.isBlank()) {
|
||||
showBlankCommentToast()
|
||||
return
|
||||
}
|
||||
|
||||
modifyComment(ModifyCommentRequest(commentId = commentId, comment = trimmedComment))
|
||||
}
|
||||
|
||||
fun deleteComment(commentId: Long) {
|
||||
if (commentId <= 0 || isModifyingComment) return
|
||||
|
||||
modifyComment(ModifyCommentRequest(commentId = commentId, isActive = false))
|
||||
}
|
||||
|
||||
private fun submitCommentEdit(
|
||||
content: CreatorChannelCommunityDetailUiState.Content,
|
||||
commentId: Long,
|
||||
comment: String
|
||||
) {
|
||||
if (commentId <= 0 || isModifyingComment) return
|
||||
if (comment.isBlank()) {
|
||||
showBlankCommentToast()
|
||||
return
|
||||
}
|
||||
if (comment == content.editingOriginalComment.trim()) {
|
||||
showNoChangeToast()
|
||||
return
|
||||
}
|
||||
|
||||
isModifyingComment = true
|
||||
_detailStateLiveData.value = content.copy(isSendingComment = true)
|
||||
compositeDisposable.add(
|
||||
legacyRepository.modifyComment(ModifyCommentRequest(commentId = commentId, comment = comment), authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isModifyingComment = false
|
||||
val current = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
|
||||
?: return@subscribe
|
||||
if (response.success) {
|
||||
_postChangedEventLiveData.value = true
|
||||
_detailStateLiveData.value = current.copy(
|
||||
commentInput = "",
|
||||
isSendingComment = false,
|
||||
editingCommentId = null,
|
||||
editingOriginalComment = ""
|
||||
)
|
||||
loadDetail(postId)
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
_detailStateLiveData.value = current.copy(isSendingComment = false)
|
||||
}
|
||||
},
|
||||
{
|
||||
isModifyingComment = false
|
||||
val current = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
|
||||
?: return@subscribe
|
||||
showMutationFailureToast()
|
||||
_detailStateLiveData.value = current.copy(isSendingComment = false)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleDetailResponse(response: ApiResponse<CreatorChannelCommunityPostDetailResponse>) {
|
||||
val data = response.data
|
||||
if (!response.success || data == null) {
|
||||
_detailStateLiveData.value = CreatorChannelCommunityDetailUiState.Error(response.message)
|
||||
return
|
||||
}
|
||||
|
||||
val isCreatorOwner = data.creatorId == SharedPreferenceManager.userId
|
||||
val comments = if (data.isCommentAvailable) {
|
||||
data.comments.comments.map { it.toUiModel(isCreatorOwner) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
_detailStateLiveData.value = CreatorChannelCommunityDetailUiState.Content(
|
||||
post = data.toUiModel(),
|
||||
isCommentAvailable = data.isCommentAvailable,
|
||||
isCommentInputVisible = data.isCommentAvailable,
|
||||
comments = comments,
|
||||
commentPage = data.comments.page,
|
||||
hasNextComment = data.isCommentAvailable && data.comments.hasNext
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadComments(page: Int, append: Boolean) {
|
||||
if (postId <= 0 || isLoadingComments && append) return
|
||||
val generation = if (append) {
|
||||
commentRequestGeneration
|
||||
} else {
|
||||
++commentRequestGeneration
|
||||
}
|
||||
isLoadingComments = true
|
||||
val beforeRequest = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
|
||||
if (beforeRequest != null) {
|
||||
_detailStateLiveData.value = beforeRequest.copy(isLoadingComments = true)
|
||||
}
|
||||
compositeDisposable.add(
|
||||
repository.getCommunityPostComments(postId, page, DEFAULT_PAGE_SIZE, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response -> handleCommentsResponse(response, append, generation) },
|
||||
{
|
||||
if (generation != commentRequestGeneration) return@subscribe
|
||||
|
||||
isLoadingComments = false
|
||||
val current = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content
|
||||
?: return@subscribe
|
||||
_detailStateLiveData.value = current.copy(isLoadingComments = false)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleCommentsResponse(
|
||||
response: ApiResponse<CreatorChannelCommunityCommentsResponse>,
|
||||
append: Boolean,
|
||||
generation: Int
|
||||
) {
|
||||
if (generation != commentRequestGeneration) return
|
||||
|
||||
isLoadingComments = false
|
||||
val current = _detailStateLiveData.value as? CreatorChannelCommunityDetailUiState.Content ?: return
|
||||
val data = response.data
|
||||
if (!response.success || data == null) {
|
||||
_detailStateLiveData.value = current.copy(isLoadingComments = false)
|
||||
return
|
||||
}
|
||||
|
||||
val isCreatorOwner = current.post.creatorId == SharedPreferenceManager.userId
|
||||
val nextComments = data.comments.map { it.toUiModel(isCreatorOwner) }
|
||||
_detailStateLiveData.value = current.copy(
|
||||
post = current.post.copy(commentCount = data.commentCount),
|
||||
comments = if (append) current.comments + nextComments else nextComments,
|
||||
commentPage = data.page,
|
||||
hasNextComment = data.hasNext,
|
||||
isLoadingComments = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun modifyComment(request: ModifyCommentRequest) {
|
||||
if (postId <= 0) return
|
||||
|
||||
isModifyingComment = true
|
||||
compositeDisposable.add(
|
||||
legacyRepository.modifyComment(request, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isModifyingComment = false
|
||||
if (response.success) {
|
||||
_postChangedEventLiveData.value = true
|
||||
loadDetail(postId)
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isModifyingComment = false
|
||||
showMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showMutationFailureToast(message: String? = null) {
|
||||
_toastLiveData.value = CreatorChannelEvent(
|
||||
if (message.isNullOrBlank()) {
|
||||
ToastMessage(resId = R.string.common_error_unknown)
|
||||
} else {
|
||||
ToastMessage(message = message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showBlankCommentToast() {
|
||||
_toastLiveData.value = CreatorChannelEvent(
|
||||
ToastMessage(resId = R.string.screen_creator_community_write_content_hint)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNoChangeToast() {
|
||||
_toastLiveData.value = CreatorChannelEvent(
|
||||
ToastMessage(resId = R.string.audio_content_comment_no_change)
|
||||
)
|
||||
}
|
||||
|
||||
private fun CreatorChannelCommunityPostDetailResponse.toUiModel() = CreatorChannelCommunityPostDetailUiModel(
|
||||
postId = postId,
|
||||
creatorId = creatorId,
|
||||
creatorNickname = creatorNickname,
|
||||
creatorProfileUrl = creatorProfileUrl,
|
||||
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
|
||||
content = content,
|
||||
imageUrl = imageUrl.takeUnless { isLocked() },
|
||||
audioUrl = audioUrl.takeUnless { isLocked() },
|
||||
price = price,
|
||||
existOrdered = existOrdered,
|
||||
likeCount = likeCount,
|
||||
commentCount = commentCount,
|
||||
isLiked = isLiked,
|
||||
isPinned = isPinned,
|
||||
isLocked = isLocked(),
|
||||
showPaywall = isLocked()
|
||||
)
|
||||
|
||||
private fun CreatorChannelCommunityPostDetailResponse.isLocked(): Boolean {
|
||||
return price > 0 && !existOrdered && creatorId != SharedPreferenceManager.userId
|
||||
}
|
||||
|
||||
private fun CreatorChannelCommunityCommentResponse.toUiModel(
|
||||
isCreatorOwner: Boolean
|
||||
) = CreatorChannelCommunityCommentUiModel(
|
||||
commentId = commentId,
|
||||
memberId = writerId,
|
||||
memberNickname = writerNickname,
|
||||
memberProfileUrl = writerProfileImageUrl,
|
||||
createdAtUtc = createdAtUtc,
|
||||
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
|
||||
comment = content,
|
||||
likeCount = 0,
|
||||
replyCount = if (latestReply != null) 1 else 0,
|
||||
isLiked = false,
|
||||
isMine = writerId == SharedPreferenceManager.userId,
|
||||
isCreatorOwner = isCreatorOwner,
|
||||
latestReply = latestReply?.toUiModel(parentCommentId = commentId, isCreatorOwner = isCreatorOwner)
|
||||
)
|
||||
|
||||
private fun CreatorChannelCommunityReplyResponse.toUiModel(
|
||||
parentCommentId: Long,
|
||||
isCreatorOwner: Boolean
|
||||
) = CreatorChannelCommunityReplyUiModel(
|
||||
replyId = commentId,
|
||||
commentId = parentCommentId,
|
||||
memberId = writerId,
|
||||
memberNickname = writerNickname,
|
||||
memberProfileUrl = writerProfileImageUrl,
|
||||
createdAtUtc = createdAtUtc,
|
||||
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
|
||||
comment = content,
|
||||
likeCount = 0,
|
||||
isLiked = false,
|
||||
isMine = writerId == SharedPreferenceManager.userId,
|
||||
isCreatorOwner = isCreatorOwner
|
||||
)
|
||||
|
||||
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_PAGE_SIZE = 20
|
||||
private const val FIRST_PAGE = 0
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface CreatorChannelCommunityDetailUiState {
|
||||
data object Loading : CreatorChannelCommunityDetailUiState
|
||||
data class Error(val message: String?) : CreatorChannelCommunityDetailUiState
|
||||
data class Content(
|
||||
val post: CreatorChannelCommunityPostDetailUiModel,
|
||||
val isCommentAvailable: Boolean,
|
||||
val isCommentInputVisible: Boolean,
|
||||
val comments: List<CreatorChannelCommunityCommentUiModel>,
|
||||
val commentPage: Int,
|
||||
val hasNextComment: Boolean,
|
||||
val isLoadingComments: Boolean = false,
|
||||
val commentInput: String = "",
|
||||
val isSendingComment: Boolean = false,
|
||||
val editingCommentId: Long? = null,
|
||||
val editingOriginalComment: String = ""
|
||||
) : CreatorChannelCommunityDetailUiState
|
||||
}
|
||||
|
||||
data class CreatorChannelCommunityPostDetailUiModel(
|
||||
val postId: Long,
|
||||
val creatorId: Long,
|
||||
val creatorNickname: String,
|
||||
val creatorProfileUrl: String?,
|
||||
val createdAtText: String,
|
||||
val content: String,
|
||||
val imageUrl: String?,
|
||||
val audioUrl: String?,
|
||||
val price: Int,
|
||||
val existOrdered: Boolean,
|
||||
val likeCount: Int,
|
||||
val commentCount: Int,
|
||||
val isLiked: Boolean,
|
||||
val isPinned: Boolean,
|
||||
val isLocked: Boolean,
|
||||
val showPaywall: Boolean
|
||||
)
|
||||
|
||||
data class CreatorChannelCommunityCommentUiModel(
|
||||
val commentId: Long,
|
||||
val memberId: Long,
|
||||
val memberNickname: String,
|
||||
val memberProfileUrl: String?,
|
||||
val createdAtUtc: String,
|
||||
val createdAtText: String,
|
||||
val comment: String,
|
||||
val likeCount: Int,
|
||||
val replyCount: Int,
|
||||
val isLiked: Boolean,
|
||||
val isMine: Boolean,
|
||||
val isCreatorOwner: Boolean,
|
||||
val latestReply: CreatorChannelCommunityReplyUiModel?
|
||||
)
|
||||
|
||||
data class CreatorChannelCommunityReplyUiModel(
|
||||
val replyId: Long,
|
||||
val commentId: Long,
|
||||
val memberId: Long,
|
||||
val memberNickname: String,
|
||||
val memberProfileUrl: String?,
|
||||
val createdAtUtc: String,
|
||||
val createdAtText: String,
|
||||
val comment: String,
|
||||
val likeCount: Int,
|
||||
val isLiked: Boolean,
|
||||
val isMine: Boolean,
|
||||
val isCreatorOwner: Boolean
|
||||
)
|
||||
@@ -0,0 +1,63 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
data class CreatorChannelCommunityPostDetailResponse(
|
||||
@SerializedName("postId") val postId: Long,
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("creatorProfileUrl") val creatorProfileUrl: String?,
|
||||
@SerializedName("createdAtUtc") val createdAtUtc: String,
|
||||
@SerializedName("content") val content: String,
|
||||
@SerializedName("imageUrl") val imageUrl: String?,
|
||||
@SerializedName("audioUrl") val audioUrl: String?,
|
||||
@SerializedName("price") val price: Int,
|
||||
@SerializedName("existOrdered") val existOrdered: Boolean,
|
||||
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean,
|
||||
@SerializedName("likeCount") val likeCount: Int,
|
||||
@SerializedName("commentCount") val commentCount: Int,
|
||||
@SerializedName("isLiked") val isLiked: Boolean,
|
||||
@SerializedName("isPinned") val isPinned: Boolean,
|
||||
@SerializedName("comments") val comments: CreatorChannelCommunityCommentsResponse
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class CreatorChannelCommunityCommentsResponse(
|
||||
@SerializedName("commentCount") val commentCount: Int,
|
||||
@SerializedName("comments") val comments: List<CreatorChannelCommunityCommentResponse>,
|
||||
@SerializedName("page") val page: Int,
|
||||
@SerializedName("size") val size: Int,
|
||||
@SerializedName("hasNext") val hasNext: Boolean
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class CreatorChannelCommunityCommentResponse(
|
||||
@SerializedName("commentId") val commentId: Long,
|
||||
@SerializedName("writerId") val writerId: Long,
|
||||
@SerializedName("writerProfileImageUrl") val writerProfileImageUrl: String,
|
||||
@SerializedName("writerNickname") val writerNickname: String,
|
||||
@SerializedName("content") val content: String,
|
||||
@SerializedName("isSecret") val isSecret: Boolean,
|
||||
@SerializedName("createdAtUtc") val createdAtUtc: String,
|
||||
@SerializedName("latestReply") val latestReply: CreatorChannelCommunityReplyResponse?
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class CreatorChannelCommunityReplyResponse(
|
||||
@SerializedName("commentId") val commentId: Long,
|
||||
@SerializedName("writerId") val writerId: Long,
|
||||
@SerializedName("writerProfileImageUrl") val writerProfileImageUrl: String,
|
||||
@SerializedName("writerNickname") val writerNickname: String,
|
||||
@SerializedName("content") val content: String,
|
||||
@SerializedName("createdAtUtc") val createdAtUtc: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class CreatorChannelCommunityRepliesResponse(
|
||||
@SerializedName("replies") val replies: List<CreatorChannelCommunityReplyResponse>,
|
||||
@SerializedName("page") val page: Int,
|
||||
@SerializedName("size") val size: Int,
|
||||
@SerializedName("hasNext") val hasNext: Boolean
|
||||
)
|
||||
@@ -0,0 +1,229 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
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.databinding.ActivityCreatorChannelCommunityReplyBinding
|
||||
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.ui.CreatorChannelCommunityReplyAdapter
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityCommentAdapter
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class CreatorChannelCommunityReplyActivity : BaseActivity<ActivityCreatorChannelCommunityReplyBinding>(
|
||||
ActivityCreatorChannelCommunityReplyBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: CreatorChannelCommunityReplyViewModel by viewModel()
|
||||
private val replyAdapter = CreatorChannelCommunityReplyAdapter(
|
||||
onModifyClick = { reply -> viewModel.startReplyEdit(reply.replyId, reply.comment) },
|
||||
onDeleteClick = { reply -> showDeleteReplyDialog(reply.replyId) }
|
||||
)
|
||||
private var postId: Long = 0L
|
||||
private var renderedEditingTarget: CreatorChannelCommunityReplyEditTarget? = null
|
||||
|
||||
override fun setupView() {
|
||||
postId = intent.getLongExtra(EXTRA_POST_ID, 0L)
|
||||
val parentComment = parentCommentFromIntent()
|
||||
if (postId <= 0L || parentComment.commentId <= 0L) {
|
||||
Toast.makeText(this, R.string.creator_channel_community_invalid_post, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
binding.btnCreatorChannelCommunityReplyBack.setOnClickListener { finish() }
|
||||
binding.rvCreatorChannelCommunityReply.layoutManager = LinearLayoutManager(this)
|
||||
binding.rvCreatorChannelCommunityReply.adapter = replyAdapter
|
||||
binding.rvCreatorChannelCommunityReply.addOnScrollListener(
|
||||
object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
if (dy > 0 && !recyclerView.canScrollVertically(1)) {
|
||||
viewModel.loadMoreReplies()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
binding.etCreatorChannelCommunityReply.movementMethod = ScrollingMovementMethod()
|
||||
binding.etCreatorChannelCommunityReply.doAfterTextChanged { editable ->
|
||||
viewModel.updateReplyInput(editable?.toString().orEmpty())
|
||||
}
|
||||
binding.btnCreatorChannelCommunityReplySend.setOnClickListener { viewModel.submitReply() }
|
||||
binding.btnCreatorChannelCommunityReplyEditCancel.setOnClickListener { viewModel.cancelReplyEdit() }
|
||||
|
||||
viewModel.replyStateLiveData.observe(this) { state -> render(state) }
|
||||
viewModel.replyWrittenEventLiveData.observe(this) { written ->
|
||||
if (written == true) {
|
||||
hideReplyKeyboard()
|
||||
viewModel.consumeReplyWrittenEvent()
|
||||
}
|
||||
}
|
||||
viewModel.replyChangedEventLiveData.observe(this) { changed ->
|
||||
if (changed == true) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
hideReplyKeyboard()
|
||||
viewModel.consumeReplyChangedEvent()
|
||||
}
|
||||
}
|
||||
viewModel.parentCommentChangedEventLiveData.observe(this) { changed ->
|
||||
if (changed == true) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
hideReplyKeyboard()
|
||||
viewModel.consumeParentCommentChangedEvent()
|
||||
}
|
||||
}
|
||||
viewModel.parentCommentDeletedEventLiveData.observe(this) { deleted ->
|
||||
if (deleted == true) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
viewModel.consumeParentCommentDeletedEvent()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
viewModel.toastLiveData.observe(this) { event ->
|
||||
event.consume()?.let { toast ->
|
||||
val message = toast.message ?: toast.resId?.let(::getString)
|
||||
message?.let(::showToast)
|
||||
}
|
||||
}
|
||||
viewModel.loadReplies(postId, parentComment)
|
||||
}
|
||||
|
||||
private fun render(state: CreatorChannelCommunityReplyUiState) {
|
||||
when (state) {
|
||||
is CreatorChannelCommunityReplyUiState.Content -> renderContent(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderContent(content: CreatorChannelCommunityReplyUiState.Content) = with(binding) {
|
||||
CreatorChannelCommunityCommentAdapter.bindComment(
|
||||
layoutCreatorChannelCommunityReplyParent,
|
||||
content.parentComment,
|
||||
onReplyClick = {},
|
||||
onModifyClick = { comment -> viewModel.startParentCommentEdit(comment.comment) },
|
||||
onDeleteClick = { showDeleteParentCommentDialog() }
|
||||
)
|
||||
layoutCreatorChannelCommunityReplyParent.layoutCreatorChannelCommunityCommentLatestReply.isVisible = false
|
||||
replyAdapter.submitItems(content.replies)
|
||||
if (etCreatorChannelCommunityReply.text.toString() != content.replyInput) {
|
||||
etCreatorChannelCommunityReply.setText(content.replyInput)
|
||||
etCreatorChannelCommunityReply.setSelection(content.replyInput.length)
|
||||
}
|
||||
btnCreatorChannelCommunityReplyEditCancel.isVisible = content.editingTarget != null
|
||||
if (content.editingTarget != null && content.editingTarget != renderedEditingTarget) {
|
||||
showReplyKeyboard()
|
||||
}
|
||||
renderedEditingTarget = content.editingTarget
|
||||
updateSendButton(content.replyInput.trim().isNotEmpty() && !content.isSendingReply)
|
||||
}
|
||||
|
||||
private fun showDeleteParentCommentDialog() {
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.confirm_delete_title),
|
||||
desc = getString(R.string.confirm_delete_message),
|
||||
confirmButtonTitle = getString(R.string.confirm),
|
||||
confirmButtonClick = { viewModel.deleteParentComment() },
|
||||
cancelButtonTitle = getString(R.string.cancel)
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun showDeleteReplyDialog(replyId: Long) {
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.confirm_delete_title),
|
||||
desc = getString(R.string.confirm_delete_message),
|
||||
confirmButtonTitle = getString(R.string.confirm),
|
||||
confirmButtonClick = { viewModel.deleteReply(replyId) },
|
||||
cancelButtonTitle = getString(R.string.cancel)
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun updateSendButton(enabled: Boolean) = with(binding.btnCreatorChannelCommunityReplySend) {
|
||||
isEnabled = enabled
|
||||
setBackgroundResource(
|
||||
if (enabled) {
|
||||
R.drawable.bg_creator_channel_community_send_enabled
|
||||
} else {
|
||||
R.drawable.bg_creator_channel_community_send_disabled
|
||||
}
|
||||
)
|
||||
setImageResource(if (enabled) R.drawable.ic_new_arrow_up_white else R.drawable.ic_new_arrow_up_gray)
|
||||
}
|
||||
|
||||
private fun showReplyKeyboard() {
|
||||
val editText = binding.etCreatorChannelCommunityReply
|
||||
editText.requestFocus()
|
||||
editText.post {
|
||||
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideReplyKeyboard() {
|
||||
val editText = binding.etCreatorChannelCommunityReply
|
||||
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(editText.windowToken, 0)
|
||||
editText.clearFocus()
|
||||
}
|
||||
|
||||
private fun parentCommentFromIntent(): CreatorChannelCommunityCommentUiModel {
|
||||
return CreatorChannelCommunityCommentUiModel(
|
||||
commentId = intent.getLongExtra(EXTRA_COMMENT_ID, 0L),
|
||||
memberId = intent.getLongExtra(EXTRA_MEMBER_ID, 0L),
|
||||
memberNickname = intent.getStringExtra(EXTRA_MEMBER_NICKNAME).orEmpty(),
|
||||
memberProfileUrl = intent.getStringExtra(EXTRA_MEMBER_PROFILE_URL),
|
||||
createdAtUtc = intent.getStringExtra(EXTRA_CREATED_AT_UTC).orEmpty(),
|
||||
createdAtText = intent.getStringExtra(EXTRA_CREATED_AT_TEXT).orEmpty(),
|
||||
comment = intent.getStringExtra(EXTRA_COMMENT).orEmpty(),
|
||||
likeCount = intent.getIntExtra(EXTRA_LIKE_COUNT, 0),
|
||||
replyCount = intent.getIntExtra(EXTRA_REPLY_COUNT, 0),
|
||||
isLiked = intent.getBooleanExtra(EXTRA_IS_LIKED, false),
|
||||
isMine = intent.getBooleanExtra(EXTRA_IS_MINE, false),
|
||||
isCreatorOwner = intent.getBooleanExtra(EXTRA_IS_CREATOR_OWNER, false),
|
||||
latestReply = null
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_POST_ID = "extra_post_id"
|
||||
private const val EXTRA_COMMENT_ID = "extra_comment_id"
|
||||
private const val EXTRA_MEMBER_ID = "extra_member_id"
|
||||
private const val EXTRA_MEMBER_NICKNAME = "extra_member_nickname"
|
||||
private const val EXTRA_MEMBER_PROFILE_URL = "extra_member_profile_url"
|
||||
private const val EXTRA_CREATED_AT_UTC = "extra_created_at_utc"
|
||||
private const val EXTRA_CREATED_AT_TEXT = "extra_created_at_text"
|
||||
private const val EXTRA_COMMENT = "extra_comment"
|
||||
private const val EXTRA_LIKE_COUNT = "extra_like_count"
|
||||
private const val EXTRA_REPLY_COUNT = "extra_reply_count"
|
||||
private const val EXTRA_IS_LIKED = "extra_is_liked"
|
||||
private const val EXTRA_IS_MINE = "extra_is_mine"
|
||||
private const val EXTRA_IS_CREATOR_OWNER = "extra_is_creator_owner"
|
||||
|
||||
fun newIntent(context: Context, postId: Long, parentComment: CreatorChannelCommunityCommentUiModel): Intent {
|
||||
return Intent(context, CreatorChannelCommunityReplyActivity::class.java)
|
||||
.putExtra(EXTRA_POST_ID, postId)
|
||||
.putExtra(EXTRA_COMMENT_ID, parentComment.commentId)
|
||||
.putExtra(EXTRA_MEMBER_ID, parentComment.memberId)
|
||||
.putExtra(EXTRA_MEMBER_NICKNAME, parentComment.memberNickname)
|
||||
.putExtra(EXTRA_MEMBER_PROFILE_URL, parentComment.memberProfileUrl)
|
||||
.putExtra(EXTRA_CREATED_AT_UTC, parentComment.createdAtUtc)
|
||||
.putExtra(EXTRA_CREATED_AT_TEXT, parentComment.createdAtText)
|
||||
.putExtra(EXTRA_COMMENT, parentComment.comment)
|
||||
.putExtra(EXTRA_LIKE_COUNT, parentComment.likeCount)
|
||||
.putExtra(EXTRA_REPLY_COUNT, parentComment.replyCount)
|
||||
.putExtra(EXTRA_IS_LIKED, parentComment.isLiked)
|
||||
.putExtra(EXTRA_IS_MINE, parentComment.isMine)
|
||||
.putExtra(EXTRA_IS_CREATOR_OWNER, parentComment.isCreatorOwner)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.comment.ModifyCommentRequest
|
||||
import kr.co.vividnext.sodalive.base.BaseViewModel
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityReplyUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityRepliesResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityReplyResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
||||
|
||||
class CreatorChannelCommunityReplyViewModel(
|
||||
private val repository: CreatorChannelRepository,
|
||||
private val legacyRepository: CreatorCommunityRepository,
|
||||
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _replyStateLiveData = MutableLiveData<CreatorChannelCommunityReplyUiState>()
|
||||
val replyStateLiveData: LiveData<CreatorChannelCommunityReplyUiState>
|
||||
get() = _replyStateLiveData
|
||||
|
||||
private val _replyWrittenEventLiveData = MutableLiveData<Boolean>()
|
||||
val replyWrittenEventLiveData: LiveData<Boolean>
|
||||
get() = _replyWrittenEventLiveData
|
||||
|
||||
private val _replyChangedEventLiveData = MutableLiveData<Boolean>()
|
||||
val replyChangedEventLiveData: LiveData<Boolean>
|
||||
get() = _replyChangedEventLiveData
|
||||
|
||||
private val _parentCommentChangedEventLiveData = MutableLiveData<Boolean>()
|
||||
val parentCommentChangedEventLiveData: LiveData<Boolean>
|
||||
get() = _parentCommentChangedEventLiveData
|
||||
|
||||
private val _parentCommentDeletedEventLiveData = MutableLiveData<Boolean>()
|
||||
val parentCommentDeletedEventLiveData: LiveData<Boolean>
|
||||
get() = _parentCommentDeletedEventLiveData
|
||||
|
||||
private val _toastLiveData = MutableLiveData<CreatorChannelEvent<ToastMessage>>()
|
||||
val toastLiveData: LiveData<CreatorChannelEvent<ToastMessage>>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var postId: Long = 0L
|
||||
private var parentComment: CreatorChannelCommunityCommentUiModel? = null
|
||||
private var replyRequestGeneration = 0
|
||||
private var isLoadingReplies = false
|
||||
private var isSendingReply = false
|
||||
private var isModifyingReply = false
|
||||
|
||||
fun loadReplies(postId: Long, parentComment: CreatorChannelCommunityCommentUiModel) {
|
||||
val formattedParentComment = parentComment.copy(
|
||||
createdAtText = relativeTimeTextFormatter.format(parentComment.createdAtUtc)
|
||||
)
|
||||
this.postId = postId
|
||||
this.parentComment = formattedParentComment
|
||||
replyRequestGeneration++
|
||||
isLoadingReplies = false
|
||||
_replyStateLiveData.value = CreatorChannelCommunityReplyUiState.Content(
|
||||
parentComment = formattedParentComment,
|
||||
replies = emptyList(),
|
||||
replyPage = FIRST_PAGE,
|
||||
hasNextReply = false
|
||||
)
|
||||
if (formattedParentComment.commentId > 0) {
|
||||
loadReplyPage(page = FIRST_PAGE, append = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMoreReplies() {
|
||||
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
if (!content.hasNextReply || isLoadingReplies) return
|
||||
|
||||
loadReplyPage(page = content.replyPage + 1, append = true)
|
||||
}
|
||||
|
||||
fun updateReplyInput(input: String) {
|
||||
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
_replyStateLiveData.value = content.copy(replyInput = input)
|
||||
}
|
||||
|
||||
fun submitReply() {
|
||||
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
val comment = content.replyInput.trim()
|
||||
val editingTarget = content.editingTarget
|
||||
if (editingTarget != null) {
|
||||
submitReplyEdit(content, editingTarget, comment)
|
||||
return
|
||||
}
|
||||
val commentId = content.parentComment.commentId
|
||||
if (postId <= 0 || commentId <= 0 || comment.isBlank() || isSendingReply) return
|
||||
|
||||
isSendingReply = true
|
||||
_replyStateLiveData.value = content.copy(isSendingReply = true)
|
||||
compositeDisposable.add(
|
||||
legacyRepository.registerComment(
|
||||
postId = postId,
|
||||
comment = comment,
|
||||
parentId = commentId,
|
||||
isSecret = false,
|
||||
token = authToken()
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response -> handleWriteResponse(response) },
|
||||
{
|
||||
isSendingReply = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
?: return@subscribe
|
||||
showMutationFailureToast()
|
||||
_replyStateLiveData.value = current.copy(isSendingReply = false)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun consumeReplyWrittenEvent() {
|
||||
_replyWrittenEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun consumeReplyChangedEvent() {
|
||||
_replyChangedEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun consumeParentCommentChangedEvent() {
|
||||
_parentCommentChangedEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun consumeParentCommentDeletedEvent() {
|
||||
_parentCommentDeletedEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun startReplyEdit(replyId: Long, comment: String) {
|
||||
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
if (replyId <= 0) return
|
||||
|
||||
_replyStateLiveData.value = content.copy(
|
||||
replyInput = comment,
|
||||
editingTarget = CreatorChannelCommunityReplyEditTarget.Reply(replyId, comment)
|
||||
)
|
||||
}
|
||||
|
||||
fun startParentCommentEdit(comment: String) {
|
||||
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
if (content.parentComment.commentId <= 0) return
|
||||
|
||||
_replyStateLiveData.value = content.copy(
|
||||
replyInput = comment,
|
||||
editingTarget = CreatorChannelCommunityReplyEditTarget.ParentComment(comment)
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelReplyEdit() {
|
||||
val content = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
_replyStateLiveData.value = content.copy(replyInput = "", editingTarget = null)
|
||||
}
|
||||
|
||||
fun modifyReply(replyId: Long, comment: String) {
|
||||
val trimmedComment = comment.trim()
|
||||
if (replyId <= 0 || isModifyingReply) return
|
||||
if (trimmedComment.isBlank()) {
|
||||
showBlankCommentToast()
|
||||
return
|
||||
}
|
||||
|
||||
modifyComment(ModifyCommentRequest(commentId = replyId, comment = trimmedComment))
|
||||
}
|
||||
|
||||
fun deleteReply(replyId: Long) {
|
||||
if (replyId <= 0 || isModifyingReply) return
|
||||
|
||||
modifyComment(ModifyCommentRequest(commentId = replyId, isActive = false))
|
||||
}
|
||||
|
||||
fun modifyParentComment(comment: String) {
|
||||
val parentComment = parentComment ?: return
|
||||
val trimmedComment = comment.trim()
|
||||
if (parentComment.commentId <= 0 || isModifyingReply) return
|
||||
if (trimmedComment.isBlank()) {
|
||||
showBlankCommentToast()
|
||||
return
|
||||
}
|
||||
|
||||
modifyParentComment(parentComment, trimmedComment)
|
||||
}
|
||||
|
||||
fun deleteParentComment() {
|
||||
val parentComment = parentComment ?: return
|
||||
if (parentComment.commentId <= 0 || isModifyingReply) return
|
||||
|
||||
deleteParentComment(parentComment.commentId)
|
||||
}
|
||||
|
||||
private fun submitReplyEdit(
|
||||
content: CreatorChannelCommunityReplyUiState.Content,
|
||||
target: CreatorChannelCommunityReplyEditTarget,
|
||||
comment: String
|
||||
) {
|
||||
if (isModifyingReply) return
|
||||
if (comment.isBlank()) {
|
||||
showBlankCommentToast()
|
||||
return
|
||||
}
|
||||
if (comment == target.originalComment.trim()) {
|
||||
showNoChangeToast()
|
||||
return
|
||||
}
|
||||
|
||||
when (target) {
|
||||
is CreatorChannelCommunityReplyEditTarget.ParentComment -> modifyParentCommentFromInput(content, comment)
|
||||
is CreatorChannelCommunityReplyEditTarget.Reply -> modifyReplyFromInput(content, target.replyId, comment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun modifyReplyFromInput(
|
||||
content: CreatorChannelCommunityReplyUiState.Content,
|
||||
replyId: Long,
|
||||
comment: String
|
||||
) {
|
||||
isModifyingReply = true
|
||||
_replyStateLiveData.value = content.copy(isSendingReply = true)
|
||||
compositeDisposable.add(
|
||||
legacyRepository.modifyComment(ModifyCommentRequest(commentId = replyId, comment = comment), authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isModifyingReply = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
?: return@subscribe
|
||||
if (response.success) {
|
||||
_replyChangedEventLiveData.value = true
|
||||
_replyStateLiveData.value = current.copy(
|
||||
replyInput = "",
|
||||
isSendingReply = false,
|
||||
editingTarget = null
|
||||
)
|
||||
loadReplyPage(page = FIRST_PAGE, append = false)
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
_replyStateLiveData.value = current.copy(isSendingReply = false)
|
||||
}
|
||||
},
|
||||
{
|
||||
isModifyingReply = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
?: return@subscribe
|
||||
showMutationFailureToast()
|
||||
_replyStateLiveData.value = current.copy(isSendingReply = false)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun modifyParentCommentFromInput(
|
||||
content: CreatorChannelCommunityReplyUiState.Content,
|
||||
comment: String
|
||||
) {
|
||||
val parentComment = parentComment ?: return
|
||||
isModifyingReply = true
|
||||
_replyStateLiveData.value = content.copy(isSendingReply = true)
|
||||
compositeDisposable.add(
|
||||
legacyRepository.modifyComment(
|
||||
ModifyCommentRequest(commentId = parentComment.commentId, comment = comment),
|
||||
authToken()
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isModifyingReply = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
?: return@subscribe
|
||||
if (response.success) {
|
||||
val updatedParent = parentComment.copy(comment = comment)
|
||||
this.parentComment = updatedParent
|
||||
_replyStateLiveData.value = current.copy(
|
||||
parentComment = updatedParent,
|
||||
replyInput = "",
|
||||
isSendingReply = false,
|
||||
editingTarget = null
|
||||
)
|
||||
_parentCommentChangedEventLiveData.value = true
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
_replyStateLiveData.value = current.copy(isSendingReply = false)
|
||||
}
|
||||
},
|
||||
{
|
||||
isModifyingReply = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
?: return@subscribe
|
||||
showMutationFailureToast()
|
||||
_replyStateLiveData.value = current.copy(isSendingReply = false)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun modifyParentComment(parentComment: CreatorChannelCommunityCommentUiModel, comment: String) {
|
||||
isModifyingReply = true
|
||||
compositeDisposable.add(
|
||||
legacyRepository.modifyComment(
|
||||
ModifyCommentRequest(commentId = parentComment.commentId, comment = comment),
|
||||
authToken()
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isModifyingReply = false
|
||||
if (response.success) {
|
||||
val updatedParent = parentComment.copy(comment = comment)
|
||||
this.parentComment = updatedParent
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
?: return@subscribe
|
||||
_replyStateLiveData.value = current.copy(parentComment = updatedParent)
|
||||
_parentCommentChangedEventLiveData.value = true
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isModifyingReply = false
|
||||
showMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun deleteParentComment(commentId: Long) {
|
||||
isModifyingReply = true
|
||||
compositeDisposable.add(
|
||||
legacyRepository.modifyComment(ModifyCommentRequest(commentId = commentId, isActive = false), authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isModifyingReply = false
|
||||
if (response.success) {
|
||||
_parentCommentDeletedEventLiveData.value = true
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isModifyingReply = false
|
||||
showMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadReplyPage(page: Int, append: Boolean) {
|
||||
val commentId = parentComment?.commentId ?: return
|
||||
if (commentId <= 0 || isLoadingReplies && append) return
|
||||
val generation = if (append) {
|
||||
replyRequestGeneration
|
||||
} else {
|
||||
++replyRequestGeneration
|
||||
}
|
||||
|
||||
isLoadingReplies = true
|
||||
val beforeRequest = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
if (beforeRequest != null) {
|
||||
_replyStateLiveData.value = beforeRequest.copy(isLoadingReplies = true)
|
||||
}
|
||||
compositeDisposable.add(
|
||||
repository.getCommunityCommentReplies(commentId, page, DEFAULT_PAGE_SIZE, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response -> handleRepliesResponse(response, append, generation) },
|
||||
{
|
||||
if (generation != replyRequestGeneration) return@subscribe
|
||||
|
||||
isLoadingReplies = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content
|
||||
?: return@subscribe
|
||||
_replyStateLiveData.value = current.copy(isLoadingReplies = false)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleRepliesResponse(
|
||||
response: ApiResponse<CreatorChannelCommunityRepliesResponse>,
|
||||
append: Boolean,
|
||||
generation: Int
|
||||
) {
|
||||
if (generation != replyRequestGeneration) return
|
||||
|
||||
isLoadingReplies = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
val data = response.data
|
||||
if (!response.success || data == null) {
|
||||
_replyStateLiveData.value = current.copy(isLoadingReplies = false)
|
||||
return
|
||||
}
|
||||
|
||||
val nextReplies = data.replies.map { it.toUiModel() }
|
||||
_replyStateLiveData.value = current.copy(
|
||||
replies = if (append) current.replies + nextReplies else nextReplies,
|
||||
replyPage = data.page,
|
||||
hasNextReply = data.hasNext,
|
||||
isLoadingReplies = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleWriteResponse(response: ApiResponse<Any>) {
|
||||
isSendingReply = false
|
||||
val current = _replyStateLiveData.value as? CreatorChannelCommunityReplyUiState.Content ?: return
|
||||
if (response.success) {
|
||||
_replyWrittenEventLiveData.value = true
|
||||
_replyChangedEventLiveData.value = true
|
||||
_replyStateLiveData.value = current.copy(replyInput = "", isSendingReply = false)
|
||||
loadReplyPage(page = FIRST_PAGE, append = false)
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
_replyStateLiveData.value = current.copy(isSendingReply = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun modifyComment(request: ModifyCommentRequest) {
|
||||
val commentId = parentComment?.commentId ?: return
|
||||
if (commentId <= 0) return
|
||||
|
||||
isModifyingReply = true
|
||||
compositeDisposable.add(
|
||||
legacyRepository.modifyComment(request, authToken())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
isModifyingReply = false
|
||||
if (response.success) {
|
||||
_replyChangedEventLiveData.value = true
|
||||
loadReplyPage(page = FIRST_PAGE, append = false)
|
||||
} else {
|
||||
showMutationFailureToast(response.message)
|
||||
}
|
||||
},
|
||||
{
|
||||
isModifyingReply = false
|
||||
showMutationFailureToast()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showMutationFailureToast(message: String? = null) {
|
||||
_toastLiveData.value = CreatorChannelEvent(
|
||||
if (message.isNullOrBlank()) {
|
||||
ToastMessage(resId = R.string.common_error_unknown)
|
||||
} else {
|
||||
ToastMessage(message = message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showBlankCommentToast() {
|
||||
_toastLiveData.value = CreatorChannelEvent(
|
||||
ToastMessage(resId = R.string.screen_creator_community_write_content_hint)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showNoChangeToast() {
|
||||
_toastLiveData.value = CreatorChannelEvent(
|
||||
ToastMessage(resId = R.string.audio_content_comment_no_change)
|
||||
)
|
||||
}
|
||||
|
||||
private fun CreatorChannelCommunityReplyResponse.toUiModel() = CreatorChannelCommunityReplyUiModel(
|
||||
replyId = commentId,
|
||||
commentId = parentComment?.commentId ?: 0L,
|
||||
memberId = writerId,
|
||||
memberNickname = writerNickname,
|
||||
memberProfileUrl = writerProfileImageUrl,
|
||||
createdAtUtc = createdAtUtc,
|
||||
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
|
||||
comment = content,
|
||||
likeCount = 0,
|
||||
isLiked = false,
|
||||
isMine = writerId == SharedPreferenceManager.userId,
|
||||
isCreatorOwner = parentComment?.isCreatorOwner == true
|
||||
)
|
||||
|
||||
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_PAGE_SIZE = 20
|
||||
private const val FIRST_PAGE = 0
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface CreatorChannelCommunityReplyEditTarget {
|
||||
val originalComment: String
|
||||
|
||||
data class ParentComment(
|
||||
override val originalComment: String
|
||||
) : CreatorChannelCommunityReplyEditTarget
|
||||
|
||||
data class Reply(
|
||||
val replyId: Long,
|
||||
override val originalComment: String
|
||||
) : CreatorChannelCommunityReplyEditTarget
|
||||
}
|
||||
|
||||
sealed interface CreatorChannelCommunityReplyUiState {
|
||||
data class Content(
|
||||
val parentComment: CreatorChannelCommunityCommentUiModel,
|
||||
val replies: List<CreatorChannelCommunityReplyUiModel>,
|
||||
val replyPage: Int,
|
||||
val hasNextReply: Boolean,
|
||||
val isLoadingReplies: Boolean = false,
|
||||
val replyInput: String = "",
|
||||
val isSendingReply: Boolean = false,
|
||||
val editingTarget: CreatorChannelCommunityReplyEditTarget? = null
|
||||
) : CreatorChannelCommunityReplyUiState
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.reply.ui
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityReplyBinding
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityReplyUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui.CreatorChannelCommunityMorePopup
|
||||
|
||||
class CreatorChannelCommunityReplyAdapter(
|
||||
private val onModifyClick: (CreatorChannelCommunityReplyUiModel) -> Unit,
|
||||
private val onDeleteClick: (CreatorChannelCommunityReplyUiModel) -> Unit
|
||||
) : RecyclerView.Adapter<CreatorChannelCommunityReplyAdapter.ViewHolder>() {
|
||||
|
||||
private var items: List<CreatorChannelCommunityReplyUiModel> = emptyList()
|
||||
|
||||
fun submitItems(items: List<CreatorChannelCommunityReplyUiModel>) {
|
||||
this.items = items
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(
|
||||
ItemCreatorChannelCommunityReplyBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
onModifyClick,
|
||||
onDeleteClick
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
class ViewHolder(
|
||||
private val binding: ItemCreatorChannelCommunityReplyBinding,
|
||||
private val onModifyClick: (CreatorChannelCommunityReplyUiModel) -> Unit,
|
||||
private val onDeleteClick: (CreatorChannelCommunityReplyUiModel) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: CreatorChannelCommunityReplyUiModel) = with(binding) {
|
||||
ivCreatorChannelCommunityReplyItemProfile.loadUrl(item.memberProfileUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
tvCreatorChannelCommunityReplyItemNickname.text = item.memberNickname
|
||||
tvCreatorChannelCommunityReplyItemTime.text = item.createdAtText
|
||||
tvCreatorChannelCommunityReplyItemBody.text = item.comment
|
||||
ivCreatorChannelCommunityReplyItemMore.isVisible = item.canShowMore
|
||||
ivCreatorChannelCommunityReplyItemMore.setOnClickListener { anchor ->
|
||||
showOptionMenu(anchor, item, onModifyClick, onDeleteClick)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showOptionMenu(
|
||||
anchor: View,
|
||||
item: CreatorChannelCommunityReplyUiModel,
|
||||
onModifyClick: (CreatorChannelCommunityReplyUiModel) -> Unit,
|
||||
onDeleteClick: (CreatorChannelCommunityReplyUiModel) -> Unit
|
||||
) {
|
||||
if (!item.canShowMore) return
|
||||
CreatorChannelCommunityMorePopup(
|
||||
anchor = anchor,
|
||||
showEdit = item.isMine,
|
||||
showDelete = true,
|
||||
onEditClick = { onModifyClick(item) },
|
||||
onDeleteClick = { onDeleteClick(item) }
|
||||
).show(anchor)
|
||||
}
|
||||
|
||||
private val CreatorChannelCommunityReplyUiModel.canShowMore: Boolean
|
||||
get() = isMine || isCreatorOwner
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelCommunityCommentBinding
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.CreatorChannelCommunityCommentUiModel
|
||||
|
||||
class CreatorChannelCommunityCommentAdapter(
|
||||
private val onReplyClick: (CreatorChannelCommunityCommentUiModel) -> Unit,
|
||||
private val onModifyClick: (CreatorChannelCommunityCommentUiModel) -> Unit = {},
|
||||
private val onDeleteClick: (CreatorChannelCommunityCommentUiModel) -> Unit = {}
|
||||
) : RecyclerView.Adapter<CreatorChannelCommunityCommentAdapter.ViewHolder>() {
|
||||
|
||||
private var items: List<CreatorChannelCommunityCommentUiModel> = emptyList()
|
||||
|
||||
fun submitItems(items: List<CreatorChannelCommunityCommentUiModel>) {
|
||||
this.items = items
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(
|
||||
ItemCreatorChannelCommunityCommentBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
onReplyClick,
|
||||
onModifyClick,
|
||||
onDeleteClick
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
class ViewHolder(
|
||||
private val binding: ItemCreatorChannelCommunityCommentBinding,
|
||||
private val onReplyClick: (CreatorChannelCommunityCommentUiModel) -> Unit,
|
||||
private val onModifyClick: (CreatorChannelCommunityCommentUiModel) -> Unit,
|
||||
private val onDeleteClick: (CreatorChannelCommunityCommentUiModel) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: CreatorChannelCommunityCommentUiModel) {
|
||||
bindComment(binding, item, onReplyClick)
|
||||
binding.ivCreatorChannelCommunityCommentMore.isVisible = item.canShowMore
|
||||
binding.ivCreatorChannelCommunityCommentMore.setOnClickListener { anchor ->
|
||||
showOptionMenu(anchor, item, onModifyClick, onDeleteClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun bindComment(
|
||||
binding: ItemCreatorChannelCommunityCommentBinding,
|
||||
item: CreatorChannelCommunityCommentUiModel,
|
||||
onReplyClick: (CreatorChannelCommunityCommentUiModel) -> Unit,
|
||||
onModifyClick: ((CreatorChannelCommunityCommentUiModel) -> Unit)? = null,
|
||||
onDeleteClick: ((CreatorChannelCommunityCommentUiModel) -> Unit)? = null
|
||||
) = with(binding) {
|
||||
ivCreatorChannelCommunityCommentProfile.loadUrl(item.memberProfileUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
tvCreatorChannelCommunityCommentNickname.text = item.memberNickname
|
||||
tvCreatorChannelCommunityCommentTime.text = item.createdAtText
|
||||
tvCreatorChannelCommunityCommentBody.text = item.comment
|
||||
ivCreatorChannelCommunityCommentMore.isVisible = item.canShowMore && onDeleteClick != null
|
||||
ivCreatorChannelCommunityCommentMore.setOnClickListener { anchor ->
|
||||
showOptionMenu(
|
||||
anchor,
|
||||
item,
|
||||
onModifyClick ?: {},
|
||||
onDeleteClick ?: {}
|
||||
)
|
||||
}
|
||||
layoutCreatorChannelCommunityCommentLatestReply.isVisible = item.latestReply != null
|
||||
item.latestReply?.let { latestReply ->
|
||||
tvCreatorChannelCommunityCommentLatestReplyNickname.text = latestReply.memberNickname
|
||||
tvCreatorChannelCommunityCommentLatestReplyBody.text = latestReply.comment
|
||||
}
|
||||
root.setOnClickListener { onReplyClick(item) }
|
||||
layoutCreatorChannelCommunityCommentLatestReply.setOnClickListener { onReplyClick(item) }
|
||||
}
|
||||
|
||||
private fun showOptionMenu(
|
||||
anchor: View,
|
||||
item: CreatorChannelCommunityCommentUiModel,
|
||||
onModifyClick: (CreatorChannelCommunityCommentUiModel) -> Unit,
|
||||
onDeleteClick: (CreatorChannelCommunityCommentUiModel) -> Unit
|
||||
) {
|
||||
if (!item.canShowMore) return
|
||||
CreatorChannelCommunityMorePopup(
|
||||
anchor = anchor,
|
||||
showEdit = item.isMine,
|
||||
showDelete = true,
|
||||
onEditClick = { onModifyClick(item) },
|
||||
onDeleteClick = { onDeleteClick(item) }
|
||||
).show(anchor)
|
||||
}
|
||||
|
||||
private val CreatorChannelCommunityCommentUiModel.canShowMore: Boolean
|
||||
get() = isMine || isCreatorOwner
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.ui
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupWindow
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.view.isVisible
|
||||
import kr.co.vividnext.sodalive.databinding.ViewCreatorChannelCommunityMorePopupBinding
|
||||
|
||||
class CreatorChannelCommunityMorePopup(
|
||||
anchor: View,
|
||||
showEdit: Boolean,
|
||||
showDelete: Boolean,
|
||||
onEditClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit
|
||||
) {
|
||||
|
||||
private val binding = ViewCreatorChannelCommunityMorePopupBinding.inflate(LayoutInflater.from(anchor.context))
|
||||
private val popupWindow: PopupWindow = PopupWindow(anchor.context).apply {
|
||||
contentView = binding.root
|
||||
width = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
isOutsideTouchable = true
|
||||
isFocusable = true
|
||||
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
|
||||
}
|
||||
|
||||
init {
|
||||
binding.tvCreatorChannelCommunityMoreEdit.isVisible = showEdit
|
||||
binding.tvCreatorChannelCommunityMoreDelete.isVisible = showDelete
|
||||
binding.tvCreatorChannelCommunityMoreEdit.setOnClickListener {
|
||||
dismiss()
|
||||
onEditClick()
|
||||
}
|
||||
binding.tvCreatorChannelCommunityMoreDelete.setOnClickListener {
|
||||
dismiss()
|
||||
onDeleteClick()
|
||||
}
|
||||
}
|
||||
|
||||
fun show(anchor: View) {
|
||||
popupWindow.showAsDropDown(anchor)
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityImageMode
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
|
||||
|
||||
class CreatorChannelCommunityGridAdapter : RecyclerView.Adapter<CreatorChannelCommunityGridAdapter.ViewHolder>() {
|
||||
class CreatorChannelCommunityGridAdapter(
|
||||
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {}
|
||||
) : RecyclerView.Adapter<CreatorChannelCommunityGridAdapter.ViewHolder>() {
|
||||
|
||||
private var items: List<CreatorChannelCommunityPostUiModel> = emptyList()
|
||||
private var itemSizePx: Int = 0
|
||||
@@ -31,7 +33,10 @@ class CreatorChannelCommunityGridAdapter : RecyclerView.Adapter<CreatorChannelCo
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(ItemCreatorChannelCommunityGridBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
return ViewHolder(
|
||||
ItemCreatorChannelCommunityGridBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
onPostClick
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
@@ -41,10 +46,12 @@ class CreatorChannelCommunityGridAdapter : RecyclerView.Adapter<CreatorChannelCo
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
class ViewHolder(
|
||||
private val binding: ItemCreatorChannelCommunityGridBinding
|
||||
private val binding: ItemCreatorChannelCommunityGridBinding,
|
||||
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: CreatorChannelCommunityPostUiModel, itemSizePx: Int) = with(binding) {
|
||||
root.setOnClickListener { onPostClick(item) }
|
||||
if (itemSizePx > 0) {
|
||||
root.layoutParams = root.layoutParams.apply {
|
||||
width = itemSizePx
|
||||
|
||||
@@ -17,6 +17,7 @@ import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.model.CreatorChannelCommunityPostUiModel
|
||||
|
||||
class CreatorChannelCommunityListAdapter(
|
||||
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
||||
private val onPlayClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
||||
private val onOwnerMoreClick: (CreatorChannelCommunityPostUiModel) -> Unit = {},
|
||||
private val isPlayingContent: (Long) -> Boolean = { false }
|
||||
@@ -32,6 +33,7 @@ class CreatorChannelCommunityListAdapter(
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(
|
||||
ItemCreatorChannelCommunityListBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
onPostClick,
|
||||
onPlayClick,
|
||||
onOwnerMoreClick,
|
||||
isPlayingContent
|
||||
@@ -46,12 +48,14 @@ class CreatorChannelCommunityListAdapter(
|
||||
|
||||
class ViewHolder(
|
||||
private val binding: ItemCreatorChannelCommunityListBinding,
|
||||
private val onPostClick: (CreatorChannelCommunityPostUiModel) -> Unit,
|
||||
private val onPlayClick: (CreatorChannelCommunityPostUiModel) -> Unit,
|
||||
private val onOwnerMoreClick: (CreatorChannelCommunityPostUiModel) -> Unit,
|
||||
private val isPlayingContent: (Long) -> Boolean
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: CreatorChannelCommunityPostUiModel) = with(binding) {
|
||||
root.setOnClickListener { onPostClick(item) }
|
||||
ivCreatorChannelCommunityListProfile.loadUrl(item.creatorProfileUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
|
||||
@@ -2,9 +2,11 @@ package kr.co.vividnext.sodalive.v2.creator.channel.data
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.v2.common.data.ContentSort
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioTabResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityTabResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentsResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityPostDetailResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityRepliesResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationTabResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkTabResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.live.data.CreatorChannelLiveTabResponse
|
||||
@@ -26,7 +28,7 @@ interface CreatorChannelApi {
|
||||
@Path("creatorId") creatorId: Long,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("sort") sort: ContentSort,
|
||||
@Query("sort") sort: String,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<CreatorChannelLiveTabResponse>>
|
||||
|
||||
@@ -35,7 +37,7 @@ interface CreatorChannelApi {
|
||||
@Path("creatorId") creatorId: Long,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("sort") sort: ContentSort,
|
||||
@Query("sort") sort: String,
|
||||
@Query("themeId") themeId: Long?,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<CreatorChannelAudioTabResponse>>
|
||||
@@ -45,7 +47,7 @@ interface CreatorChannelApi {
|
||||
@Path("creatorId") creatorId: Long,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("sort") sort: ContentSort,
|
||||
@Query("sort") sort: String,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<CreatorChannelSeriesTabResponse>>
|
||||
|
||||
@@ -57,6 +59,28 @@ interface CreatorChannelApi {
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<CreatorChannelCommunityTabResponse>>
|
||||
|
||||
@GET("/api/v2/creator-channels/community-posts/{postId}")
|
||||
fun getCommunityPostDetail(
|
||||
@Path("postId") postId: Long,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<CreatorChannelCommunityPostDetailResponse>>
|
||||
|
||||
@GET("/api/v2/creator-channels/community-posts/{postId}/comments")
|
||||
fun getCommunityPostComments(
|
||||
@Path("postId") postId: Long,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<CreatorChannelCommunityCommentsResponse>>
|
||||
|
||||
@GET("/api/v2/creator-channels/community-comments/{commentId}/replies")
|
||||
fun getCommunityCommentReplies(
|
||||
@Path("commentId") commentId: Long,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Header("Authorization") authHeader: String
|
||||
): Single<ApiResponse<CreatorChannelCommunityRepliesResponse>>
|
||||
|
||||
@GET("/api/v2/creator-channels/{creatorId}/fan-talks")
|
||||
fun getFanTalks(
|
||||
@Path("creatorId") creatorId: Long,
|
||||
|
||||
@@ -33,7 +33,7 @@ class CreatorChannelRepository(
|
||||
creatorId = creatorId,
|
||||
page = page,
|
||||
size = size,
|
||||
sort = sort,
|
||||
sort = sort.queryValue,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
@@ -48,7 +48,7 @@ class CreatorChannelRepository(
|
||||
creatorId = creatorId,
|
||||
page = page,
|
||||
size = size,
|
||||
sort = sort,
|
||||
sort = sort.queryValue,
|
||||
themeId = themeId,
|
||||
authHeader = token
|
||||
)
|
||||
@@ -63,7 +63,7 @@ class CreatorChannelRepository(
|
||||
creatorId = creatorId,
|
||||
page = page,
|
||||
size = size,
|
||||
sort = sort,
|
||||
sort = sort.queryValue,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
@@ -79,6 +79,35 @@ class CreatorChannelRepository(
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getCommunityPostDetail(postId: Long, token: String) = api.getCommunityPostDetail(
|
||||
postId = postId,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getCommunityPostComments(
|
||||
postId: Long,
|
||||
page: Int = 0,
|
||||
size: Int = 20,
|
||||
token: String
|
||||
) = api.getCommunityPostComments(
|
||||
postId = postId,
|
||||
page = page,
|
||||
size = size,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getCommunityCommentReplies(
|
||||
commentId: Long,
|
||||
page: Int = 0,
|
||||
size: Int = 20,
|
||||
token: String
|
||||
) = api.getCommunityCommentReplies(
|
||||
commentId = commentId,
|
||||
page = page,
|
||||
size = size,
|
||||
authHeader = token
|
||||
)
|
||||
|
||||
fun getFanTalks(
|
||||
creatorId: Long,
|
||||
page: Int,
|
||||
@@ -160,4 +189,28 @@ class CreatorChannelRepository(
|
||||
request = PutModifyCheersRequest(cheersId = fanTalkId, isActive = false),
|
||||
token = token
|
||||
)
|
||||
|
||||
fun writeFanTalk(creatorId: Long, content: String, token: String) = explorerRepository.writeCheers(
|
||||
parentCheersId = null,
|
||||
creatorId = creatorId,
|
||||
content = content,
|
||||
token = token
|
||||
)
|
||||
|
||||
fun writeFanTalkReply(fanTalkId: Long, creatorId: Long, content: String, token: String) = explorerRepository.writeCheers(
|
||||
parentCheersId = fanTalkId,
|
||||
creatorId = creatorId,
|
||||
content = content,
|
||||
token = token
|
||||
)
|
||||
|
||||
fun modifyFanTalkReply(replyFanTalkId: Long, content: String, token: String) = explorerRepository.modifyCheers(
|
||||
request = PutModifyCheersRequest(cheersId = replyFanTalkId, content = content),
|
||||
token = token
|
||||
)
|
||||
|
||||
fun deleteFanTalkReply(replyFanTalkId: Long, token: String) = explorerRepository.modifyCheers(
|
||||
request = PutModifyCheersRequest(cheersId = replyFanTalkId, isActive = false),
|
||||
token = token
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ private fun CreatorChannelDonationResponse.toDonationUiModel(
|
||||
|
||||
@ColorRes
|
||||
internal fun calculateDonationHeaderColorRes(can: Int): Int = when {
|
||||
can >= 500 -> R.color.red_400
|
||||
can >= 501 -> R.color.red_400
|
||||
can >= 101 -> R.color.creator_channel_donation_cyan
|
||||
can >= 51 -> R.color.green_400
|
||||
else -> R.color.gray_200
|
||||
|
||||
@@ -8,15 +8,12 @@ import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationEmptyBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ItemCreatorChannelDonationRankingBinding
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.CreatorChannelDonationRankingUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.model.CreatorChannelDonationUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.ui.bindCreatorChannelDonationCard
|
||||
|
||||
class CreatorChannelDonationAdapter(
|
||||
private val onRankingAllClick: () -> Unit = { },
|
||||
@@ -119,20 +116,20 @@ class CreatorChannelDonationAdapter(
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: CreatorChannelDonationUiModel) = with(binding) {
|
||||
layoutCreatorChannelDonationHeader.setBackgroundColor(root.context.getColor(item.headerColorResId))
|
||||
ivCreatorChannelDonationProfile.loadUrl(item.profileImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_placeholder_profile)
|
||||
error(R.drawable.ic_placeholder_profile)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
tvCreatorChannelDonationNickname.text = item.nickname
|
||||
tvCreatorChannelDonationCreatedAt.text = item.createdAtText
|
||||
tvCreatorChannelDonationCan.text = root.context.getString(
|
||||
R.string.creator_channel_donation_can_format,
|
||||
item.can.moneyFormat()
|
||||
bindCreatorChannelDonationCard(
|
||||
headerView = layoutCreatorChannelDonationHeader,
|
||||
profileView = ivCreatorChannelDonationProfile,
|
||||
nicknameView = tvCreatorChannelDonationNickname,
|
||||
createdAtView = tvCreatorChannelDonationCreatedAt,
|
||||
canView = tvCreatorChannelDonationCan,
|
||||
messageView = tvCreatorChannelDonationMessage,
|
||||
headerColorResId = item.headerColorResId,
|
||||
profileImageUrl = item.profileImageUrl,
|
||||
nickname = item.nickname,
|
||||
createdAtText = item.createdAtText,
|
||||
can = item.can,
|
||||
message = item.message
|
||||
)
|
||||
tvCreatorChannelDonationMessage.text = item.message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
|
||||
private val viewModel: CreatorChannelFanTalkViewModel by viewModel()
|
||||
private val fanTalkAdapter = CreatorChannelFanTalkAdapter(
|
||||
onOwnerMoreClick = ::showOwnerMorePopup,
|
||||
onReportClick = ::showReportDialog
|
||||
onReportClick = ::showReportDialog,
|
||||
onItemClick = { host.onCreatorChannelFanTalkItemClicked(it) }
|
||||
)
|
||||
private var morePopup: CreatorChannelFanTalkMorePopup? = null
|
||||
private var lastContentLayoutKey: CreatorChannelFanTalkContentLayoutKey? = null
|
||||
@@ -43,6 +44,7 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
|
||||
morePopup?.dismiss()
|
||||
morePopup = null
|
||||
lastContentLayoutKey = null
|
||||
host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(false)
|
||||
binding.rvCreatorChannelFantalk.adapter = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
@@ -77,8 +79,8 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
|
||||
btnCreatorChannelFantalkRetry.setOnClickListener {
|
||||
viewModel.retryFanTalks()
|
||||
}
|
||||
btnCreatorChannelFantalkWrite.setOnClickListener { }
|
||||
layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener { }
|
||||
btnCreatorChannelFantalkWrite.setOnClickListener { host.onCreatorChannelFanTalkWriteClicked() }
|
||||
layoutCreatorChannelFantalkEmptyWriteButton.setOnClickListener { host.onCreatorChannelFanTalkWriteClicked() }
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
@@ -100,6 +102,7 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
|
||||
tvCreatorChannelFantalkErrorMessage.isVisible = false
|
||||
btnCreatorChannelFantalkRetry.isVisible = false
|
||||
btnCreatorChannelFantalkWrite.isVisible = false
|
||||
host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(false)
|
||||
}
|
||||
|
||||
private fun bindEmpty() = with(binding) {
|
||||
@@ -110,6 +113,7 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
|
||||
tvCreatorChannelFantalkErrorMessage.isVisible = false
|
||||
btnCreatorChannelFantalkRetry.isVisible = false
|
||||
btnCreatorChannelFantalkWrite.isVisible = false
|
||||
host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(false)
|
||||
host.onCreatorChannelFanTalkContentChanged()
|
||||
}
|
||||
|
||||
@@ -122,6 +126,7 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
|
||||
tvCreatorChannelFantalkErrorMessage.text = state.message ?: getString(R.string.creator_channel_fantalk_error_message)
|
||||
btnCreatorChannelFantalkRetry.isVisible = true
|
||||
btnCreatorChannelFantalkWrite.isVisible = false
|
||||
host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(false)
|
||||
host.onCreatorChannelFanTalkContentChanged()
|
||||
}
|
||||
|
||||
@@ -133,7 +138,8 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
|
||||
layoutCreatorChannelFantalkEmpty.isVisible = false
|
||||
tvCreatorChannelFantalkErrorMessage.isVisible = false
|
||||
btnCreatorChannelFantalkRetry.isVisible = false
|
||||
btnCreatorChannelFantalkWrite.isVisible = true
|
||||
btnCreatorChannelFantalkWrite.isVisible = false
|
||||
host.onCreatorChannelFanTalkFloatingButtonVisibilityChanged(true)
|
||||
notifyContentChangedIfLayoutChanged(state)
|
||||
state.paginationErrorMessage?.let {
|
||||
Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show()
|
||||
@@ -178,7 +184,10 @@ class CreatorChannelFanTalkFragment : BaseFragment<FragmentCreatorChannelFantalk
|
||||
interface Host {
|
||||
fun isCreatorChannelOwner(): Boolean
|
||||
fun onCreatorChannelFanTalkContentChanged()
|
||||
fun onCreatorChannelFanTalkWriteClicked()
|
||||
fun onCreatorChannelFanTalkFloatingButtonVisibilityChanged(isVisible: Boolean)
|
||||
fun onCreatorChannelFanTalkDeleteClicked(fanTalkId: Long)
|
||||
fun onCreatorChannelFanTalkItemClicked(item: CreatorChannelFanTalkUiModel) = Unit
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.PopupWindow
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelFantalkDetailBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ViewCreatorChannelFantalkMorePopupBinding
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkReplyUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkUiModel
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class CreatorChannelFanTalkDetailActivity : BaseActivity<ActivityCreatorChannelFantalkDetailBinding>(
|
||||
ActivityCreatorChannelFantalkDetailBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: CreatorChannelFanTalkDetailViewModel by viewModel()
|
||||
private var creatorId: Long = 0L
|
||||
private var replyMorePopup: PopupWindow? = null
|
||||
private var renderedEditingTarget: CreatorChannelFanTalkDetailEditTarget? = null
|
||||
|
||||
override fun setupView() {
|
||||
creatorId = intent.getLongExtra(EXTRA_CREATOR_ID, 0L)
|
||||
val payload = payloadFromIntent() ?: run {
|
||||
Toast.makeText(this, R.string.creator_channel_community_invalid_post, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
if (creatorId <= 0L || payload.fanTalkId <= 0L) {
|
||||
Toast.makeText(this, R.string.creator_channel_community_invalid_post, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
binding.btnCreatorChannelFantalkDetailBack.setOnClickListener { finish() }
|
||||
binding.etCreatorChannelFantalkDetailReply.movementMethod = ScrollingMovementMethod()
|
||||
binding.etCreatorChannelFantalkDetailReply.doAfterTextChanged { editable ->
|
||||
viewModel.updateReplyInput(editable?.toString().orEmpty())
|
||||
}
|
||||
binding.btnCreatorChannelFantalkDetailSend.setOnClickListener { viewModel.submitReply() }
|
||||
binding.btnCreatorChannelFantalkDetailEditCancel.setOnClickListener { viewModel.cancelReplyEdit() }
|
||||
|
||||
viewModel.detailStateLiveData.observe(this) { state -> render(state) }
|
||||
viewModel.replacementModalEventLiveData.observe(this) { event ->
|
||||
event.consume()?.let(::showReplacementDialog)
|
||||
}
|
||||
viewModel.replyChangedEventLiveData.observe(this) { changed ->
|
||||
if (changed == true) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
hideReplyKeyboard()
|
||||
viewModel.consumeReplyChangedEvent()
|
||||
}
|
||||
}
|
||||
viewModel.toastLiveData.observe(this) { event ->
|
||||
event.consume()?.let { toast ->
|
||||
val message = toast.message ?: toast.resId?.let(::getString)
|
||||
message?.let(::showToast)
|
||||
}
|
||||
}
|
||||
viewModel.load(creatorId, payload)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
replyMorePopup?.dismiss()
|
||||
replyMorePopup = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun render(state: CreatorChannelFanTalkDetailUiState) {
|
||||
when (state) {
|
||||
is CreatorChannelFanTalkDetailUiState.Content -> renderContent(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderContent(content: CreatorChannelFanTalkDetailUiState.Content) = with(binding) {
|
||||
bindParentFanTalk(content.parentFanTalk)
|
||||
bindReply(content.reply)
|
||||
if (etCreatorChannelFantalkDetailReply.text.toString() != content.replyInput) {
|
||||
etCreatorChannelFantalkDetailReply.setText(content.replyInput)
|
||||
etCreatorChannelFantalkDetailReply.setSelection(content.replyInput.length)
|
||||
}
|
||||
btnCreatorChannelFantalkDetailEditCancel.isVisible = content.editingTarget != null
|
||||
if (content.editingTarget != null && content.editingTarget != renderedEditingTarget) {
|
||||
showReplyKeyboard()
|
||||
}
|
||||
renderedEditingTarget = content.editingTarget
|
||||
updateSendButton(content.isSendEnabled)
|
||||
}
|
||||
|
||||
private fun bindParentFanTalk(item: CreatorChannelFanTalkUiModel) = with(binding) {
|
||||
ivCreatorChannelFantalkDetailProfile.loadProfile(item.writerProfileImageUrl)
|
||||
tvCreatorChannelFantalkDetailNickname.text = item.writerNickname
|
||||
tvCreatorChannelFantalkDetailTime.text = item.createdAtText
|
||||
tvCreatorChannelFantalkDetailContent.text = item.content
|
||||
ivCreatorChannelFantalkDetailMore.isVisible = false
|
||||
ivCreatorChannelFantalkDetailMore.setOnClickListener(null)
|
||||
}
|
||||
|
||||
private fun bindReply(reply: CreatorChannelFanTalkReplyUiModel?) = with(binding) {
|
||||
viewCreatorChannelFantalkDetailReplyConnector.isVisible = reply != null
|
||||
layoutCreatorChannelFantalkDetailReply.isVisible = reply != null
|
||||
if (reply == null) {
|
||||
replyMorePopup?.dismiss()
|
||||
return@with
|
||||
}
|
||||
|
||||
ivCreatorChannelFantalkDetailReplyProfile.loadProfile(reply.writerProfileImageUrl)
|
||||
tvCreatorChannelFantalkDetailReplyNickname.text = reply.writerNickname
|
||||
tvCreatorChannelFantalkDetailReplyTime.text = reply.createdAtText
|
||||
tvCreatorChannelFantalkDetailReplyContent.text = reply.content
|
||||
ivCreatorChannelFantalkDetailReplyMore.isVisible = reply.fanTalkId > 0L
|
||||
ivCreatorChannelFantalkDetailReplyMore.setOnClickListener(
|
||||
if (reply.fanTalkId > 0L) {
|
||||
View.OnClickListener { anchor -> showReplyMorePopup(anchor) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun showReplyMorePopup(anchor: View) {
|
||||
replyMorePopup?.dismiss()
|
||||
val popupBinding = ViewCreatorChannelFantalkMorePopupBinding.inflate(LayoutInflater.from(anchor.context))
|
||||
replyMorePopup = PopupWindow(anchor.context).apply {
|
||||
contentView = popupBinding.root
|
||||
width = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
isOutsideTouchable = true
|
||||
isFocusable = true
|
||||
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
|
||||
}
|
||||
popupBinding.tvCreatorChannelFantalkMoreEdit.isVisible = true
|
||||
popupBinding.tvCreatorChannelFantalkMoreDelete.isVisible = true
|
||||
popupBinding.tvCreatorChannelFantalkMoreEdit.setOnClickListener {
|
||||
replyMorePopup?.dismiss()
|
||||
viewModel.startReplyEdit()
|
||||
}
|
||||
popupBinding.tvCreatorChannelFantalkMoreDelete.setOnClickListener {
|
||||
replyMorePopup?.dismiss()
|
||||
showDeleteReplyDialog()
|
||||
}
|
||||
replyMorePopup?.showAsDropDown(anchor)
|
||||
}
|
||||
|
||||
private fun showReplacementDialog(modal: CreatorChannelFanTalkReplacementModalUiModel) {
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(modal.titleResId),
|
||||
desc = getString(modal.descriptionResId),
|
||||
confirmButtonTitle = getString(modal.confirmResId),
|
||||
confirmButtonClick = { viewModel.confirmReplacement() },
|
||||
cancelButtonTitle = getString(modal.cancelResId),
|
||||
cancelButtonClick = { viewModel.cancelReplacement() }
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun showDeleteReplyDialog() {
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.confirm_delete_title),
|
||||
desc = getString(R.string.confirm_delete_message),
|
||||
confirmButtonTitle = getString(R.string.confirm),
|
||||
confirmButtonClick = { viewModel.deleteReply() },
|
||||
cancelButtonTitle = getString(R.string.cancel)
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
private fun updateSendButton(enabled: Boolean) = with(binding.btnCreatorChannelFantalkDetailSend) {
|
||||
isEnabled = enabled
|
||||
setBackgroundResource(
|
||||
if (enabled) {
|
||||
R.drawable.bg_creator_channel_community_send_enabled
|
||||
} else {
|
||||
R.drawable.bg_creator_channel_community_send_disabled
|
||||
}
|
||||
)
|
||||
setImageResource(if (enabled) R.drawable.ic_new_arrow_up_white else R.drawable.ic_new_arrow_up_gray)
|
||||
}
|
||||
|
||||
private fun showReplyKeyboard() {
|
||||
val editText = binding.etCreatorChannelFantalkDetailReply
|
||||
editText.requestFocus()
|
||||
editText.post {
|
||||
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideReplyKeyboard() {
|
||||
val editText = binding.etCreatorChannelFantalkDetailReply
|
||||
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(editText.windowToken, 0)
|
||||
editText.clearFocus()
|
||||
}
|
||||
|
||||
private fun payloadFromIntent(): CreatorChannelFanTalkDetailPayload? {
|
||||
return IntentCompat.getParcelableExtra(intent, EXTRA_PAYLOAD, CreatorChannelFanTalkDetailPayload::class.java)
|
||||
}
|
||||
|
||||
private fun ImageView.loadProfile(url: String) {
|
||||
loadUrl(url) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_placeholder_profile)
|
||||
error(R.drawable.ic_placeholder_profile)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_CREATOR_ID = "extra_creator_id"
|
||||
private const val EXTRA_PAYLOAD = "extra_payload"
|
||||
|
||||
fun newIntent(context: Context, creatorId: Long, payload: CreatorChannelFanTalkDetailPayload): Intent {
|
||||
return Intent(context, CreatorChannelFanTalkDetailActivity::class.java)
|
||||
.putExtra(EXTRA_CREATOR_ID, creatorId)
|
||||
.putExtra(EXTRA_PAYLOAD, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkReplyUiModel
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkRightAction
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkUiModel
|
||||
|
||||
@Parcelize
|
||||
data class CreatorChannelFanTalkDetailPayload(
|
||||
val fanTalkId: Long,
|
||||
val writerId: Long,
|
||||
val writerNickname: String,
|
||||
val writerProfileImageUrl: String,
|
||||
val content: String,
|
||||
val createdAtText: String,
|
||||
val reply: CreatorChannelFanTalkDetailReplyPayload?,
|
||||
val showEdit: Boolean,
|
||||
val showDelete: Boolean
|
||||
) : Parcelable {
|
||||
fun toUiModel(): CreatorChannelFanTalkUiModel = CreatorChannelFanTalkUiModel(
|
||||
fanTalkId = fanTalkId,
|
||||
writerId = writerId,
|
||||
writerNickname = writerNickname,
|
||||
writerProfileImageUrl = writerProfileImageUrl,
|
||||
content = content,
|
||||
createdAtText = createdAtText,
|
||||
reply = reply?.toUiModel(),
|
||||
rightAction = CreatorChannelFanTalkRightAction.OwnerMore(showEdit = showEdit, showDelete = showDelete)
|
||||
)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class CreatorChannelFanTalkDetailReplyPayload(
|
||||
val fanTalkId: Long,
|
||||
val writerId: Long,
|
||||
val writerNickname: String,
|
||||
val writerProfileImageUrl: String,
|
||||
val content: String,
|
||||
val createdAtText: String
|
||||
) : Parcelable {
|
||||
fun toUiModel(): CreatorChannelFanTalkReplyUiModel = CreatorChannelFanTalkReplyUiModel(
|
||||
fanTalkId = fanTalkId,
|
||||
writerId = writerId,
|
||||
writerNickname = writerNickname,
|
||||
writerProfileImageUrl = writerProfileImageUrl,
|
||||
content = content,
|
||||
createdAtText = createdAtText
|
||||
)
|
||||
}
|
||||
|
||||
fun CreatorChannelFanTalkUiModel.toFanTalkDetailPayload(): CreatorChannelFanTalkDetailPayload {
|
||||
val ownerMore = rightAction as? CreatorChannelFanTalkRightAction.OwnerMore
|
||||
return CreatorChannelFanTalkDetailPayload(
|
||||
fanTalkId = fanTalkId,
|
||||
writerId = writerId,
|
||||
writerNickname = writerNickname,
|
||||
writerProfileImageUrl = writerProfileImageUrl,
|
||||
content = content,
|
||||
createdAtText = createdAtText,
|
||||
reply = reply?.toFanTalkDetailReplyPayload(),
|
||||
showEdit = ownerMore?.showEdit == true,
|
||||
showDelete = ownerMore?.showDelete == true
|
||||
)
|
||||
}
|
||||
|
||||
fun CreatorChannelFanTalkReplyUiModel.toFanTalkDetailReplyPayload(): CreatorChannelFanTalkDetailReplyPayload {
|
||||
return CreatorChannelFanTalkDetailReplyPayload(
|
||||
fanTalkId = fanTalkId,
|
||||
writerId = writerId,
|
||||
writerNickname = writerNickname,
|
||||
writerProfileImageUrl = writerProfileImageUrl,
|
||||
content = content,
|
||||
createdAtText = createdAtText
|
||||
)
|
||||
}
|
||||
|
||||
data class CreatorChannelFanTalkReplacementModalUiModel(
|
||||
@StringRes val titleResId: Int = R.string.screen_user_profile_reply_edit,
|
||||
@StringRes val descriptionResId: Int = R.string.creator_channel_fantalk_reply_replace_description,
|
||||
@StringRes val cancelResId: Int = R.string.cancel,
|
||||
@StringRes val confirmResId: Int = R.string.confirm
|
||||
)
|
||||
|
||||
sealed interface CreatorChannelFanTalkDetailEditTarget {
|
||||
data class Reply(val replyFanTalkId: Long) : CreatorChannelFanTalkDetailEditTarget
|
||||
}
|
||||
|
||||
sealed interface CreatorChannelFanTalkDetailUiState {
|
||||
data class Content(
|
||||
val parentFanTalk: CreatorChannelFanTalkUiModel,
|
||||
val reply: CreatorChannelFanTalkReplyUiModel?,
|
||||
val replyInput: String = "",
|
||||
val isSubmitting: Boolean = false,
|
||||
val editingTarget: CreatorChannelFanTalkDetailEditTarget? = null
|
||||
) : CreatorChannelFanTalkDetailUiState {
|
||||
val isSendEnabled: Boolean = replyInput.trim().isNotEmpty() && !isSubmitting
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.orhanobut.logger.Logger
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
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.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelEvent
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.toFanTalkReplyUiModel
|
||||
|
||||
class CreatorChannelFanTalkDetailViewModel(
|
||||
private val repository: CreatorChannelRepository,
|
||||
private val relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _detailStateLiveData = MutableLiveData<CreatorChannelFanTalkDetailUiState>()
|
||||
val detailStateLiveData: LiveData<CreatorChannelFanTalkDetailUiState>
|
||||
get() = _detailStateLiveData
|
||||
|
||||
private val _replacementModalEventLiveData =
|
||||
MutableLiveData<CreatorChannelEvent<CreatorChannelFanTalkReplacementModalUiModel>>()
|
||||
val replacementModalEventLiveData: LiveData<CreatorChannelEvent<CreatorChannelFanTalkReplacementModalUiModel>>
|
||||
get() = _replacementModalEventLiveData
|
||||
|
||||
private val _replyChangedEventLiveData = MutableLiveData(false)
|
||||
val replyChangedEventLiveData: LiveData<Boolean>
|
||||
get() = _replyChangedEventLiveData
|
||||
|
||||
private val _toastLiveData = MutableLiveData<CreatorChannelEvent<ToastMessage>>()
|
||||
val toastLiveData: LiveData<CreatorChannelEvent<ToastMessage>>
|
||||
get() = _toastLiveData
|
||||
|
||||
private var creatorId: Long = 0L
|
||||
private var isSubmitting: Boolean = false
|
||||
private var pendingReplacementContent: String? = null
|
||||
|
||||
fun load(creatorId: Long, payload: CreatorChannelFanTalkDetailPayload) {
|
||||
this.creatorId = creatorId
|
||||
val parentFanTalk = payload.toUiModel()
|
||||
_detailStateLiveData.value = CreatorChannelFanTalkDetailUiState.Content(
|
||||
parentFanTalk = parentFanTalk,
|
||||
reply = parentFanTalk.reply
|
||||
)
|
||||
}
|
||||
|
||||
fun updateReplyInput(input: String) {
|
||||
val content = currentContent() ?: return
|
||||
_detailStateLiveData.value = content.copy(replyInput = input)
|
||||
}
|
||||
|
||||
fun submitReply() {
|
||||
val content = currentContent() ?: return
|
||||
val trimmedContent = content.replyInput.trim()
|
||||
if (trimmedContent.isEmpty() || isSubmitting) return
|
||||
|
||||
val editingTarget = content.editingTarget
|
||||
if (editingTarget is CreatorChannelFanTalkDetailEditTarget.Reply) {
|
||||
modifyReply(content, editingTarget.replyFanTalkId, trimmedContent)
|
||||
return
|
||||
}
|
||||
|
||||
val reply = content.reply
|
||||
if (reply == null) {
|
||||
writeReply(content, trimmedContent)
|
||||
} else {
|
||||
if (reply.fanTalkId <= 0L) {
|
||||
showFailureToast()
|
||||
return
|
||||
}
|
||||
pendingReplacementContent = trimmedContent
|
||||
_replacementModalEventLiveData.value = CreatorChannelEvent(CreatorChannelFanTalkReplacementModalUiModel())
|
||||
}
|
||||
}
|
||||
|
||||
fun confirmReplacement() {
|
||||
val content = currentContent() ?: return
|
||||
val reply = content.reply ?: return
|
||||
val replacementContent = pendingReplacementContent ?: content.replyInput.trim()
|
||||
if (replacementContent.isBlank() || isSubmitting) return
|
||||
if (reply.fanTalkId <= 0L) {
|
||||
pendingReplacementContent = null
|
||||
showFailureToast()
|
||||
return
|
||||
}
|
||||
|
||||
modifyReply(content, reply.fanTalkId, replacementContent)
|
||||
pendingReplacementContent = null
|
||||
}
|
||||
|
||||
fun cancelReplacement() {
|
||||
pendingReplacementContent = null
|
||||
}
|
||||
|
||||
fun consumeReplyChangedEvent() {
|
||||
_replyChangedEventLiveData.value = false
|
||||
}
|
||||
|
||||
fun cancelReplyEdit() {
|
||||
val content = currentContent() ?: return
|
||||
_detailStateLiveData.value = content.copy(replyInput = "", editingTarget = null)
|
||||
}
|
||||
|
||||
fun startReplyEdit() {
|
||||
val content = currentContent() ?: return
|
||||
val reply = content.reply ?: return
|
||||
_detailStateLiveData.value = content.copy(
|
||||
replyInput = reply.content,
|
||||
editingTarget = CreatorChannelFanTalkDetailEditTarget.Reply(reply.fanTalkId)
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteReply() {
|
||||
val content = currentContent() ?: return
|
||||
val reply = content.reply ?: return
|
||||
if (isSubmitting) return
|
||||
|
||||
submitMutation(content, repository.deleteFanTalkReply(reply.fanTalkId, authToken())) { current ->
|
||||
current.copy(reply = null, replyInput = "", editingTarget = null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeReply(content: CreatorChannelFanTalkDetailUiState.Content, replyContent: String) {
|
||||
if (creatorId <= 0L || content.parentFanTalk.fanTalkId <= 0L) return
|
||||
|
||||
isSubmitting = true
|
||||
_detailStateLiveData.value = content.copy(isSubmitting = true)
|
||||
compositeDisposable.add(
|
||||
repository.writeFanTalkReply(
|
||||
fanTalkId = content.parentFanTalk.fanTalkId,
|
||||
creatorId = creatorId,
|
||||
content = replyContent,
|
||||
token = authToken()
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response -> handleWriteReplyResponse(response) },
|
||||
{ error -> handleMutationError(error) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun modifyReply(
|
||||
content: CreatorChannelFanTalkDetailUiState.Content,
|
||||
replyFanTalkId: Long,
|
||||
replyContent: String
|
||||
) {
|
||||
if (replyFanTalkId <= 0L) return
|
||||
|
||||
submitMutation(
|
||||
content = content,
|
||||
request = repository.modifyFanTalkReply(replyFanTalkId, replyContent, authToken())
|
||||
) { current ->
|
||||
current.copy(
|
||||
reply = current.reply?.copy(content = replyContent),
|
||||
replyInput = "",
|
||||
editingTarget = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitMutation(
|
||||
content: CreatorChannelFanTalkDetailUiState.Content,
|
||||
request: Single<ApiResponse<CreatorChannelFanTalkReplyResponse>>,
|
||||
onSuccess: (CreatorChannelFanTalkDetailUiState.Content) -> CreatorChannelFanTalkDetailUiState.Content
|
||||
) {
|
||||
isSubmitting = true
|
||||
_detailStateLiveData.value = content.copy(isSubmitting = true)
|
||||
compositeDisposable.add(
|
||||
request
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response -> handleMutationResponse(response, onSuccess) },
|
||||
{ error -> handleMutationError(error) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleWriteReplyResponse(response: ApiResponse<CreatorChannelFanTalkReplyResponse>) {
|
||||
isSubmitting = false
|
||||
val current = currentContent() ?: return
|
||||
if (response.success) {
|
||||
_detailStateLiveData.value = current.copy(
|
||||
reply = response.data?.toFanTalkReplyUiModel(relativeTimeTextFormatter) ?: current.reply,
|
||||
replyInput = "",
|
||||
isSubmitting = false,
|
||||
editingTarget = null
|
||||
)
|
||||
_replyChangedEventLiveData.value = true
|
||||
} else {
|
||||
showFailureToast(response.message)
|
||||
_detailStateLiveData.value = current.copy(isSubmitting = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMutationError(error: Throwable) {
|
||||
isSubmitting = false
|
||||
error.message?.let { Logger.e(it) }
|
||||
val current = currentContent() ?: return
|
||||
showFailureToast()
|
||||
_detailStateLiveData.value = current.copy(isSubmitting = false)
|
||||
}
|
||||
|
||||
private fun handleMutationResponse(
|
||||
response: ApiResponse<CreatorChannelFanTalkReplyResponse>,
|
||||
onSuccess: (CreatorChannelFanTalkDetailUiState.Content) -> CreatorChannelFanTalkDetailUiState.Content
|
||||
) {
|
||||
isSubmitting = false
|
||||
val current = currentContent() ?: return
|
||||
if (response.success) {
|
||||
_detailStateLiveData.value = onSuccess(current).copy(isSubmitting = false)
|
||||
_replyChangedEventLiveData.value = true
|
||||
} else {
|
||||
showFailureToast(response.message)
|
||||
_detailStateLiveData.value = current.copy(isSubmitting = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFailureToast(message: String? = null) {
|
||||
_toastLiveData.value = CreatorChannelEvent(
|
||||
if (message.isNullOrBlank()) {
|
||||
ToastMessage(resId = R.string.common_error_unknown)
|
||||
} else {
|
||||
ToastMessage(message = message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun currentContent(): CreatorChannelFanTalkDetailUiState.Content? {
|
||||
return _detailStateLiveData.value as? CreatorChannelFanTalkDetailUiState.Content
|
||||
}
|
||||
|
||||
private fun authToken(): String = "Bearer ${SharedPreferenceManager.token}"
|
||||
}
|
||||
@@ -23,11 +23,22 @@ private fun CreatorChannelFanTalkResponse.toFanTalkUiModel(
|
||||
writerProfileImageUrl = writerProfileImageUrl,
|
||||
content = content,
|
||||
createdAtText = relativeTimeTextFormatter.format(createdAtUtc),
|
||||
reply = creatorReplies.firstOrNull()?.toReplyUiModel(relativeTimeTextFormatter),
|
||||
reply = creatorReplies.orEmpty().firstOrNull()?.toFanTalkReplyUiModel(relativeTimeTextFormatter),
|
||||
rightAction = toRightAction(isOwner = isOwner, currentUserId = currentUserId)
|
||||
)
|
||||
|
||||
private fun CreatorChannelFanTalkReplyResponse.toReplyUiModel(
|
||||
fun CreatorChannelFanTalkReplyResponse.toFanTalkReplyUiModel(
|
||||
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
|
||||
) = CreatorChannelFanTalkReplyUiModel(
|
||||
fanTalkId = fanTalkId,
|
||||
writerId = writerId,
|
||||
writerNickname = writerNickname,
|
||||
writerProfileImageUrl = writerProfileImageUrl,
|
||||
content = content,
|
||||
createdAtText = relativeTimeTextFormatter.format(createdAtUtc)
|
||||
)
|
||||
|
||||
fun CreatorChannelFanTalkResponse.toFanTalkReplyUiModel(
|
||||
relativeTimeTextFormatter: UtcRelativeTimeTextFormatter
|
||||
) = CreatorChannelFanTalkReplyUiModel(
|
||||
fanTalkId = fanTalkId,
|
||||
|
||||
@@ -15,7 +15,8 @@ import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelF
|
||||
|
||||
class CreatorChannelFanTalkAdapter(
|
||||
private val onOwnerMoreClick: (View, CreatorChannelFanTalkUiModel) -> Unit = { _, _ -> },
|
||||
private val onReportClick: (CreatorChannelFanTalkUiModel) -> Unit = { }
|
||||
private val onReportClick: (CreatorChannelFanTalkUiModel) -> Unit = { },
|
||||
private val onItemClick: (CreatorChannelFanTalkUiModel) -> Unit = { }
|
||||
) : RecyclerView.Adapter<CreatorChannelFanTalkAdapter.ViewHolder>() {
|
||||
|
||||
private var items: List<CreatorChannelFanTalkUiModel> = emptyList()
|
||||
@@ -29,7 +30,8 @@ class CreatorChannelFanTalkAdapter(
|
||||
return ViewHolder(
|
||||
ItemCreatorChannelFantalkBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
onOwnerMoreClick,
|
||||
onReportClick
|
||||
onReportClick,
|
||||
onItemClick
|
||||
)
|
||||
}
|
||||
|
||||
@@ -42,7 +44,8 @@ class CreatorChannelFanTalkAdapter(
|
||||
class ViewHolder(
|
||||
private val binding: ItemCreatorChannelFantalkBinding,
|
||||
private val onOwnerMoreClick: (View, CreatorChannelFanTalkUiModel) -> Unit,
|
||||
private val onReportClick: (CreatorChannelFanTalkUiModel) -> Unit
|
||||
private val onReportClick: (CreatorChannelFanTalkUiModel) -> Unit,
|
||||
private val onItemClick: (CreatorChannelFanTalkUiModel) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: CreatorChannelFanTalkUiModel) = with(binding) {
|
||||
@@ -50,6 +53,8 @@ class CreatorChannelFanTalkAdapter(
|
||||
tvCreatorChannelFantalkNickname.text = item.writerNickname
|
||||
tvCreatorChannelFantalkTime.text = item.createdAtText
|
||||
tvCreatorChannelFantalkContent.text = item.content
|
||||
root.setOnClickListener { onItemClick(item) }
|
||||
tvCreatorChannelFantalkContent.setOnClickListener { onItemClick(item) }
|
||||
bindRightAction(item)
|
||||
bindReply(item.reply)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.write
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.InputFilter
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelFantalkWriteBinding
|
||||
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class CreatorChannelFanTalkWriteActivity : BaseActivity<ActivityCreatorChannelFantalkWriteBinding>(
|
||||
ActivityCreatorChannelFantalkWriteBinding::inflate
|
||||
) {
|
||||
|
||||
private val viewModel: CreatorChannelFanTalkWriteViewModel by viewModel()
|
||||
private var creatorId: Long = 0L
|
||||
|
||||
override fun setupView() {
|
||||
creatorId = intent.getLongExtra(EXTRA_CREATOR_ID, 0L)
|
||||
if (creatorId <= 0L) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
setupContentInput()
|
||||
setupClickListeners()
|
||||
setupBackHandler()
|
||||
observeViewModel()
|
||||
focusContentInput()
|
||||
}
|
||||
|
||||
private fun setupContentInput() = with(binding.etCreatorChannelFantalkWriteContent) {
|
||||
filters = arrayOf(InputFilter.LengthFilter(CreatorChannelFanTalkWriteViewModel.MAX_CONTENT_LENGTH))
|
||||
}
|
||||
|
||||
private fun setupClickListeners() = with(binding) {
|
||||
tvCreatorChannelFantalkWriteCancel.setOnClickListener { handleCancel() }
|
||||
tvCreatorChannelFantalkWriteSend.setOnClickListener { viewModel.submit(creatorId) }
|
||||
etCreatorChannelFantalkWriteContent.addTextChangedListener { text ->
|
||||
viewModel.onContentChanged(text?.toString().orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupBackHandler() {
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
handleCancel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
viewModel.isSendEnabledLiveData.observe(this) { isEnabled ->
|
||||
bindSendEnabled(isEnabled && viewModel.isLoadingLiveData.value != true)
|
||||
}
|
||||
viewModel.contentLengthLiveData.observe(this) { length ->
|
||||
binding.tvCreatorChannelFantalkWriteCount.text = length.toString()
|
||||
}
|
||||
viewModel.isLoadingLiveData.observe(this) { isLoading ->
|
||||
bindLoading(isLoading)
|
||||
}
|
||||
viewModel.finishEventLiveData.observe(this) { shouldFinish ->
|
||||
if (shouldFinish == true) {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
viewModel.toastEventLiveData.observe(this) { message ->
|
||||
message?.let { Toast.makeText(this, it, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindSendEnabled(isEnabled: Boolean) = with(binding.tvCreatorChannelFantalkWriteSend) {
|
||||
this.isEnabled = isEnabled
|
||||
setTextColor(
|
||||
ContextCompat.getColor(
|
||||
context,
|
||||
if (isEnabled) R.color.white else R.color.gray_500
|
||||
)
|
||||
)
|
||||
setBackgroundResource(
|
||||
if (isEnabled) {
|
||||
R.drawable.bg_creator_channel_fantalk_write_send_enabled
|
||||
} else {
|
||||
R.drawable.bg_creator_channel_fantalk_write_send_disabled
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindLoading(isLoading: Boolean) = with(binding) {
|
||||
etCreatorChannelFantalkWriteContent.isEnabled = !isLoading
|
||||
tvCreatorChannelFantalkWriteSend.text = getString(
|
||||
if (isLoading) R.string.creator_channel_fantalk_write_sending else R.string.creator_channel_fantalk_write_send
|
||||
)
|
||||
bindSendEnabled(!isLoading && viewModel.isSendEnabledLiveData.value == true)
|
||||
}
|
||||
|
||||
private fun focusContentInput() {
|
||||
binding.etCreatorChannelFantalkWriteContent.post {
|
||||
binding.etCreatorChannelFantalkWriteContent.requestFocus()
|
||||
WindowCompat.getInsetsController(window, binding.etCreatorChannelFantalkWriteContent)
|
||||
.show(WindowInsetsCompat.Type.ime())
|
||||
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(binding.etCreatorChannelFantalkWriteContent, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCancel() {
|
||||
if (binding.etCreatorChannelFantalkWriteContent.text?.toString().orEmpty().trim().isEmpty()) {
|
||||
finish()
|
||||
} else {
|
||||
showCancelConfirmDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCancelConfirmDialog() {
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.creator_channel_fantalk_write_exit_title),
|
||||
desc = getString(R.string.creator_channel_fantalk_write_exit_desc),
|
||||
confirmButtonTitle = getString(R.string.creator_channel_fantalk_write_exit_confirm),
|
||||
confirmButtonClick = { finish() },
|
||||
cancelButtonTitle = getString(R.string.creator_channel_fantalk_write_exit_cancel),
|
||||
cancelButtonClick = {}
|
||||
).show(screenWidth)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_CREATOR_ID: String = "extra_creator_id"
|
||||
|
||||
fun newIntent(context: Context, creatorId: Long): Intent {
|
||||
return Intent(context, CreatorChannelFanTalkWriteActivity::class.java).apply {
|
||||
putExtra(EXTRA_CREATOR_ID, creatorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.write
|
||||
|
||||
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.v2.creator.channel.data.CreatorChannelRepository
|
||||
|
||||
class CreatorChannelFanTalkWriteViewModel(
|
||||
private val repository: CreatorChannelRepository
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val _contentLiveData = MutableLiveData("")
|
||||
val contentLiveData: LiveData<String>
|
||||
get() = _contentLiveData
|
||||
|
||||
private val _isSendEnabledLiveData = MutableLiveData(false)
|
||||
val isSendEnabledLiveData: LiveData<Boolean>
|
||||
get() = _isSendEnabledLiveData
|
||||
|
||||
private val _contentLengthLiveData = MutableLiveData(0)
|
||||
val contentLengthLiveData: LiveData<Int>
|
||||
get() = _contentLengthLiveData
|
||||
|
||||
private val _isLoadingLiveData = MutableLiveData(false)
|
||||
val isLoadingLiveData: LiveData<Boolean>
|
||||
get() = _isLoadingLiveData
|
||||
|
||||
private val _finishEventLiveData = MutableLiveData(false)
|
||||
val finishEventLiveData: LiveData<Boolean>
|
||||
get() = _finishEventLiveData
|
||||
|
||||
private val _toastEventLiveData = MutableLiveData<String?>()
|
||||
val toastEventLiveData: LiveData<String?>
|
||||
get() = _toastEventLiveData
|
||||
|
||||
fun onContentChanged(content: String) {
|
||||
val limitedContent = content.take(MAX_CONTENT_LENGTH)
|
||||
_contentLiveData.value = limitedContent
|
||||
_contentLengthLiveData.value = limitedContent.length
|
||||
_isSendEnabledLiveData.value = limitedContent.trim().isNotEmpty()
|
||||
}
|
||||
|
||||
fun submit(creatorId: Long) {
|
||||
val trimmedContent = _contentLiveData.value.orEmpty().trim()
|
||||
if (creatorId <= 0L) return
|
||||
if (_isLoadingLiveData.value == true) return
|
||||
if (trimmedContent.isEmpty()) return
|
||||
|
||||
_isLoadingLiveData.value = true
|
||||
compositeDisposable.add(
|
||||
repository.writeFanTalk(
|
||||
creatorId = creatorId,
|
||||
content = trimmedContent,
|
||||
token = "Bearer ${SharedPreferenceManager.token}"
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ response ->
|
||||
_isLoadingLiveData.value = false
|
||||
if (response.success) {
|
||||
_finishEventLiveData.value = true
|
||||
} else {
|
||||
_toastEventLiveData.value = response.message ?: unknownErrorMessage()
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
_isLoadingLiveData.value = false
|
||||
error.message?.let { Logger.e(it) }
|
||||
_toastEventLiveData.value = unknownErrorMessage()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun unknownErrorMessage(): String {
|
||||
return SodaLiveApplicationHolder.get().getString(R.string.common_error_unknown)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_CONTENT_LENGTH: Int = 500
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.ui
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorRes
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
|
||||
internal fun bindCreatorChannelDonationCard(
|
||||
headerView: View,
|
||||
profileView: ImageView,
|
||||
nicknameView: TextView,
|
||||
createdAtView: TextView,
|
||||
canView: TextView,
|
||||
messageView: TextView,
|
||||
@ColorRes headerColorResId: Int,
|
||||
profileImageUrl: String,
|
||||
nickname: String,
|
||||
createdAtText: String,
|
||||
can: Int,
|
||||
message: String
|
||||
) {
|
||||
val context = headerView.context
|
||||
headerView.setBackgroundColor(context.getColor(headerColorResId))
|
||||
profileView.loadUrl(profileImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.ic_placeholder_profile)
|
||||
error(R.drawable.ic_placeholder_profile)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
nicknameView.text = nickname
|
||||
createdAtView.text = createdAtText
|
||||
canView.text = context.getString(R.string.creator_channel_donation_can_format, can.moneyFormat())
|
||||
messageView.text = message
|
||||
if (headerColorResId == R.color.red_400) {
|
||||
nicknameView.setTextColor(context.getColor(R.color.white))
|
||||
createdAtView.setTextColor(context.getColor(R.color.gray_100))
|
||||
} else {
|
||||
nicknameView.setTextColor(context.getColor(R.color.black))
|
||||
createdAtView.setTextColor(context.getColor(R.color.gray_700))
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,18 @@ import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import coil.transform.CircleCropTransformation
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
|
||||
import kr.co.vividnext.sodalive.common.image.BlurTransformation
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelCommunityPostResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelLiveResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelSeriesResponse
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeSection
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedSize
|
||||
@@ -40,7 +40,9 @@ class CreatorChannelHomeSectionAdapter(
|
||||
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit = {},
|
||||
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit = {},
|
||||
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit = {},
|
||||
private val onDonationClick: () -> Unit = {}
|
||||
private val onDonationClick: () -> Unit = {},
|
||||
private val onSectionChevronClick: (CreatorChannelTab) -> Unit = {},
|
||||
private val onCommunityClick: (Long) -> Unit = {}
|
||||
) : RecyclerView.Adapter<CreatorChannelHomeSectionAdapter.SectionViewHolder>() {
|
||||
|
||||
private var items: List<CreatorChannelHomeSection> = emptyList()
|
||||
@@ -54,7 +56,16 @@ class CreatorChannelHomeSectionAdapter(
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||
return SectionViewHolder(view, onLiveClick, onScheduleClick, onAudioContentClick, onSeriesClick, onDonationClick)
|
||||
return SectionViewHolder(
|
||||
view,
|
||||
onLiveClick,
|
||||
onScheduleClick,
|
||||
onAudioContentClick,
|
||||
onSeriesClick,
|
||||
onDonationClick,
|
||||
onSectionChevronClick,
|
||||
onCommunityClick
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SectionViewHolder, position: Int) {
|
||||
@@ -69,9 +80,12 @@ class CreatorChannelHomeSectionAdapter(
|
||||
private val onScheduleClick: (CreatorChannelScheduleResponse) -> Unit,
|
||||
private val onAudioContentClick: (CreatorChannelAudioContentResponse) -> Unit,
|
||||
private val onSeriesClick: (CreatorChannelSeriesResponse) -> Unit,
|
||||
private val onDonationClick: () -> Unit
|
||||
private val onDonationClick: () -> Unit,
|
||||
private val onSectionChevronClick: (CreatorChannelTab) -> Unit,
|
||||
private val onCommunityClick: (Long) -> Unit
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
private val title: TextView? = view.findViewById(R.id.tv_section_title)
|
||||
private val sectionTitleChevron: ImageView? = view.findViewById(R.id.iv_section_title_chevron)
|
||||
private val currentLiveTitle: TextView? = view.findViewById(R.id.tv_current_live_title)
|
||||
private val currentLiveStartTime: TextView? = view.findViewById(R.id.tv_current_live_start_time)
|
||||
private val currentLivePrice: TextView? = view.findViewById(R.id.tv_current_live_price)
|
||||
@@ -94,6 +108,7 @@ class CreatorChannelHomeSectionAdapter(
|
||||
private val audioContentsRecyclerView: RecyclerView? = view.findViewById(R.id.rv_audio_contents)
|
||||
private val seriesItems: LinearLayout? = view.findViewById(R.id.ll_series_items)
|
||||
private val communityItems: LinearLayout? = view.findViewById(R.id.ll_community_items)
|
||||
private val communityMoreButton: View? = view.findViewById(R.id.layout_community_more_button)
|
||||
private val fanTalkCard: View? = view.findViewById(R.id.layout_fantalk_card)
|
||||
private val fanTalkTotalRow: View? = view.findViewById(R.id.layout_fantalk_total_row)
|
||||
private val fanTalkLatestRow: View? = view.findViewById(R.id.layout_fantalk_latest_row)
|
||||
@@ -121,6 +136,11 @@ class CreatorChannelHomeSectionAdapter(
|
||||
fun bind(item: CreatorChannelHomeSection) {
|
||||
itemView.setOnClickListener(null)
|
||||
title?.setText(item.titleResId)
|
||||
sectionTitleChevron?.isVisible = item.chevronTab != null
|
||||
sectionTitleChevron?.setOnClickListener {
|
||||
item.chevronTab?.let(onSectionChevronClick)
|
||||
}
|
||||
communityMoreButton?.setOnClickListener(null)
|
||||
donationItems?.removeAllViews()
|
||||
noticeItems?.removeAllViews()
|
||||
scheduleTimeline?.removeAllViews()
|
||||
@@ -195,22 +215,24 @@ class CreatorChannelHomeSectionAdapter(
|
||||
).apply {
|
||||
marginEnd = if (index == visibleDonations.lastIndex) 0 else 4.dp()
|
||||
}
|
||||
row.findViewById<View>(R.id.layout_donation_header)
|
||||
.setBackgroundColor(itemView.context.getColor(calculateCreatorChannelDonationHeaderColorRes(donation.can)))
|
||||
row.findViewById<ImageView>(R.id.iv_donation_profile).loadUrl(donation.profileImageUrl) {
|
||||
placeholder(R.drawable.ic_placeholder_profile)
|
||||
transformations(CircleCropTransformation())
|
||||
}
|
||||
row.findViewById<TextView>(R.id.tv_donation_nickname).text = donation.nickname
|
||||
row.findViewById<TextView>(R.id.tv_donation_created_at).text =
|
||||
formatUtcRelativeTimeText(itemView.context, donation.createdAtUtc)
|
||||
row.findViewById<TextView>(R.id.tv_donation_can).text = itemView.context.getString(
|
||||
R.string.creator_channel_donation_can_format,
|
||||
donation.can.moneyFormat()
|
||||
)
|
||||
row.findViewById<TextView>(R.id.tv_donation_message).text = donation.message.ifBlank {
|
||||
val headerColorResId = calculateCreatorChannelDonationHeaderColorRes(donation.can)
|
||||
val donationMessage = donation.message.ifBlank {
|
||||
itemView.context.getString(R.string.creator_channel_donation_fallback_message, donation.can)
|
||||
}
|
||||
bindCreatorChannelDonationCard(
|
||||
headerView = row.findViewById(R.id.layout_donation_header),
|
||||
profileView = row.findViewById(R.id.iv_donation_profile),
|
||||
nicknameView = row.findViewById(R.id.tv_donation_nickname),
|
||||
createdAtView = row.findViewById(R.id.tv_donation_created_at),
|
||||
canView = row.findViewById(R.id.tv_donation_can),
|
||||
messageView = row.findViewById(R.id.tv_donation_message),
|
||||
headerColorResId = headerColorResId,
|
||||
profileImageUrl = donation.profileImageUrl,
|
||||
nickname = donation.nickname,
|
||||
createdAtText = formatUtcRelativeTimeText(itemView.context, donation.createdAtUtc),
|
||||
can = donation.can,
|
||||
message = donationMessage
|
||||
)
|
||||
donationItems?.addView(row)
|
||||
}
|
||||
}
|
||||
@@ -350,6 +372,7 @@ class CreatorChannelHomeSectionAdapter(
|
||||
}
|
||||
|
||||
private fun bindCommunities(item: CreatorChannelHomeSection.Communities) {
|
||||
communityMoreButton?.setOnClickListener { onSectionChevronClick(CreatorChannelTab.Community) }
|
||||
val visibleCommunities = item.communities.take(MAX_COMMUNITY_ITEM_COUNT)
|
||||
visibleCommunities.forEachIndexed { index, community ->
|
||||
val communityWidthDp = calculateCreatorChannelCommunityCardWidthDp(
|
||||
@@ -376,6 +399,7 @@ class CreatorChannelHomeSectionAdapter(
|
||||
).apply {
|
||||
bottomMargin = if (index == visibleCommunities.lastIndex) 0 else 8.dp()
|
||||
}
|
||||
row.setOnClickListener { onCommunityClick(community.postId) }
|
||||
communityItems?.addView(row)
|
||||
}
|
||||
}
|
||||
@@ -589,6 +613,15 @@ class CreatorChannelHomeSectionAdapter(
|
||||
is CreatorChannelHomeSection.Sns -> R.string.creator_channel_section_sns
|
||||
}
|
||||
|
||||
val CreatorChannelHomeSection.chevronTab: CreatorChannelTab?
|
||||
get() = when (this) {
|
||||
is CreatorChannelHomeSection.Donations -> CreatorChannelTab.Donation
|
||||
is CreatorChannelHomeSection.AudioContents -> CreatorChannelTab.Audio
|
||||
is CreatorChannelHomeSection.Series -> CreatorChannelTab.Series
|
||||
is CreatorChannelHomeSection.FanTalk -> CreatorChannelTab.FanTalk
|
||||
else -> null
|
||||
}
|
||||
|
||||
private const val MAX_DONATION_ITEM_COUNT = 8
|
||||
private const val MAX_NOTICE_ITEM_COUNT = 3
|
||||
private const val MAX_SCHEDULE_ITEM_COUNT = 3
|
||||
@@ -678,7 +711,7 @@ private const val SNS_TOTAL_GAP_DP = 64
|
||||
|
||||
@ColorRes
|
||||
internal fun calculateCreatorChannelDonationHeaderColorRes(can: Int): Int = when {
|
||||
can >= 500 -> R.color.red_400
|
||||
can >= 501 -> R.color.red_400
|
||||
can >= 101 -> R.color.creator_channel_donation_cyan
|
||||
can >= 51 -> R.color.green_400
|
||||
else -> R.color.gray_200
|
||||
|
||||
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.v2.live.onair
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
@@ -14,7 +13,6 @@ import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
|
||||
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
@@ -34,6 +32,7 @@ import kr.co.vividnext.sodalive.settings.language.LanguageManager
|
||||
import kr.co.vividnext.sodalive.settings.language.LocaleHelper
|
||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||
import kr.co.vividnext.sodalive.user.login.LoginActivity
|
||||
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||
import kr.co.vividnext.sodalive.v2.live.onair.model.HomeOnAirLivePageUiState
|
||||
import kr.co.vividnext.sodalive.v2.live.onair.model.canEnterHomeOnAirLiveRoom
|
||||
import kr.co.vividnext.sodalive.v2.live.onair.ui.HomeOnAirLiveAdapter
|
||||
@@ -183,7 +182,7 @@ class HomeOnAirLiveActivity : BaseActivity<ActivityHomeOnAirLiveBinding>(
|
||||
if (isAdult) {
|
||||
val isKoreanCountry = SharedPreferenceManager.countryCode.ifBlank { "KR" } == "KR"
|
||||
if (isKoreanCountry && !SharedPreferenceManager.isAuth) {
|
||||
SodaDialog(
|
||||
V2ModalDialog(
|
||||
activity = this,
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.auth_title),
|
||||
@@ -191,8 +190,7 @@ class HomeOnAirLiveActivity : BaseActivity<ActivityHomeOnAirLiveBinding>(
|
||||
confirmButtonTitle = getString(R.string.auth_go),
|
||||
confirmButtonClick = { startAuthFlow() },
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {},
|
||||
descGravity = Gravity.CENTER
|
||||
cancelButtonClick = {}
|
||||
).show(screenWidth)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package kr.co.vividnext.sodalive.v2.main
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@@ -39,6 +42,7 @@ import kr.co.vividnext.sodalive.audition.AuditionActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.databinding.ActivityMainV2Binding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
|
||||
import kr.co.vividnext.sodalive.main.EventPopupDialogFragment
|
||||
@@ -72,6 +76,7 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
|
||||
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val showMiniPlayerRunnable = Runnable { initAndVisibleMiniPlayer() }
|
||||
private val audioContentReceiver = AudioContentReceiver()
|
||||
private var playerStateJob: Job? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -82,11 +87,16 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
|
||||
trackAppLaunchIfNeeded()
|
||||
pushTokenUpdate()
|
||||
|
||||
handler.postDelayed({
|
||||
if (!handleAudioNotificationRoute(intent) && isLoggedIn()) {
|
||||
executeDeeplink(intent)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
if (isLoggedIn()) {
|
||||
updatePidAndGaid()
|
||||
getEventPopup()
|
||||
observePlayerState()
|
||||
handler.postDelayed({ executeDeeplink(intent) }, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,14 +124,22 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
|
||||
return
|
||||
}
|
||||
|
||||
if (isLoggedIn()) {
|
||||
if (!handleAudioNotificationRoute(intent) && isLoggedIn()) {
|
||||
executeDeeplink(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
getMemberInfo()
|
||||
val intentFilter = IntentFilter(Constants.ACTION_MAIN_AUDIO_CONTENT_RECEIVER)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(audioContentReceiver, intentFilter, RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
registerReceiver(audioContentReceiver, intentFilter)
|
||||
}
|
||||
|
||||
startService(
|
||||
Intent(this, AudioContentPlayService::class.java).apply {
|
||||
action = AudioContentPlayService.MusicAction.INIT.name
|
||||
@@ -129,6 +147,11 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
unregisterReceiver(audioContentReceiver)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
deInitMiniPlayer()
|
||||
playerStateJob?.cancel()
|
||||
@@ -405,6 +428,39 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
|
||||
playerFragment.show(supportFragmentManager, playerFragment.tag)
|
||||
}
|
||||
|
||||
private fun handleAudioNotificationRoute(intent: Intent): Boolean {
|
||||
val route = intent.getStringExtra(EXTRA_AUDIO_NOTIFICATION_ROUTE) ?: return false
|
||||
intent.removeExtra(EXTRA_AUDIO_NOTIFICATION_ROUTE)
|
||||
|
||||
return when (route) {
|
||||
ROUTE_AUDIO_PLAYER -> {
|
||||
if (isLoggedIn()) {
|
||||
showPlayerFragment()
|
||||
} else {
|
||||
showLoginActivity()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
ROUTE_AUDIO_DETAIL -> {
|
||||
val contentId = intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
|
||||
intent.removeExtra(Constants.EXTRA_AUDIO_CONTENT_ID)
|
||||
if (contentId > 0 && isLoggedIn()) {
|
||||
startActivity(
|
||||
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
|
||||
}
|
||||
)
|
||||
} else if (!isLoggedIn()) {
|
||||
showLoginActivity()
|
||||
}
|
||||
contentId > 0
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermissions() {
|
||||
val permissions = mutableListOf(Manifest.permission.RECORD_AUDIO)
|
||||
|
||||
@@ -734,8 +790,90 @@ class MainV2Activity : BaseActivity<ActivityMainV2Binding>(ActivityMainV2Binding
|
||||
return SharedPreferenceManager.token.isNotBlank() && SharedPreferenceManager.token.length > 10
|
||||
}
|
||||
|
||||
inner class AudioContentReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val contentId = intent?.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0)
|
||||
val title = intent?.getStringExtra(Constants.EXTRA_AUDIO_CONTENT_TITLE)
|
||||
val nickname = intent?.getStringExtra(Constants.EXTRA_NICKNAME)
|
||||
val coverImageUrl = intent?.getStringExtra(
|
||||
Constants.EXTRA_AUDIO_CONTENT_COVER_IMAGE_URL
|
||||
)
|
||||
val isPlaying = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_PLAYING, false)
|
||||
val isShowing = intent?.getBooleanExtra(Constants.EXTRA_AUDIO_CONTENT_SHOWING, false)
|
||||
|
||||
if (isShowing == true) {
|
||||
binding.clMiniPlayer.visibility = View.VISIBLE
|
||||
if (contentId != null && contentId > 0) {
|
||||
binding.clMiniPlayer.setOnClickListener {
|
||||
startActivity(
|
||||
Intent(applicationContext, AudioContentDetailActivity::class.java).apply {
|
||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isPlaying == true) {
|
||||
binding.ivPlayerPlayOrPause.setImageResource(R.drawable.ic_noti_pause)
|
||||
binding.ivPlayerPlayOrPause.setOnClickListener {
|
||||
startService(
|
||||
Intent(this@MainV2Activity, AudioContentPlayService::class.java).apply {
|
||||
action = AudioContentPlayService.MusicAction.PAUSE.name
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
binding.ivPlayerPlayOrPause.setImageResource(R.drawable.ic_noti_play)
|
||||
binding.ivPlayerPlayOrPause.setOnClickListener {
|
||||
startService(
|
||||
Intent(this@MainV2Activity, AudioContentPlayService::class.java).apply {
|
||||
action = AudioContentPlayService.MusicAction.PLAY.name
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
binding.ivPlayerStop.setOnClickListener {
|
||||
startService(
|
||||
Intent(this@MainV2Activity, AudioContentPlayService::class.java).apply {
|
||||
action = AudioContentPlayService.MusicAction.STOP.name
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!title.isNullOrBlank()) {
|
||||
binding.tvPlayerTitle.text = title
|
||||
}
|
||||
|
||||
if (!nickname.isNullOrBlank()) {
|
||||
binding.tvPlayerNickname.text = nickname
|
||||
}
|
||||
|
||||
if (!coverImageUrl.isNullOrBlank()) {
|
||||
binding.ivPlayerCover.load(coverImageUrl) {
|
||||
crossfade(true)
|
||||
placeholder(R.drawable.bg_placeholder)
|
||||
transformations(RoundedCornersTransformation(5.3f.dpToPx()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handler.post {
|
||||
binding.ivPlayerPlayOrPause.setImageResource(0)
|
||||
binding.ivPlayerCover.setImageResource(0)
|
||||
binding.tvPlayerTitle.text = ""
|
||||
binding.tvPlayerNickname.text = ""
|
||||
binding.clMiniPlayer.visibility = View.GONE
|
||||
binding.ivPlayerPlayOrPause.setOnClickListener {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_CHAT_FILTER: String = "extra_chat_filter"
|
||||
const val EXTRA_AUDIO_NOTIFICATION_ROUTE: String = "extra_audio_notification_route"
|
||||
const val ROUTE_AUDIO_PLAYER: String = "audio_player"
|
||||
const val ROUTE_AUDIO_DETAIL: String = "audio_detail"
|
||||
|
||||
fun newChatDmIntent(context: Context): Intent {
|
||||
return Intent(context, MainV2Activity::class.java).apply {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.v2.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.view.Gravity
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.gson.Gson
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.base.SodaDialog
|
||||
import kr.co.vividnext.sodalive.common.Constants
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.mypage.MyPageViewModel
|
||||
@@ -14,6 +12,7 @@ import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest
|
||||
import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse
|
||||
import kr.co.vividnext.sodalive.settings.ContentSettingsActivity
|
||||
import kr.co.vividnext.sodalive.splash.SplashActivity
|
||||
import kr.co.vividnext.sodalive.v2.components.modal.V2ModalDialog
|
||||
import org.koin.android.ext.android.get
|
||||
|
||||
fun Fragment.ensureMainV2NavigationAllowed(
|
||||
@@ -46,7 +45,7 @@ fun Fragment.ensureMainV2NavigationAllowed(
|
||||
}
|
||||
|
||||
private fun Fragment.showMainV2AuthDialog() {
|
||||
SodaDialog(
|
||||
V2ModalDialog(
|
||||
activity = requireActivity(),
|
||||
layoutInflater = layoutInflater,
|
||||
title = getString(R.string.auth_title),
|
||||
@@ -54,8 +53,7 @@ private fun Fragment.showMainV2AuthDialog() {
|
||||
confirmButtonTitle = getString(R.string.auth_go),
|
||||
confirmButtonClick = { startMainV2AuthFlow() },
|
||||
cancelButtonTitle = getString(R.string.cancel),
|
||||
cancelButtonClick = {},
|
||||
descGravity = Gravity.CENTER
|
||||
cancelButtonClick = {}
|
||||
).show(resources.displayMetrics.widthPixels)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.chat
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
@@ -12,6 +13,9 @@ import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainChatBinding
|
||||
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
||||
import kr.co.vividnext.sodalive.search.SearchActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.ensureMainV2NavigationAllowed
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomFilter
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem
|
||||
@@ -63,8 +67,11 @@ class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
|
||||
private fun setupTitleBar() {
|
||||
binding.viewChatTitleBar.tvTitleBarTitle.setText(R.string.tab_chat)
|
||||
binding.viewChatTitleBar.ivTitleBarMenu.setImageResource(R.drawable.ic_bar_cash)
|
||||
binding.viewChatTitleBar.llTitleBarActions.addView(
|
||||
ImageView(requireContext()).apply {
|
||||
binding.viewChatTitleBar.ivTitleBarMenu.setOnClickListener {
|
||||
openWithLoginGuard(CanChargeActivity::class.java)
|
||||
}
|
||||
|
||||
val searchAction = ImageView(requireContext()).apply {
|
||||
setImageResource(R.drawable.ic_bar_search)
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
@@ -73,7 +80,16 @@ class ChatMainFragment : BaseFragment<FragmentV2MainChatBinding>(
|
||||
marginStart = resources.getDimensionPixelSize(R.dimen.spacing_14)
|
||||
}
|
||||
}
|
||||
)
|
||||
searchAction.setOnClickListener {
|
||||
openWithLoginGuard(SearchActivity::class.java)
|
||||
}
|
||||
binding.viewChatTitleBar.llTitleBarActions.addView(searchAction)
|
||||
}
|
||||
|
||||
private fun openWithLoginGuard(activityClass: Class<*>) {
|
||||
ensureMainV2NavigationAllowed {
|
||||
startActivity(Intent(requireContext(), activityClass))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupFilterTabs(initialFilter: ChatRoomFilter) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.audio_content.box.AudioContentBoxActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||
@@ -23,6 +24,8 @@ import kr.co.vividnext.sodalive.databinding.FragmentV2MainContentBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
|
||||
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
||||
import kr.co.vividnext.sodalive.search.SearchActivity
|
||||
import kr.co.vividnext.sodalive.v2.common.data.ContentSort
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.model.toLabelResId
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelSortPopup
|
||||
@@ -51,6 +54,7 @@ import kr.co.vividnext.sodalive.v2.main.content.model.usesSeriesItems
|
||||
import kr.co.vividnext.sodalive.v2.main.content.overview.ContentOverviewActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewType
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_ALL_GRID_SPAN_COUNT
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_RECOMMENDED_GRID_GAP_DP
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_RECOMMENDED_GRID_SPAN_COUNT
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllAudioCardAdapter
|
||||
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllSeriesCardAdapter
|
||||
@@ -113,6 +117,7 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
setUpAllTypeTabs()
|
||||
setUpAllDayFilter()
|
||||
setUpAllSortButton()
|
||||
setupTitleBarActions()
|
||||
showContentTab(CONTENT_TAB_RECOMMENDATION)
|
||||
setUpSectionTitles()
|
||||
setUpAdapters()
|
||||
@@ -129,6 +134,24 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setupTitleBarActions() {
|
||||
binding.viewContentTitleBar.ivTitleBarCash.setOnClickListener {
|
||||
openWithLoginGuard(CanChargeActivity::class.java)
|
||||
}
|
||||
binding.viewContentTitleBar.ivTitleBarSearch.setOnClickListener {
|
||||
openWithLoginGuard(SearchActivity::class.java)
|
||||
}
|
||||
binding.viewContentTitleBar.ivTitleBarStorage.setOnClickListener {
|
||||
openWithLoginGuard(AudioContentBoxActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openWithLoginGuard(activityClass: Class<*>) {
|
||||
ensureMainV2NavigationAllowed {
|
||||
startActivity(Intent(requireContext(), activityClass))
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAllTab(type: MainContentAllType, sort: ContentSort = ContentSort.LATEST) {
|
||||
pendingAllTabSelection = ContentAllTabSelection(type, sort)
|
||||
if (view != null) {
|
||||
@@ -296,7 +319,8 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
binding.rvContentRecommendedAudios.apply {
|
||||
layoutManager = GridLayoutManager(requireContext(), CONTENT_RECOMMENDED_GRID_SPAN_COUNT)
|
||||
adapter = recommendedAudioAdapter
|
||||
addContentGridItemSpacing()
|
||||
addContentGridItemSpacing(gapDp = CONTENT_RECOMMENDED_GRID_GAP_DP)
|
||||
doOnLayout { updateRecommendedAudioGridItemWidth() }
|
||||
}
|
||||
binding.rvContentRankings.apply {
|
||||
layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext())
|
||||
@@ -401,6 +425,14 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRecommendedAudioGridItemWidth() {
|
||||
val widthPx = binding.rvContentRecommendedAudios.calculateContentGridItemWidthPx(
|
||||
CONTENT_RECOMMENDED_GRID_SPAN_COUNT,
|
||||
gapDp = CONTENT_RECOMMENDED_GRID_GAP_DP
|
||||
)
|
||||
recommendedAudioAdapter.setGridItemWidthPx(widthPx)
|
||||
}
|
||||
|
||||
private fun updateAllTabGridItemWidth() {
|
||||
val widthPx = binding.rvContentAllItems.calculateContentGridItemWidthPx(CONTENT_ALL_GRID_SPAN_COUNT)
|
||||
contentAllAudioCardAdapter.setGridItemWidthPx(widthPx)
|
||||
@@ -524,6 +556,7 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
|
||||
|
||||
private fun bindRecommendedAudioSection(section: ContentAudioCardSection) {
|
||||
binding.llContentRecommendedAudioSection.visibility = section.items.toSectionVisibility()
|
||||
updateRecommendedAudioGridItemWidth()
|
||||
recommendedAudioAdapter.submitItems(section.items)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@ interface AudioRankingsApi {
|
||||
@GET("/api/v2/audio/rankings")
|
||||
fun getRankings(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Query("type") type: AudioRankingType
|
||||
@Query("type") type: String
|
||||
): Single<ApiResponse<AudioRankingResponse>>
|
||||
}
|
||||
|
||||
@@ -14,11 +14,22 @@ enum class AudioRankingType(
|
||||
val queryValue: String,
|
||||
val label: String
|
||||
) {
|
||||
@SerializedName("WEEKLY_POPULAR")
|
||||
WEEKLY_POPULAR("WEEKLY_POPULAR", "주간 인기"),
|
||||
|
||||
@SerializedName("RISING")
|
||||
RISING("RISING", "지금 뜨는 중"),
|
||||
|
||||
@SerializedName("REVENUE")
|
||||
REVENUE("REVENUE", "매출"),
|
||||
|
||||
@SerializedName("SALES_COUNT")
|
||||
SALES_COUNT("SALES_COUNT", "판매량"),
|
||||
|
||||
@SerializedName("COMMENT_COUNT")
|
||||
COMMENT_COUNT("COMMENT_COUNT", "댓글수"),
|
||||
|
||||
@SerializedName("LIKE_COUNT")
|
||||
LIKE_COUNT("LIKE_COUNT", "좋아요")
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@ package kr.co.vividnext.sodalive.v2.main.content.data
|
||||
class AudioRankingsRepository(private val api: AudioRankingsApi) {
|
||||
fun getRankings(token: String, type: AudioRankingType) = api.getRankings(
|
||||
authHeader = token,
|
||||
type = type
|
||||
type = type.queryValue
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package kr.co.vividnext.sodalive.v2.main.content.data
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek
|
||||
import kr.co.vividnext.sodalive.v2.common.data.ContentSort
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.Query
|
||||
@@ -12,10 +10,10 @@ interface MainContentAllTabApi {
|
||||
@GET("/api/v2/audio/contents")
|
||||
fun getContents(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Query("type") type: MainContentAllType,
|
||||
@Query("sort") sort: ContentSort,
|
||||
@Query("type") type: String,
|
||||
@Query("sort") sort: String,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek?
|
||||
@Query("dayOfWeek") dayOfWeek: String?
|
||||
): Single<ApiResponse<MainContentAllTabResponse>>
|
||||
}
|
||||
|
||||
@@ -18,12 +18,22 @@ data class MainContentAllTabResponse(
|
||||
@SerializedName("hasNext") val hasNext: Boolean
|
||||
)
|
||||
|
||||
enum class MainContentAllType {
|
||||
AUDIO,
|
||||
SERIES,
|
||||
ORIGINAL,
|
||||
FREE,
|
||||
POINT
|
||||
@Keep
|
||||
enum class MainContentAllType(val queryValue: String) {
|
||||
@SerializedName("AUDIO")
|
||||
AUDIO("AUDIO"),
|
||||
|
||||
@SerializedName("SERIES")
|
||||
SERIES("SERIES"),
|
||||
|
||||
@SerializedName("ORIGINAL")
|
||||
ORIGINAL("ORIGINAL"),
|
||||
|
||||
@SerializedName("FREE")
|
||||
FREE("FREE"),
|
||||
|
||||
@SerializedName("POINT")
|
||||
POINT("POINT")
|
||||
}
|
||||
|
||||
@Keep
|
||||
|
||||
@@ -13,10 +13,10 @@ class MainContentAllTabRepository(private val api: MainContentAllTabApi) {
|
||||
dayOfWeek: SeriesPublishedDaysOfWeek?
|
||||
) = api.getContents(
|
||||
authHeader = token,
|
||||
type = type,
|
||||
sort = sort,
|
||||
type = type.queryValue,
|
||||
sort = sort.queryValue,
|
||||
page = page,
|
||||
size = size,
|
||||
dayOfWeek = dayOfWeek
|
||||
dayOfWeek = dayOfWeek?.queryValue
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ interface ContentOverviewApi {
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Query("page") page: Int,
|
||||
@Query("size") size: Int,
|
||||
@Query("type") type: ContentOverviewType
|
||||
@Query("type") type: String
|
||||
): Single<ApiResponse<ContentOverviewPageResponse>>
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ data class ContentOverviewPageResponse(
|
||||
@SerializedName("hasNext") val hasNext: Boolean
|
||||
)
|
||||
|
||||
enum class ContentOverviewType {
|
||||
NEW_AND_HOT_AUDIO
|
||||
@Keep
|
||||
enum class ContentOverviewType(val queryValue: String) {
|
||||
@SerializedName("NEW_AND_HOT_AUDIO")
|
||||
NEW_AND_HOT_AUDIO("NEW_AND_HOT_AUDIO")
|
||||
}
|
||||
|
||||
@Keep
|
||||
|
||||
@@ -10,6 +10,6 @@ class ContentOverviewRepository(private val api: ContentOverviewApi) {
|
||||
authHeader = token,
|
||||
page = page,
|
||||
size = size,
|
||||
type = type
|
||||
type = type.queryValue
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,10 +14,35 @@ class ContentAudioCardAdapter(
|
||||
) : RecyclerView.Adapter<ContentAudioCardAdapter.ViewHolder>() {
|
||||
|
||||
private var items: List<ContentAudioCardUiModel> = emptyList()
|
||||
private var gridItemWidthPx: Int = 0
|
||||
|
||||
fun setGridItemWidthPx(widthPx: Int) {
|
||||
if (widthPx <= 0 || gridItemWidthPx == widthPx) return
|
||||
gridItemWidthPx = widthPx
|
||||
if (items.isNotEmpty()) notifyItemRangeChanged(0, items.size)
|
||||
}
|
||||
|
||||
fun submitItems(items: List<ContentAudioCardUiModel>) {
|
||||
val previousItemCount = this.items.size
|
||||
this.items = items
|
||||
notifyDataSetChanged()
|
||||
notifyItemChanges(previousItemCount, items.size)
|
||||
}
|
||||
|
||||
private fun notifyItemChanges(previousItemCount: Int, currentItemCount: Int) {
|
||||
when {
|
||||
previousItemCount == 0 && currentItemCount > 0 -> notifyItemRangeInserted(0, currentItemCount)
|
||||
previousItemCount > 0 && currentItemCount == 0 -> notifyItemRangeRemoved(0, previousItemCount)
|
||||
previousItemCount == currentItemCount -> notifyItemRangeChanged(0, currentItemCount)
|
||||
else -> {
|
||||
val changedItemCount = minOf(previousItemCount, currentItemCount)
|
||||
if (changedItemCount > 0) notifyItemRangeChanged(0, changedItemCount)
|
||||
if (currentItemCount > previousItemCount) {
|
||||
notifyItemRangeInserted(previousItemCount, currentItemCount - previousItemCount)
|
||||
} else {
|
||||
notifyItemRangeRemoved(currentItemCount, previousItemCount - currentItemCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
@@ -29,7 +54,10 @@ class ContentAudioCardAdapter(
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
holder.bind(
|
||||
item = items[position],
|
||||
gridItemWidthPx = gridItemWidthPx
|
||||
)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
@@ -39,8 +67,12 @@ class ContentAudioCardAdapter(
|
||||
private val cardSize: AudioContentCardSize,
|
||||
private val onAudioClick: (ContentAudioCardUiModel) -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: ContentAudioCardUiModel) = with(binding.audioContentCard.root) {
|
||||
fun bind(item: ContentAudioCardUiModel, gridItemWidthPx: Int) = with(binding.audioContentCard.root) {
|
||||
if (gridItemWidthPx > 0) {
|
||||
setGridItemWidthPx(gridItemWidthPx)
|
||||
} else {
|
||||
setSize(cardSize)
|
||||
}
|
||||
setContent(item.title, item.creatorNickname)
|
||||
setTags(item.tags)
|
||||
setAdultVisible(item.showAdultBadge)
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.databinding.ItemContentCommentedAudioBinding
|
||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.v2.main.content.model.ContentCommentedAudioUiModel
|
||||
|
||||
@@ -24,8 +25,11 @@ class ContentCommentedAudioAdapter(
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding = ItemContentCommentedAudioBinding.inflate(LayoutInflater.from(parent.context), parent, false).apply {
|
||||
root.layoutParams.width = parent.resources.displayMetrics.widthPixels - (CONTENT_HORIZONTAL_MARGIN_DP * 2).dpToPx().toInt()
|
||||
}
|
||||
return ViewHolder(
|
||||
ItemContentCommentedAudioBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
binding,
|
||||
onAudioClick
|
||||
)
|
||||
}
|
||||
@@ -47,6 +51,12 @@ class ContentCommentedAudioAdapter(
|
||||
outline.setRoundRect(0, 0, view.width, view.height, view.resources.getDimension(R.dimen.radius_14))
|
||||
}
|
||||
}
|
||||
binding.ivContentCommentProfile.clipToOutline = true
|
||||
binding.ivContentCommentProfile.outlineProvider = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setOval(0, 0, view.width, view.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: ContentCommentedAudioUiModel) = with(binding) {
|
||||
@@ -58,4 +68,8 @@ class ContentCommentedAudioAdapter(
|
||||
root.setOnClickListener { onAudioClick(item) }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CONTENT_HORIZONTAL_MARGIN_DP = 14
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,20 @@ fun RecyclerView.addContentHorizontalItemSpacing() {
|
||||
if (itemDecorationCount == 0) addItemDecoration(ContentHorizontalItemDecoration())
|
||||
}
|
||||
|
||||
fun RecyclerView.addContentGridItemSpacing(spanCount: Int = CONTENT_RECOMMENDED_GRID_SPAN_COUNT) {
|
||||
if (itemDecorationCount == 0) addItemDecoration(ContentGridItemDecoration(spanCount))
|
||||
fun RecyclerView.addContentGridItemSpacing(
|
||||
spanCount: Int = CONTENT_RECOMMENDED_GRID_SPAN_COUNT,
|
||||
gapDp: Int = GRID_ITEM_GAP_DP
|
||||
) {
|
||||
if (itemDecorationCount == 0) addItemDecoration(ContentGridItemDecoration(spanCount, gapDp))
|
||||
}
|
||||
|
||||
fun RecyclerView.calculateContentGridItemWidthPx(spanCount: Int): Int {
|
||||
fun RecyclerView.calculateContentGridItemWidthPx(
|
||||
spanCount: Int,
|
||||
gapDp: Int = GRID_ITEM_GAP_DP
|
||||
): Int {
|
||||
val availableWidth = measuredWidth - paddingLeft - paddingRight
|
||||
if (availableWidth <= 0 || spanCount <= 0) return 0
|
||||
val totalGap = GRID_ITEM_GAP_DP.dpToPx() * (spanCount - 1)
|
||||
val totalGap = gapDp.dpToPx() * (spanCount - 1)
|
||||
return ((availableWidth - totalGap) / spanCount).roundToInt()
|
||||
}
|
||||
|
||||
@@ -31,13 +37,14 @@ private class ContentHorizontalItemDecoration : RecyclerView.ItemDecoration() {
|
||||
}
|
||||
|
||||
private class ContentGridItemDecoration(
|
||||
private val spanCount: Int
|
||||
private val spanCount: Int,
|
||||
private val gapDp: Int
|
||||
) : RecyclerView.ItemDecoration() {
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
if (position == RecyclerView.NO_POSITION) return
|
||||
val columnIndex = position % spanCount
|
||||
val gap = GRID_ITEM_GAP_DP.dpToPx()
|
||||
val gap = gapDp.dpToPx()
|
||||
outRect.left = (columnIndex * gap / spanCount).roundToInt()
|
||||
outRect.right = (gap - (columnIndex + 1) * gap / spanCount).roundToInt()
|
||||
if (position >= spanCount) outRect.top = GRID_ITEM_VERTICAL_GAP_DP.dpToPx().toInt()
|
||||
@@ -45,6 +52,7 @@ private class ContentGridItemDecoration(
|
||||
}
|
||||
|
||||
private const val HORIZONTAL_ITEM_GAP_DP = 8
|
||||
const val CONTENT_RECOMMENDED_GRID_GAP_DP = 4
|
||||
private const val GRID_ITEM_GAP_DP = 8
|
||||
private const val GRID_ITEM_VERTICAL_GAP_DP = 28
|
||||
const val CONTENT_RECOMMENDED_GRID_SPAN_COUNT = 2
|
||||
|
||||
@@ -14,8 +14,14 @@ import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainHomeBinding
|
||||
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity
|
||||
import kr.co.vividnext.sodalive.following.FollowingCreatorActivity
|
||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
|
||||
import kr.co.vividnext.sodalive.home.pushnotification.PushNotificationListActivity
|
||||
import kr.co.vividnext.sodalive.live.LiveViewModel
|
||||
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
|
||||
import kr.co.vividnext.sodalive.search.SearchActivity
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelLiveCoordinator
|
||||
import kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem
|
||||
@@ -70,6 +76,7 @@ import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentDebutCreatorAdapter
|
||||
import kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingAdapter
|
||||
import kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingItem
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
@@ -78,7 +85,18 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
private val homeRecommendationViewModel: HomeRecommendationViewModel by viewModel()
|
||||
private val homeCreatorRankingViewModel: HomeCreatorRankingViewModel by viewModel()
|
||||
private val homeFollowingViewModel: HomeFollowingViewModel by viewModel()
|
||||
private val liveViewModel: LiveViewModel by inject()
|
||||
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) }
|
||||
private val liveCoordinator: CreatorChannelLiveCoordinator by lazy {
|
||||
CreatorChannelLiveCoordinator(
|
||||
activity = requireActivity(),
|
||||
layoutInflater = layoutInflater,
|
||||
fragmentManager = requireActivity().supportFragmentManager,
|
||||
liveViewModel = liveViewModel,
|
||||
screenWidthProvider = { screenWidth },
|
||||
refreshHome = { homeRecommendationViewModel.loadRecommendations() }
|
||||
)
|
||||
}
|
||||
private val liveAdapter = HomeLiveAdapter()
|
||||
private val recentActivityCreatorAdapter = HomeRecentActivityCreatorAdapter { onRecentActivityClick(it) }
|
||||
private val recentDebutCreatorAdapter = HomeRecentDebutCreatorAdapter { openCreatorProfile(it.creatorId) }
|
||||
@@ -93,7 +111,10 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
)
|
||||
private val popularCommunityAdapter = HomePopularCommunityAdapter { openPopularCommunityPost(it) }
|
||||
private val creatorRankingAdapter = CreatorRankingAdapter { openCreatorRankingProfile(it) }
|
||||
private val followingCreatorAdapter = HomeFollowingCreatorAdapter { openCreatorProfile(it.creatorId) }
|
||||
private val followingCreatorAdapter = HomeFollowingCreatorAdapter(
|
||||
onClickItem = { openCreatorProfile(it.creatorId) },
|
||||
onClickAll = { openFollowingCreatorAll() }
|
||||
)
|
||||
private val followingLiveAdapter = HomeFollowingLiveAdapter { onFollowingLiveClick(it) }
|
||||
private val followingChatAdapter = HomeFollowingChatAdapter { openFollowingChat(it) }
|
||||
private val followingScheduleAdapter = HomeFollowingScheduleAdapter { onFollowingScheduleClick(it) }
|
||||
@@ -104,6 +125,10 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
private var hasLoadedCreatorRankings = false
|
||||
private var hasLoadedFollowing = false
|
||||
private var currentHomeTabIndex = HOME_TAB_RECOMMENDATION
|
||||
private var isRecommendationLoading = false
|
||||
private var isCreatorRankingLoading = false
|
||||
private var isFollowingLoading = false
|
||||
private var isLiveEntryLoading = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -127,12 +152,32 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
setUpCreatorRankingAdapter()
|
||||
setUpFollowingAdapters()
|
||||
setUpBusinessInfo()
|
||||
setupTitleBarActions()
|
||||
bindHomeRecommendationObservers()
|
||||
bindHomeCreatorRankingObservers()
|
||||
bindHomeFollowingObservers()
|
||||
bindLiveEntryObservers()
|
||||
homeRecommendationViewModel.loadRecommendations()
|
||||
}
|
||||
|
||||
private fun setupTitleBarActions() {
|
||||
binding.viewHomeTitleBar.ivTitleBarCash.setOnClickListener {
|
||||
openWithLoginGuard(CanChargeActivity::class.java)
|
||||
}
|
||||
binding.viewHomeTitleBar.ivTitleBarSearch.setOnClickListener {
|
||||
openWithLoginGuard(SearchActivity::class.java)
|
||||
}
|
||||
binding.viewHomeTitleBar.ivTitleBarBell.setOnClickListener {
|
||||
openWithLoginGuard(PushNotificationListActivity::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openWithLoginGuard(activityClass: Class<*>) {
|
||||
ensureMainV2NavigationAllowed {
|
||||
startActivity(Intent(requireContext(), activityClass))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUpBusinessInfo() {
|
||||
val businessInfoText = binding.tvHomeBusinessInfo.text
|
||||
HomeBusinessInfoBinder.bind(binding.tvHomeBusinessInfo)
|
||||
@@ -226,11 +271,8 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
}
|
||||
}
|
||||
homeCreatorRankingViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||
if (isLoading) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
isCreatorRankingLoading = isLoading
|
||||
updateLoadingDialog()
|
||||
}
|
||||
homeCreatorRankingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
||||
toastMessage?.let(::showToast)
|
||||
@@ -289,11 +331,8 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
}
|
||||
}
|
||||
homeFollowingViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||
if (isLoading) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
isFollowingLoading = isLoading
|
||||
updateLoadingDialog()
|
||||
}
|
||||
homeFollowingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
||||
toastMessage?.let(::showToast)
|
||||
@@ -311,17 +350,32 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
}
|
||||
}
|
||||
homeRecommendationViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||
if (isLoading) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
isRecommendationLoading = isLoading
|
||||
updateLoadingDialog()
|
||||
}
|
||||
homeRecommendationViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
|
||||
toastMessage?.let(::showToast)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindLiveEntryObservers() {
|
||||
liveViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
|
||||
isLiveEntryLoading = isLoading
|
||||
updateLoadingDialog()
|
||||
}
|
||||
liveViewModel.toastLiveData.observe(viewLifecycleOwner) { message ->
|
||||
message?.let { showToast(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLoadingDialog() {
|
||||
if (isRecommendationLoading || isCreatorRankingLoading || isFollowingLoading || isLiveEntryLoading) {
|
||||
loadingDialog.show(screenWidth)
|
||||
} else {
|
||||
loadingDialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindHomeRecommendationContent(content: HomeRecommendationUiState.Content) {
|
||||
bindLiveSection(content.lives)
|
||||
bindBannerSection(content.banners)
|
||||
@@ -488,7 +542,16 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
ivSectionTitleChevron.visibility = if (showMore) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun onLiveClick(item: HomeRecommendationLiveUiModel) = Unit
|
||||
private fun onLiveClick(item: HomeRecommendationLiveUiModel) {
|
||||
ensureMainV2NavigationAllowed {
|
||||
liveViewModel.getRoomDetail(item.roomId) { roomDetail ->
|
||||
if (!isAdded || view == null) return@getRoomDetail
|
||||
ensureMainV2NavigationAllowed(requiresAdultContentAccess = roomDetail.isAdult) {
|
||||
liveCoordinator.enterLiveRoom(item.roomId, roomDetail)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openHomeOnAirLive() {
|
||||
ensureMainV2NavigationAllowed {
|
||||
@@ -504,6 +567,12 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
|
||||
private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem) = Unit
|
||||
|
||||
private fun openFollowingCreatorAll() {
|
||||
ensureMainV2NavigationAllowed {
|
||||
startActivity(Intent(requireContext(), FollowingCreatorActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
private fun openFollowingChat(item: ChatRoomListUiItem) {
|
||||
ensureMainV2NavigationAllowed {
|
||||
when (item.chatType) {
|
||||
|
||||
@@ -48,15 +48,49 @@ data class FollowingScheduleResponse(
|
||||
data class FollowingNewsResponse(
|
||||
@SerializedName("newsId") val newsId: String,
|
||||
@SerializedName("type") val type: FollowingNewsType,
|
||||
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("body") val body: String,
|
||||
@SerializedName("thumbnailImageUrl") val thumbnailImageUrl: String?,
|
||||
@SerializedName("targetId") val targetId: Long,
|
||||
@SerializedName("occurredAtUtc") val occurredAtUtc: String,
|
||||
@SerializedName("visibleFromAtUtc") val visibleFromAtUtc: String,
|
||||
@SerializedName("rank") val rank: Int?
|
||||
@SerializedName("creatorRanking") val creatorRanking: FollowingCreatorRankingNewsResponse? = null,
|
||||
@SerializedName("audioContent") val audioContent: FollowingContentNewsResponse? = null,
|
||||
@SerializedName("photoContent") val photoContent: FollowingContentNewsResponse? = null,
|
||||
@SerializedName("contentRanking") val contentRanking: FollowingContentRankingNewsResponse? = null,
|
||||
@SerializedName("communityPost") val communityPost: FollowingCommunityPostNewsResponse? = null
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class FollowingCreatorRankingNewsResponse(
|
||||
@SerializedName("rank") val rank: Int,
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("nickname") val nickname: String,
|
||||
@SerializedName("profileImageUrl") val profileImageUrl: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class FollowingContentNewsResponse(
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("contentImageUrl") val contentImageUrl: String?,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class FollowingContentRankingNewsResponse(
|
||||
@SerializedName("rank") val rank: Int,
|
||||
@SerializedName("contentId") val contentId: Long,
|
||||
@SerializedName("contentImageUrl") val contentImageUrl: String?,
|
||||
@SerializedName("title") val title: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class FollowingCommunityPostNewsResponse(
|
||||
@SerializedName("postId") val postId: Long,
|
||||
@SerializedName("creatorProfileImage") val creatorProfileImage: String,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("imageUrl") val imageUrl: String?,
|
||||
@SerializedName("content") val content: String,
|
||||
@SerializedName("createdAt") val createdAt: String,
|
||||
@SerializedName("likeCount") val likeCount: Int,
|
||||
@SerializedName("commentCount") val commentCount: Int
|
||||
)
|
||||
|
||||
enum class FollowingNewsType {
|
||||
|
||||
@@ -4,7 +4,11 @@ import androidx.annotation.StringRes
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.model.toUiItems
|
||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingCommunityPostNewsResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingContentNewsResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingContentRankingNewsResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingCreatorResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingCreatorRankingNewsResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingLiveResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsResponse
|
||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsType
|
||||
@@ -62,41 +66,87 @@ private fun FollowingNewsResponse.toUiItem(
|
||||
): HomeFollowingNewsUiItem? {
|
||||
val visibleFromText = relativeTimeTextFormatter.format(visibleFromAtUtc)
|
||||
return when (type) {
|
||||
FollowingNewsType.CREATOR_RANKING,
|
||||
FollowingNewsType.CONTENT_RANKING -> rank?.let {
|
||||
HomeFollowingNewsUiItem.Ranking(
|
||||
newsId = newsId,
|
||||
type = type,
|
||||
creatorProfileImageUrl = creatorProfileImageUrl,
|
||||
creatorNickname = creatorNickname,
|
||||
title = title,
|
||||
body = body,
|
||||
thumbnailImageUrl = thumbnailImageUrl,
|
||||
targetId = targetId,
|
||||
occurredAtUtc = occurredAtUtc,
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
visibleFromText = visibleFromText,
|
||||
rank = it
|
||||
)
|
||||
FollowingNewsType.CREATOR_RANKING -> creatorRanking?.toUiItem(newsId, type, visibleFromAtUtc, visibleFromText)
|
||||
FollowingNewsType.CONTENT_RANKING -> contentRanking?.toUiItem(newsId, type, visibleFromAtUtc, visibleFromText)
|
||||
FollowingNewsType.COMMUNITY_POST -> communityPost?.toUiItem(newsId, type, visibleFromAtUtc, visibleFromText)
|
||||
FollowingNewsType.AUDIO_CONTENT -> audioContent?.toUiItem(newsId, type, visibleFromAtUtc, visibleFromText)
|
||||
FollowingNewsType.PHOTO_CONTENT -> photoContent?.toUiItem(newsId, type, visibleFromAtUtc, visibleFromText)
|
||||
}
|
||||
FollowingNewsType.COMMUNITY_POST,
|
||||
FollowingNewsType.AUDIO_CONTENT,
|
||||
FollowingNewsType.PHOTO_CONTENT -> HomeFollowingNewsUiItem.Content(
|
||||
}
|
||||
|
||||
private fun FollowingCreatorRankingNewsResponse.toUiItem(
|
||||
newsId: String,
|
||||
type: FollowingNewsType,
|
||||
visibleFromAtUtc: String,
|
||||
visibleFromText: String
|
||||
) = HomeFollowingNewsUiItem.Ranking(
|
||||
newsId = newsId,
|
||||
type = type,
|
||||
creatorProfileImageUrl = creatorProfileImageUrl,
|
||||
creatorNickname = creatorNickname,
|
||||
title = title,
|
||||
body = body,
|
||||
thumbnailImageUrl = thumbnailImageUrl,
|
||||
targetId = targetId,
|
||||
occurredAtUtc = occurredAtUtc,
|
||||
targetId = creatorId,
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
visibleFromText = visibleFromText,
|
||||
rank = rank,
|
||||
imageUrl = profileImageUrl,
|
||||
title = nickname,
|
||||
creatorNickname = nickname
|
||||
)
|
||||
|
||||
private fun FollowingContentRankingNewsResponse.toUiItem(
|
||||
newsId: String,
|
||||
type: FollowingNewsType,
|
||||
visibleFromAtUtc: String,
|
||||
visibleFromText: String
|
||||
) = HomeFollowingNewsUiItem.Ranking(
|
||||
newsId = newsId,
|
||||
type = type,
|
||||
targetId = contentId,
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
visibleFromText = visibleFromText,
|
||||
rank = rank,
|
||||
imageUrl = contentImageUrl,
|
||||
title = title,
|
||||
creatorNickname = ""
|
||||
)
|
||||
|
||||
private fun FollowingContentNewsResponse.toUiItem(
|
||||
newsId: String,
|
||||
type: FollowingNewsType,
|
||||
visibleFromAtUtc: String,
|
||||
visibleFromText: String
|
||||
) = HomeFollowingNewsUiItem.Content(
|
||||
newsId = newsId,
|
||||
type = type,
|
||||
targetId = contentId,
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
visibleFromText = visibleFromText,
|
||||
contentId = contentId,
|
||||
contentImageUrl = contentImageUrl,
|
||||
title = title,
|
||||
creatorProfileImageUrl = creatorProfileImageUrl,
|
||||
creatorNickname = creatorNickname,
|
||||
labelResId = type.toLabelResId()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun FollowingCommunityPostNewsResponse.toUiItem(
|
||||
newsId: String,
|
||||
type: FollowingNewsType,
|
||||
visibleFromAtUtc: String,
|
||||
visibleFromText: String
|
||||
) = HomeFollowingNewsUiItem.Community(
|
||||
newsId = newsId,
|
||||
type = type,
|
||||
targetId = postId,
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
visibleFromText = visibleFromText,
|
||||
postId = postId,
|
||||
creatorProfileImageUrl = creatorProfileImage,
|
||||
creatorNickname = creatorNickname,
|
||||
imageUrl = imageUrl,
|
||||
content = content,
|
||||
createdAt = createdAt,
|
||||
likeCount = likeCount,
|
||||
commentCount = commentCount
|
||||
)
|
||||
|
||||
@StringRes
|
||||
private fun FollowingNewsType.toLabelResId(): Int = when (this) {
|
||||
|
||||
@@ -55,43 +55,49 @@ data class HomeFollowingScheduleUiItem(
|
||||
sealed interface HomeFollowingNewsUiItem {
|
||||
val newsId: String
|
||||
val type: FollowingNewsType
|
||||
val creatorProfileImageUrl: String
|
||||
val creatorNickname: String
|
||||
val title: String
|
||||
val body: String
|
||||
val thumbnailImageUrl: String?
|
||||
val targetId: Long
|
||||
val occurredAtUtc: String
|
||||
val visibleFromAtUtc: String
|
||||
val visibleFromText: String
|
||||
|
||||
data class Ranking(
|
||||
override val newsId: String,
|
||||
override val type: FollowingNewsType,
|
||||
override val creatorProfileImageUrl: String,
|
||||
override val creatorNickname: String,
|
||||
override val title: String,
|
||||
override val body: String,
|
||||
override val thumbnailImageUrl: String?,
|
||||
override val targetId: Long,
|
||||
override val occurredAtUtc: String,
|
||||
override val visibleFromAtUtc: String,
|
||||
override val visibleFromText: String,
|
||||
val rank: Int
|
||||
val rank: Int,
|
||||
val imageUrl: String?,
|
||||
val title: String,
|
||||
val creatorNickname: String
|
||||
) : HomeFollowingNewsUiItem
|
||||
|
||||
data class Content(
|
||||
override val newsId: String,
|
||||
override val type: FollowingNewsType,
|
||||
override val creatorProfileImageUrl: String,
|
||||
override val creatorNickname: String,
|
||||
override val title: String,
|
||||
override val body: String,
|
||||
override val thumbnailImageUrl: String?,
|
||||
override val targetId: Long,
|
||||
override val occurredAtUtc: String,
|
||||
override val visibleFromAtUtc: String,
|
||||
override val visibleFromText: String,
|
||||
val contentId: Long,
|
||||
val contentImageUrl: String?,
|
||||
val title: String,
|
||||
val creatorProfileImageUrl: String,
|
||||
val creatorNickname: String,
|
||||
@param:StringRes val labelResId: Int
|
||||
) : HomeFollowingNewsUiItem
|
||||
|
||||
data class Community(
|
||||
override val newsId: String,
|
||||
override val type: FollowingNewsType,
|
||||
override val targetId: Long,
|
||||
override val visibleFromAtUtc: String,
|
||||
override val visibleFromText: String,
|
||||
val postId: Long,
|
||||
val creatorProfileImageUrl: String,
|
||||
val creatorNickname: String,
|
||||
val imageUrl: String?,
|
||||
val content: String,
|
||||
val createdAt: String,
|
||||
val likeCount: Int,
|
||||
val commentCount: Int
|
||||
) : HomeFollowingNewsUiItem
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorUiItem
|
||||
|
||||
class HomeFollowingCreatorAdapter(
|
||||
private val onClickItem: (HomeFollowingCreatorUiItem) -> Unit = {}
|
||||
) : RecyclerView.Adapter<HomeFollowingCreatorAdapter.CreatorViewHolder>() {
|
||||
private val onClickItem: (HomeFollowingCreatorUiItem) -> Unit = {},
|
||||
private val onClickAll: () -> Unit = {}
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private var items: List<HomeFollowingCreatorUiItem> = emptyList()
|
||||
|
||||
fun submitItems(items: List<HomeFollowingCreatorUiItem>) {
|
||||
@@ -19,17 +20,29 @@ class HomeFollowingCreatorAdapter(
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CreatorViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_home_following_creator, parent, false)
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val layoutResId = when (viewType) {
|
||||
VIEW_TYPE_ALL -> R.layout.item_home_following_creator_all
|
||||
else -> R.layout.item_home_following_creator
|
||||
}
|
||||
val view = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
|
||||
view.layoutParams = recyclerItemLayoutParams(parent)
|
||||
return CreatorViewHolder(view, onClickItem)
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_ALL -> AllViewHolder(view, onClickAll)
|
||||
else -> CreatorViewHolder(view, onClickItem)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: CreatorViewHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is AllViewHolder -> holder.bind()
|
||||
is CreatorViewHolder -> holder.bind(items[position])
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
override fun getItemCount(): Int = items.size + if (items.isNotEmpty()) 1 else 0
|
||||
|
||||
override fun getItemViewType(position: Int): Int = if (position < items.size) VIEW_TYPE_CREATOR else VIEW_TYPE_ALL
|
||||
|
||||
class CreatorViewHolder(
|
||||
itemView: View,
|
||||
@@ -44,4 +57,18 @@ class HomeFollowingCreatorAdapter(
|
||||
itemView.setOnClickListener { onClickItem(item) }
|
||||
}
|
||||
}
|
||||
|
||||
class AllViewHolder(
|
||||
itemView: View,
|
||||
private val onClickAll: () -> Unit
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
fun bind() {
|
||||
itemView.setOnClickListener { onClickAll() }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val VIEW_TYPE_CREATOR = 0
|
||||
private const val VIEW_TYPE_ALL = 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingLiveUiItem
|
||||
|
||||
class HomeFollowingLiveAdapter(
|
||||
@@ -37,14 +36,12 @@ class HomeFollowingLiveAdapter(
|
||||
itemView: View,
|
||||
private val onClickItem: (HomeFollowingLiveUiItem) -> Unit
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
private val thumbnailImage = itemView.findViewById<ImageView>(R.id.iv_home_following_live_thumbnail)
|
||||
private val profileImage = itemView.findViewById<ImageView>(R.id.iv_home_following_live_creator_profile)
|
||||
private val titleText = itemView.findViewById<TextView>(R.id.tv_home_following_live_title)
|
||||
private val nicknameText = itemView.findViewById<TextView>(R.id.tv_home_following_live_creator_nickname)
|
||||
private val startedAtText = itemView.findViewById<TextView>(R.id.tv_home_following_live_started_at)
|
||||
|
||||
fun bind(item: HomeFollowingLiveUiItem) {
|
||||
thumbnailImage.loadUrl(item.creatorProfileImageUrl)
|
||||
profileImage.loadHomeCreatorProfileImage(item.creatorProfileImageUrl)
|
||||
titleText.text = item.title
|
||||
nicknameText.text = item.creatorNickname
|
||||
|
||||
@@ -8,7 +8,9 @@ import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsType
|
||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingNewsUiItem
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedContentCategory
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedContentView
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedRankHighlight
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedRankView
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedSize
|
||||
@@ -29,13 +31,8 @@ class HomeFollowingNewsAdapter(
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (val item = items[position]) {
|
||||
is HomeFollowingNewsUiItem.Ranking -> VIEW_TYPE_RANKING
|
||||
is HomeFollowingNewsUiItem.Content -> when (item.type) {
|
||||
FollowingNewsType.COMMUNITY_POST -> VIEW_TYPE_COMMUNITY
|
||||
FollowingNewsType.AUDIO_CONTENT,
|
||||
FollowingNewsType.PHOTO_CONTENT -> VIEW_TYPE_CONTENT
|
||||
FollowingNewsType.CREATOR_RANKING,
|
||||
FollowingNewsType.CONTENT_RANKING -> VIEW_TYPE_CONTENT
|
||||
}
|
||||
is HomeFollowingNewsUiItem.Content -> VIEW_TYPE_CONTENT
|
||||
is HomeFollowingNewsUiItem.Community -> VIEW_TYPE_COMMUNITY
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
|
||||
@@ -76,18 +73,39 @@ class HomeFollowingNewsAdapter(
|
||||
|
||||
override fun bind(item: HomeFollowingNewsUiItem) {
|
||||
val ranking = item as HomeFollowingNewsUiItem.Ranking
|
||||
val context = view.context
|
||||
val rankText = "${ranking.rank}위"
|
||||
val message = ranking.toRankingMessage(rankText)
|
||||
val feedItem = FeedItem.Rank(
|
||||
feedId = ranking.newsId,
|
||||
imageUrl = ranking.thumbnailImageUrl.orEmpty(),
|
||||
imageUrl = ranking.imageUrl.orEmpty(),
|
||||
rankText = ranking.rank.toString(),
|
||||
message = ranking.body.ifBlank { ranking.title },
|
||||
highlightRanges = emptyList()
|
||||
message = message,
|
||||
highlightRanges = listOf(
|
||||
FeedRankHighlight(
|
||||
start = message.indexOf(rankText),
|
||||
endExclusive = message.indexOf(rankText) + rankText.length
|
||||
)
|
||||
)
|
||||
)
|
||||
view.setFeedSize(feedSize(FeedVariant.Rank, parent))
|
||||
view.bind(feedItem)
|
||||
bindImage(view.imageView(), feedItem.imageUrl)
|
||||
view.setOnFeedClick { onClickItem(ranking) }
|
||||
}
|
||||
|
||||
private fun HomeFollowingNewsUiItem.Ranking.toRankingMessage(rankText: String): String {
|
||||
val messageResId = when (type) {
|
||||
FollowingNewsType.CREATOR_RANKING -> R.string.screen_home_following_creator_ranking_news_message
|
||||
FollowingNewsType.CONTENT_RANKING -> R.string.screen_home_following_content_ranking_news_message
|
||||
FollowingNewsType.COMMUNITY_POST,
|
||||
FollowingNewsType.AUDIO_CONTENT,
|
||||
FollowingNewsType.PHOTO_CONTENT -> R.string.screen_home_following_content_ranking_news_message
|
||||
}
|
||||
val subject = if (type == FollowingNewsType.CREATOR_RANKING) creatorNickname else title
|
||||
val context = view.context
|
||||
return context.getString(messageResId, subject, rankText)
|
||||
}
|
||||
}
|
||||
|
||||
class CommunityViewHolder(
|
||||
@@ -97,19 +115,19 @@ class HomeFollowingNewsAdapter(
|
||||
) : NewsViewHolder(view) {
|
||||
|
||||
override fun bind(item: HomeFollowingNewsUiItem) {
|
||||
val community = item as HomeFollowingNewsUiItem.Content
|
||||
val community = item as HomeFollowingNewsUiItem.Community
|
||||
val feedItem = FeedItem.Community(
|
||||
feedId = community.newsId,
|
||||
creatorId = community.targetId.toString(),
|
||||
creatorName = community.creatorNickname,
|
||||
creatorImageUrl = community.creatorProfileImageUrl,
|
||||
postId = community.targetId.toString(),
|
||||
bodyText = community.body.ifBlank { community.title },
|
||||
postId = community.postId.toString(),
|
||||
bodyText = community.content,
|
||||
keywordText = "",
|
||||
createdAtText = community.visibleFromText,
|
||||
commentCount = 0,
|
||||
likeCount = 0,
|
||||
imageUrl = community.thumbnailImageUrl
|
||||
commentCount = community.commentCount,
|
||||
likeCount = community.likeCount,
|
||||
imageUrl = community.imageUrl
|
||||
)
|
||||
view.setFeedSize(feedSize(FeedVariant.Community, parent))
|
||||
view.bind(feedItem)
|
||||
@@ -132,10 +150,11 @@ class HomeFollowingNewsAdapter(
|
||||
creatorId = content.targetId.toString(),
|
||||
creatorName = content.creatorNickname,
|
||||
creatorImageUrl = content.creatorProfileImageUrl,
|
||||
contentId = content.targetId.toString(),
|
||||
contentId = content.contentId.toString(),
|
||||
contentTitle = content.title,
|
||||
contentImageUrl = content.thumbnailImageUrl.orEmpty(),
|
||||
createdAtText = content.visibleFromText
|
||||
contentImageUrl = content.contentImageUrl.orEmpty(),
|
||||
createdAtText = content.visibleFromText,
|
||||
category = content.type.toFeedContentCategory()
|
||||
)
|
||||
view.setFeedSize(feedSize(FeedVariant.Content, parent))
|
||||
view.bind(feedItem)
|
||||
@@ -164,5 +183,13 @@ class HomeFollowingNewsAdapter(
|
||||
imageView.loadUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
private fun FollowingNewsType.toFeedContentCategory(): FeedContentCategory = when (this) {
|
||||
FollowingNewsType.AUDIO_CONTENT -> FeedContentCategory.Audio
|
||||
FollowingNewsType.PHOTO_CONTENT -> FeedContentCategory.Photo
|
||||
FollowingNewsType.COMMUNITY_POST,
|
||||
FollowingNewsType.CREATOR_RANKING,
|
||||
FollowingNewsType.CONTENT_RANKING -> FeedContentCategory.Content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import androidx.annotation.StringRes
|
||||
import kr.co.vividnext.sodalive.R
|
||||
|
||||
enum class FeedContentCategory(
|
||||
@StringRes val labelResId: Int,
|
||||
@param:StringRes val labelResId: Int,
|
||||
val ratioWidth: Int,
|
||||
val ratioHeight: Int
|
||||
) {
|
||||
Content(labelResId = R.string.feed_content_category_content, ratioWidth = 1, ratioHeight = 1),
|
||||
Audio(labelResId = R.string.home_recommendation_activity_audio, ratioWidth = 1, ratioHeight = 1),
|
||||
Photo(labelResId = R.string.screen_home_following_photo_content, ratioWidth = 1, ratioHeight = 1),
|
||||
Series(labelResId = R.string.feed_content_category_series, ratioWidth = 163, ratioHeight = 230),
|
||||
Magazine(labelResId = R.string.feed_content_category_magazine, ratioWidth = 163, ratioHeight = 218)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ class FeedContentView @JvmOverloads constructor(
|
||||
titleText = findViewById(R.id.tv_feed_content_title)
|
||||
categoryText = findViewById(R.id.tv_feed_content_category)
|
||||
createdAtText = findViewById(R.id.tv_feed_content_created_at)
|
||||
clipToOutline = true
|
||||
outlineProvider = roundedOutlineProvider()
|
||||
contentImageView().clipToOutline = true
|
||||
contentImageView().outlineProvider = roundedOutlineProvider()
|
||||
profileImageView().clipToOutline = true
|
||||
@@ -105,6 +107,6 @@ class FeedContentView @JvmOverloads constructor(
|
||||
private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt()
|
||||
|
||||
private companion object {
|
||||
const val CONTENT_IMAGE_WIDTH_DP = 163
|
||||
const val CONTENT_IMAGE_WIDTH_DP = 88
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/gray_800" />
|
||||
<corners android:radius="@dimen/radius_14" />
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/gray_900" />
|
||||
<corners android:radius="20dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
|
||||
<solid android:color="@color/gray_800" />
|
||||
</shape>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
|
||||
<solid android:color="@color/soda_400" />
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/gray_800" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/soda_400" />
|
||||
<corners android:radius="18dp" />
|
||||
</shape>
|
||||
@@ -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/gray_900" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@color/color_3bb9f1" />
|
||||
<corners android:radius="90dp" />
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/bg_v2_modal.xml
Normal file
5
app/src/main/res/drawable/bg_v2_modal.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/gray_900" />
|
||||
<corners android:radius="@dimen/radius_14" />
|
||||
</shape>
|
||||
10
app/src/main/res/drawable/ic_feed_community_heart_fill.xml
Normal file
10
app/src/main/res/drawable/ic_feed_community_heart_fill.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="18dp"
|
||||
android:height="18dp"
|
||||
android:viewportWidth="18"
|
||||
android:viewportHeight="18">
|
||||
<path
|
||||
android:fillColor="@color/soda_400"
|
||||
android:pathData="M9,15.8L7.9,14.8C4,11.3 1.5,9 1.5,6.2C1.5,3.9 3.3,2.2 5.5,2.2C6.8,2.2 8,2.8 9,3.8C10,2.8 11.2,2.2 12.5,2.2C14.7,2.2 16.5,3.9 16.5,6.2C16.5,9 14,11.3 10.1,14.8L9,15.8Z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_new_arrow_up_gray.xml
Normal file
10
app/src/main/res/drawable/ic_new_arrow_up_gray.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/gray_500"
|
||||
android:pathData="M12,5L5,12L6.4,13.4L11,8.8V19H13V8.8L17.6,13.4L19,12L12,5Z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_new_arrow_up_white.xml
Normal file
10
app/src/main/res/drawable/ic_new_arrow_up_white.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/white"
|
||||
android:pathData="M12,5L5,12L6.4,13.4L11,8.8V19H13V8.8L17.6,13.4L19,12L12,5Z" />
|
||||
</vector>
|
||||
@@ -407,6 +407,31 @@
|
||||
app:tint="@color/white" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/btn_creator_channel_fantalk_fixed_plus"
|
||||
android:layout_width="66dp"
|
||||
android:layout_height="66dp"
|
||||
android:layout_marginEnd="@dimen/spacing_14"
|
||||
android:layout_marginBottom="@dimen/spacing_14"
|
||||
android:background="@drawable/bg_creator_channel_owner_fab"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/creator_channel_fantalk_support_action"
|
||||
android:elevation="8dp"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/spacing_14"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="38dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_plus_no_bg"
|
||||
app:tint="@color/white" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/owner_fab_button"
|
||||
android:layout_width="66dp"
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout_creator_channel_community_detail_title_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:background="@color/black"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_creator_channel_community_detail_back"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/a11y_back"
|
||||
android:src="@drawable/ic_new_bar_back"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scroll_creator_channel_community_detail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
android:paddingBottom="@dimen/spacing_16"
|
||||
app:layout_constraintBottom_toTopOf="@id/layout_creator_channel_community_detail_input_bar"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_creator_channel_community_detail_title_bar">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_community_detail_post"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_feed_card"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_14">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_community_detail_profile"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:contentDescription="@string/a11y_feed_profile_image"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/bg_placeholder" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_nickname"
|
||||
style="@style/Typography.Body5"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintEnd_toStartOf="@id/layout_creator_channel_community_detail_price"
|
||||
app:layout_constraintStart_toEndOf="@id/iv_creator_channel_community_detail_profile"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile"
|
||||
tools:text="크리에이터" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_time"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/gray_500"
|
||||
app:layout_constraintEnd_toEndOf="@id/tv_creator_channel_community_detail_nickname"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_creator_channel_community_detail_nickname"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_community_detail_nickname"
|
||||
tools:text="2분 전" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_community_detail_price"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_creator_channel_community_price"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_4"
|
||||
android:paddingVertical="2dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_detail_profile"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_bar_cash" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_price"
|
||||
style="@style/Typography.Caption3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/black"
|
||||
tools:text="300" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_body"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_12"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"
|
||||
tools:text="게시글 본문" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_creator_channel_community_detail_image_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="236dp"
|
||||
android:layout_marginTop="@dimen/spacing_14"
|
||||
android:background="@drawable/bg_feed_community_image"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_community_detail_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/a11y_feed_content_image"
|
||||
android:scaleType="centerCrop" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_community_detail_paywall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/gray_800"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_new_community_lock" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_paywall_price"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginTop="@dimen/spacing_4"
|
||||
android:background="@drawable/bg_creator_channel_community_price"
|
||||
android:drawableStart="@drawable/ic_bar_cash"
|
||||
android:drawablePadding="@dimen/spacing_6"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/black"
|
||||
tools:text="30" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_audio"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_12"
|
||||
android:background="@drawable/bg_creator_channel_fantalk_reply"
|
||||
android:drawableStart="@drawable/ic_new_player_play"
|
||||
android:drawablePadding="@dimen/spacing_8"
|
||||
android:includeFontPadding="false"
|
||||
android:padding="@dimen/spacing_12"
|
||||
android:textColor="@color/white"
|
||||
android:visibility="gone"
|
||||
tools:text="Audio"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_purchased"
|
||||
style="@style/Typography.Caption2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="@dimen/spacing_8"
|
||||
android:background="@drawable/bg_creator_channel_community_price"
|
||||
android:includeFontPadding="false"
|
||||
android:paddingHorizontal="@dimen/spacing_8"
|
||||
android:paddingVertical="@dimen/spacing_4"
|
||||
android:text="@string/creator_channel_community_purchased_badge"
|
||||
android:textColor="@color/black"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="@dimen/spacing_16"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_community_detail_comment"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_feed_community_reply" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_comment_count"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_4"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/gray_400"
|
||||
tools:text="5" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_community_detail_heart"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginStart="15dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_feed_community_heart" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_like_count"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_4"
|
||||
android:layout_weight="1"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/gray_400"
|
||||
tools:text="6" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_detail_comment_header"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_14"
|
||||
android:includeFontPadding="false"
|
||||
android:text="@string/creator_channel_community_comment_header"
|
||||
android:textColor="@color/white"
|
||||
android:visibility="gone"
|
||||
tools:text="댓글 2"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_creator_channel_community_detail_comments"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_12"
|
||||
android:nestedScrollingEnabled="false"
|
||||
tools:itemCount="2"
|
||||
tools:listitem="@layout/item_creator_channel_community_comment" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_community_detail_input_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
android:paddingVertical="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_creator_channel_community_detail_comment"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_creator_channel_community_input_field"
|
||||
android:hint="@string/creator_channel_community_comment_placeholder"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:maxLines="3"
|
||||
android:minHeight="40dp"
|
||||
android:minLines="1"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
android:scrollbars="vertical"
|
||||
android:textColor="@color/white"
|
||||
android:textColorHint="@color/gray_500"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_creator_channel_community_detail_send"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:background="@drawable/bg_creator_channel_community_send_disabled"
|
||||
android:contentDescription="@string/action_send"
|
||||
android:enabled="false"
|
||||
android:src="@drawable/ic_new_arrow_up_gray"
|
||||
tools:background="@drawable/bg_creator_channel_community_send_enabled" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_creator_channel_community_detail_edit_cancel"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/cancel"
|
||||
android:src="@drawable/ic_x_white"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout_creator_channel_community_reply_title_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:background="@color/black"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_creator_channel_community_reply_back"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/a11y_back"
|
||||
android:src="@drawable/ic_new_bar_back"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
app:layout_constraintBottom_toTopOf="@id/layout_creator_channel_community_reply_input_bar"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_creator_channel_community_reply_title_bar">
|
||||
|
||||
<include
|
||||
android:id="@+id/layout_creator_channel_community_reply_parent"
|
||||
layout="@layout/item_creator_channel_community_comment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_creator_channel_community_reply"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="@dimen/spacing_14"
|
||||
tools:itemCount="2"
|
||||
tools:listitem="@layout/item_creator_channel_community_reply" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_community_reply_input_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
android:paddingVertical="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_creator_channel_community_reply"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_creator_channel_community_input_field"
|
||||
android:hint="@string/creator_channel_community_reply_placeholder"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:maxLines="3"
|
||||
android:minHeight="40dp"
|
||||
android:minLines="1"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
android:scrollbars="vertical"
|
||||
android:textColor="@color/white"
|
||||
android:textColorHint="@color/gray_500"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_creator_channel_community_reply_send"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:background="@drawable/bg_creator_channel_community_send_disabled"
|
||||
android:contentDescription="@string/action_send"
|
||||
android:enabled="false"
|
||||
android:src="@drawable/ic_new_arrow_up_gray" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_creator_channel_community_reply_edit_cancel"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/cancel"
|
||||
android:src="@drawable/ic_x_white"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,249 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout_creator_channel_fantalk_detail_title_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:background="@color/black"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_creator_channel_fantalk_detail_back"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/a11y_back"
|
||||
android:src="@drawable/ic_new_bar_back"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scroll_creator_channel_fantalk_detail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
app:layout_constraintBottom_toTopOf="@id/layout_creator_channel_fantalk_detail_input_bar"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_creator_channel_fantalk_detail_title_bar">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/spacing_14"
|
||||
android:paddingBottom="@dimen/spacing_14">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_fantalk_detail_profile"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:contentDescription="@string/a11y_feed_profile_image"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_placeholder_profile" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_fantalk_detail_nickname"
|
||||
style="@style/Typography.Body5"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_14"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintEnd_toStartOf="@id/iv_creator_channel_fantalk_detail_more"
|
||||
app:layout_constraintStart_toEndOf="@id/iv_creator_channel_fantalk_detail_profile"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_fantalk_detail_profile"
|
||||
tools:text="닉네임" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_fantalk_detail_time"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_4"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/gray_500"
|
||||
app:layout_constraintEnd_toEndOf="@id/tv_creator_channel_fantalk_detail_nickname"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_creator_channel_fantalk_detail_nickname"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_fantalk_detail_nickname"
|
||||
tools:text="2분 전" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_fantalk_detail_more"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_new_more"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_creator_channel_fantalk_detail_nickname"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_fantalk_detail_content"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_12"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_creator_channel_fantalk_detail_nickname"
|
||||
app:layout_constraintTop_toBottomOf="@id/iv_creator_channel_fantalk_detail_profile"
|
||||
tools:text="팬톡 응원 메시지 내용입니다." />
|
||||
|
||||
<View
|
||||
android:id="@+id/view_creator_channel_fantalk_detail_reply_connector"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="@dimen/spacing_14"
|
||||
android:layout_marginTop="@dimen/spacing_14"
|
||||
android:background="@color/gray_800"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_creator_channel_fantalk_detail_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_fantalk_detail_content"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_fantalk_detail_reply"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_8"
|
||||
android:background="@drawable/bg_creator_channel_fantalk_reply"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_14"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_creator_channel_fantalk_detail_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/view_creator_channel_fantalk_detail_reply_connector"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_fantalk_detail_reply_profile"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:contentDescription="@string/a11y_feed_profile_image"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/ic_placeholder_profile" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_fantalk_detail_reply_nickname"
|
||||
style="@style/Typography.Caption3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
tools:text="크리에이터" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_fantalk_detail_reply_time"
|
||||
style="@style/Typography.Caption3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_4"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/gray_400"
|
||||
tools:text="방금 전" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view_creator_channel_fantalk_detail_reply_header_spacer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_fantalk_detail_reply_more"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_new_more" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_fantalk_detail_reply_content"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_8"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"
|
||||
tools:text="응원 고마워요!" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_fantalk_detail_input_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/black"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
android:paddingVertical="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_creator_channel_fantalk_detail_reply"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/bg_creator_channel_community_input_field"
|
||||
android:hint="@string/creator_channel_community_reply_placeholder"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:maxLines="3"
|
||||
android:minHeight="40dp"
|
||||
android:minLines="1"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
android:scrollbars="vertical"
|
||||
android:textColor="@color/white"
|
||||
android:textColorHint="@color/gray_500"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_creator_channel_fantalk_detail_send"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:background="@drawable/bg_creator_channel_community_send_disabled"
|
||||
android:contentDescription="@string/action_send"
|
||||
android:enabled="false"
|
||||
android:src="@drawable/ic_new_arrow_up_gray" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_creator_channel_fantalk_detail_edit_cancel"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/cancel"
|
||||
android:src="@drawable/ic_x_white"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_fantalk_write_cancel"
|
||||
style="@style/Typography.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="60dp"
|
||||
android:layout_marginStart="@dimen/spacing_14"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:text="@string/creator_channel_fantalk_write_cancel"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_fantalk_write_send"
|
||||
style="@style/Typography.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginEnd="@dimen/spacing_14"
|
||||
android:background="@drawable/bg_creator_channel_fantalk_write_send_disabled"
|
||||
android:enabled="false"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:minWidth="68dp"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
android:text="@string/creator_channel_fantalk_write_send"
|
||||
android:textColor="@color/gray_500"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_creator_channel_fantalk_write_cancel"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_creator_channel_fantalk_write_cancel" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_creator_channel_fantalk_write_content"
|
||||
style="@style/Typography.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="@dimen/spacing_14"
|
||||
android:layout_marginTop="@dimen/spacing_20"
|
||||
android:background="@android:color/transparent"
|
||||
android:gravity="top|start"
|
||||
android:hint="@string/creator_channel_fantalk_write_hint"
|
||||
android:imeOptions="actionDone"
|
||||
android:includeFontPadding="false"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:maxLength="500"
|
||||
android:minHeight="180dp"
|
||||
android:padding="0dp"
|
||||
android:textColor="@color/white"
|
||||
android:textColorHint="@color/gray_500"
|
||||
app:layout_constraintBottom_toTopOf="@id/layout_creator_channel_fantalk_write_info_bar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_fantalk_write_cancel"
|
||||
tools:text="크리에이터를 향한 응원 메시지를 입력합니다." />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_fantalk_write_info_bar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="52dp"
|
||||
android:background="@color/black"
|
||||
android:gravity="center_vertical|end"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_fantalk_write_count"
|
||||
style="@style/Typography.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"
|
||||
tools:text="23" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_fantalk_write_count_limit"
|
||||
style="@style/Typography.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:text="/500자"
|
||||
android:textColor="@color/gray_500" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/gray_700"
|
||||
app:layout_constraintBottom_toTopOf="@id/layout_creator_channel_fantalk_write_info_bar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
71
app/src/main/res/layout/dialog_v2_modal.xml
Normal file
71
app/src/main/res/layout/dialog_v2_modal.xml
Normal file
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/bg_v2_modal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/bold"
|
||||
android:gravity="center"
|
||||
android:minHeight="64dp"
|
||||
android:paddingHorizontal="@dimen/spacing_20"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
tools:text="모달" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_desc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/regular"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.45"
|
||||
android:padding="@dimen/spacing_20"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="모달 안내 문구가\n들어가는 부분입니다." />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_20"
|
||||
android:paddingVertical="@dimen/spacing_14"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_cancel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/spacing_8"
|
||||
android:layout_weight="1"
|
||||
android:fontFamily="@font/medium"
|
||||
android:gravity="center"
|
||||
android:padding="@dimen/spacing_14"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
tools:text="취소" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_confirm"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:fontFamily="@font/medium"
|
||||
android:gravity="center"
|
||||
android:padding="@dimen/spacing_14"
|
||||
android:textColor="@color/soda_400"
|
||||
android:textSize="18sp"
|
||||
tools:text="확인" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -151,7 +151,9 @@
|
||||
android:background="@drawable/bg_creator_channel_fantalk_write_button"
|
||||
android:contentDescription="@string/creator_channel_fantalk_support_action"
|
||||
android:padding="@dimen/spacing_16"
|
||||
android:src="@drawable/ic_new_fantalk_plus"
|
||||
android:src="@drawable/ic_plus_no_bg"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -226,28 +226,6 @@
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_content_latest_audio_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_48"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/view_content_latest_audio_title"
|
||||
layout="@layout/view_section_title" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_content_latest_audios"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_14"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_content_new_and_hot_section"
|
||||
android:layout_width="match_parent"
|
||||
@@ -270,6 +248,28 @@
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_content_latest_audio_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_48"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/view_content_latest_audio_title"
|
||||
layout="@layout/view_section_title" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_content_latest_audios"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_14"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_content_free_audio_section"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="310dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:background="@drawable/bg_feed_card"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_14">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -46,8 +48,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_12"
|
||||
android:background="@drawable/bg_content_commented_audio_comment"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/spacing_12">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_content_comment_profile"
|
||||
@@ -66,8 +70,9 @@
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:lineSpacingMultiplier="1.45"
|
||||
android:maxLines="2"
|
||||
android:textColor="@color/gray_500"
|
||||
android:textColor="@color/white"
|
||||
tools:text="최근 댓글 내용" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="@dimen/spacing_14">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_community_comment_profile"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:contentDescription="@string/a11y_profile_image"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/bg_placeholder" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_comment_nickname"
|
||||
style="@style/Typography.Body5"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintEnd_toStartOf="@id/iv_creator_channel_community_comment_more"
|
||||
app:layout_constraintStart_toEndOf="@id/iv_creator_channel_community_comment_profile"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_comment_profile"
|
||||
tools:text="닉네임" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_comment_time"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/gray_500"
|
||||
app:layout_constraintEnd_toEndOf="@id/tv_creator_channel_community_comment_nickname"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_creator_channel_community_comment_nickname"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_community_comment_nickname"
|
||||
tools:text="2분 전" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_community_comment_more"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_new_more"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_comment_body"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="50dp"
|
||||
android:layout_marginTop="@dimen/spacing_8"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"
|
||||
tools:text="댓글 내용" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_creator_channel_community_comment_latest_reply"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="50dp"
|
||||
android:layout_marginTop="@dimen/spacing_12"
|
||||
android:background="@drawable/bg_creator_channel_fantalk_reply"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_14"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_comment_latest_reply_nickname"
|
||||
style="@style/Typography.Body5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"
|
||||
tools:text="답글 닉네임" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_comment_latest_reply_body"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_6"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"
|
||||
tools:text="최신 답글" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="@dimen/spacing_14"
|
||||
android:background="@color/gray_800" />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,75 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginBottom="@dimen/spacing_12"
|
||||
android:background="@drawable/bg_creator_channel_fantalk_reply"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_14">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_community_reply_item_profile"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:contentDescription="@string/a11y_profile_image"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/bg_placeholder" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_reply_item_nickname"
|
||||
style="@style/Typography.Body5"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintEnd_toStartOf="@id/iv_creator_channel_community_reply_item_more"
|
||||
app:layout_constraintStart_toEndOf="@id/iv_creator_channel_community_reply_item_profile"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_creator_channel_community_reply_item_profile"
|
||||
tools:text="닉네임" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_reply_item_time"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/gray_500"
|
||||
app:layout_constraintEnd_toEndOf="@id/tv_creator_channel_community_reply_item_nickname"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_creator_channel_community_reply_item_nickname"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_creator_channel_community_reply_item_nickname"
|
||||
tools:text="2분 전" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_creator_channel_community_reply_item_more"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_new_more"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_reply_item_body"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_8"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/white"
|
||||
tools:text="답글 내용" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -67,21 +67,14 @@
|
||||
android:id="@+id/tv_fantalk_content"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_14"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="2"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
tools:text="팬이 쓴 응원이 보이는 부분입니다. 두 줄까지만 보여주고 초과 시 말줄임 처리." />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginStart="@dimen/spacing_14"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_chevron_down_white" />
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
|
||||
28
app/src/main/res/layout/item_home_following_creator_all.xml
Normal file
28
app/src/main/res/layout/item_home_following_creator_all.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_creator_all"
|
||||
style="@style/Typography.Body5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="75dp"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:text="@string/screen_home_theme_all"
|
||||
android:textColor="@color/soda_400" />
|
||||
|
||||
<TextView
|
||||
style="@style/Typography.Body5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_6"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:text="@string/screen_home_theme_all"
|
||||
android:visibility="invisible" />
|
||||
</LinearLayout>
|
||||
@@ -1,38 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="244dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_feed_card"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_14">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:background="@drawable/bg_audio_content_card_thumbnail">
|
||||
android:layout_width="263dp"
|
||||
android:layout_height="100dp"
|
||||
android:background="@drawable/bg_home_following_live_capsule"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="22dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_home_following_live_thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/iv_home_following_live_creator_profile"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/ic_launcher_background" />
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_placeholder_profile" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_home_following_live_status"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/iv_home_following_live_creator_profile"
|
||||
app:layout_constraintTop_toTopOf="@id/iv_home_following_live_creator_profile"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="18dp"
|
||||
android:layout_gravity="top|start"
|
||||
android:layout_margin="@dimen/spacing_8"
|
||||
android:background="@drawable/bg_live_thumbnail_badge_capsule"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/spacing_4">
|
||||
android:paddingHorizontal="@dimen/spacing_4"
|
||||
android:paddingVertical="2dp">
|
||||
|
||||
<View
|
||||
android:layout_width="@dimen/spacing_8"
|
||||
android:layout_height="@dimen/spacing_8"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="8dp"
|
||||
android:background="@drawable/bg_live_thumbnail_dot" />
|
||||
|
||||
<TextView
|
||||
@@ -40,53 +50,11 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_4"
|
||||
android:includeFontPadding="true"
|
||||
android:includeFontPadding="false"
|
||||
android:text="LIVE"
|
||||
android:textColor="@color/white"
|
||||
tools:ignore="HardcodedText,SmallSp" />
|
||||
tools:ignore="HardcodedText" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_live_title"
|
||||
style="@style/Typography.Heading4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_12"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
tools:text="라이브 제목" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_8"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_home_following_live_creator_profile"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:background="@drawable/bg_round_corner_999_263238"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/ic_placeholder_profile" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_live_creator_nickname"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/gray_300"
|
||||
tools:text="크리에이터 이름" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_live_started_at"
|
||||
@@ -96,6 +64,36 @@
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/gray_500"
|
||||
tools:text="2분 전" />
|
||||
</LinearLayout>
|
||||
tools:text="00:00" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_live_title"
|
||||
style="@style/Typography.Heading4"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_4"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintEnd_toEndOf="@id/layout_home_following_live_status"
|
||||
app:layout_constraintStart_toStartOf="@id/layout_home_following_live_status"
|
||||
app:layout_constraintTop_toBottomOf="@id/layout_home_following_live_status"
|
||||
tools:text="라이브 제목" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_live_creator_nickname"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_4"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/gray_500"
|
||||
app:layout_constraintEnd_toEndOf="@id/tv_home_following_live_title"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_home_following_live_title"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_home_following_live_title"
|
||||
tools:text="크리에이터이름" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_creator_channel_sort_popup"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/spacing_12"
|
||||
android:paddingVertical="@dimen/spacing_8">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_more_edit"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/regular"
|
||||
android:includeFontPadding="false"
|
||||
android:paddingHorizontal="@dimen/spacing_12"
|
||||
android:paddingVertical="@dimen/spacing_8"
|
||||
android:text="@string/creator_channel_fantalk_edit"
|
||||
android:textColor="@color/white"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_creator_channel_community_more_delete"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/regular"
|
||||
android:includeFontPadding="false"
|
||||
android:paddingHorizontal="@dimen/spacing_12"
|
||||
android:paddingVertical="@dimen/spacing_8"
|
||||
android:text="@string/creator_channel_fantalk_delete"
|
||||
android:textColor="@color/white"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
</LinearLayout>
|
||||
@@ -5,20 +5,19 @@
|
||||
android:layout_width="374dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_feed_card"
|
||||
android:clipToOutline="true"
|
||||
android:padding="@dimen/spacing_14">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fl_feed_content_image_container"
|
||||
android:layout_width="163dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="88dp"
|
||||
android:layout_height="88dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_feed_content_image"
|
||||
android:layout_width="163dp"
|
||||
android:layout_height="163dp"
|
||||
android:layout_width="88dp"
|
||||
android:layout_height="88dp"
|
||||
android:contentDescription="@string/a11y_feed_content_image"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/ic_launcher_background" />
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_review_modify"
|
||||
android:title="@string/menu_modify_action"
|
||||
android:title="@string/creator_channel_fantalk_edit"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_review_delete"
|
||||
android:title="@string/menu_delete_action"
|
||||
android:title="@string/creator_channel_fantalk_delete"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user