fix(home): 팔로잉 탭 후속 검증을 반영한다
This commit is contained in:
@@ -20,6 +20,7 @@ import kr.co.vividnext.sodalive.v2.access.AccessRequirement
|
||||
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
|
||||
import kr.co.vividnext.sodalive.v2.chat.action.ChatActionCommand
|
||||
import kr.co.vividnext.sodalive.v2.chat.action.handleChatAction
|
||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||
import kr.co.vividnext.sodalive.v2.community.action.CommunityActionCommand
|
||||
import kr.co.vividnext.sodalive.v2.community.action.handleCommunityAction
|
||||
import kr.co.vividnext.sodalive.v2.content.action.ContentActionCommand
|
||||
@@ -30,6 +31,7 @@ import kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinator
|
||||
import kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveActivity
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomType
|
||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsType
|
||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingChatSection
|
||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorSection
|
||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingLiveSection
|
||||
@@ -409,7 +411,10 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
bindHomeFollowingEmpty(showEmptyMessage = true)
|
||||
finishHomePullRefresh(HOME_TAB_FOLLOWING)
|
||||
}
|
||||
HomeFollowingUiState.LoginRequired -> bindHomeFollowingEmpty(showEmptyMessage = false)
|
||||
HomeFollowingUiState.LoginRequired -> {
|
||||
bindHomeFollowingEmpty(showEmptyMessage = false)
|
||||
finishHomePullRefresh(HOME_TAB_FOLLOWING)
|
||||
}
|
||||
is HomeFollowingUiState.Error -> {
|
||||
if (shouldPreserveHomeContentOnPullRefreshError(HOME_TAB_FOLLOWING)) return@observe
|
||||
bindHomeFollowingEmpty(showEmptyMessage = false)
|
||||
@@ -665,15 +670,38 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
||||
|
||||
private fun onFollowingSectionMoreClick(section: HomeFollowingSection) = Unit
|
||||
|
||||
private fun onFollowingLiveClick(item: HomeFollowingLiveUiItem) = Unit
|
||||
private fun onFollowingLiveClick(item: HomeFollowingLiveUiItem) {
|
||||
liveActionCoordinator.enterLiveRoom(item.liveId)
|
||||
}
|
||||
|
||||
private fun onFollowingScheduleClick(item: HomeFollowingScheduleUiItem) = Unit
|
||||
private fun onFollowingScheduleClick(item: HomeFollowingScheduleUiItem) {
|
||||
if (item.targetId <= 0L) return
|
||||
when (item.type) {
|
||||
CreatorActivityType.Live -> liveActionCoordinator.enterLiveRoom(item.targetId)
|
||||
CreatorActivityType.LiveReplay,
|
||||
CreatorActivityType.Audio -> handleContentAction(
|
||||
ContentActionCommand.AudioDetail(audioContentId = item.targetId)
|
||||
)
|
||||
CreatorActivityType.Community -> handleCommunityAction(
|
||||
CommunityActionCommand.PostDetail(item.targetId)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem) {
|
||||
val community = item as? HomeFollowingNewsUiItem.Community ?: return
|
||||
val postId = community.postId
|
||||
if (postId <= 0L) return
|
||||
handleCommunityAction(CommunityActionCommand.PostDetail(postId))
|
||||
when (item) {
|
||||
is HomeFollowingNewsUiItem.Ranking -> if (item.type == FollowingNewsType.CREATOR_RANKING) {
|
||||
handleCreatorAction(CreatorActionCommand.Profile(item.targetId))
|
||||
}
|
||||
is HomeFollowingNewsUiItem.Content -> if (item.type == FollowingNewsType.AUDIO_CONTENT) {
|
||||
handleContentAction(ContentActionCommand.AudioDetail(audioContentId = item.contentId))
|
||||
}
|
||||
is HomeFollowingNewsUiItem.Community -> {
|
||||
val postId = item.postId
|
||||
if (postId <= 0L) return
|
||||
handleCommunityAction(CommunityActionCommand.PostDetail(postId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openFollowingCreatorAll() {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.home.model
|
||||
|
||||
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
|
||||
@@ -123,8 +121,7 @@ private fun FollowingContentNewsResponse.toUiItem(
|
||||
contentImageUrl = contentImageUrl,
|
||||
title = title,
|
||||
creatorProfileImageUrl = creatorProfileImageUrl,
|
||||
creatorNickname = creatorNickname,
|
||||
labelResId = type.toLabelResId()
|
||||
creatorNickname = creatorNickname
|
||||
)
|
||||
|
||||
private fun FollowingCommunityPostNewsResponse.toUiItem(
|
||||
@@ -147,12 +144,3 @@ private fun FollowingCommunityPostNewsResponse.toUiItem(
|
||||
likeCount = likeCount,
|
||||
commentCount = commentCount
|
||||
)
|
||||
|
||||
@StringRes
|
||||
private fun FollowingNewsType.toLabelResId(): Int = when (this) {
|
||||
FollowingNewsType.PHOTO_CONTENT -> R.string.screen_home_following_photo_content
|
||||
FollowingNewsType.AUDIO_CONTENT -> R.string.home_recommendation_activity_audio
|
||||
FollowingNewsType.COMMUNITY_POST -> R.string.home_recommendation_activity_community
|
||||
FollowingNewsType.CREATOR_RANKING,
|
||||
FollowingNewsType.CONTENT_RANKING -> R.string.weekly_chart
|
||||
}
|
||||
|
||||
@@ -81,8 +81,7 @@ sealed interface HomeFollowingNewsUiItem {
|
||||
val contentImageUrl: String?,
|
||||
val title: String,
|
||||
val creatorProfileImageUrl: String,
|
||||
val creatorNickname: String,
|
||||
@param:StringRes val labelResId: Int
|
||||
val creatorNickname: String
|
||||
) : HomeFollowingNewsUiItem
|
||||
|
||||
data class Community(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.home.ui
|
||||
|
||||
import android.widget.ImageView
|
||||
import coil.dispose
|
||||
import coil.transform.CircleCropTransformation
|
||||
import coil.transform.Transformation
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
@@ -9,6 +10,7 @@ fun homeCreatorProfileImageTransformations(): List<Transformation> = listOf(Circ
|
||||
|
||||
fun ImageView.loadHomeCreatorProfileImage(url: String?) {
|
||||
if (url.isNullOrBlank()) {
|
||||
dispose()
|
||||
setImageDrawable(null)
|
||||
} else {
|
||||
loadUrl(url) {
|
||||
|
||||
@@ -22,7 +22,9 @@ class HomeFollowingChatAdapter(
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_home_following_chat, parent, false)
|
||||
view.layoutParams = recyclerItemLayoutParams(parent)
|
||||
val layoutParams = view.layoutParams as RecyclerView.LayoutParams
|
||||
layoutParams.marginEnd = parent.resources.getDimensionPixelSize(R.dimen.spacing_12)
|
||||
view.layoutParams = layoutParams
|
||||
return ChatViewHolder(view, onClickItem)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,14 +21,15 @@ class HomeFollowingLiveAdapter(
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveViewHolder {
|
||||
return LiveViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.item_home_following_live,
|
||||
parent,
|
||||
false
|
||||
),
|
||||
onClickItem
|
||||
val view = LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.item_home_following_live,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
val layoutParams = view.layoutParams as RecyclerView.LayoutParams
|
||||
layoutParams.marginEnd = parent.resources.getDimensionPixelSize(R.dimen.spacing_12)
|
||||
view.layoutParams = layoutParams
|
||||
return LiveViewHolder(view, onClickItem)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: LiveViewHolder, position: Int) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.home.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.dispose
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsType
|
||||
@@ -19,6 +21,38 @@ import kr.co.vividnext.sodalive.v2.widget.feed.FeedWidthMode
|
||||
import kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
internal fun buildHomeFollowingRankingFeedItem(
|
||||
context: Context,
|
||||
ranking: HomeFollowingNewsUiItem.Ranking
|
||||
): FeedItem.Rank {
|
||||
val rankText = context.getString(R.string.screen_home_following_ranking_rank_format, ranking.rank)
|
||||
val message = ranking.toRankingMessage(context, rankText)
|
||||
return FeedItem.Rank(
|
||||
feedId = ranking.newsId,
|
||||
imageUrl = ranking.imageUrl.orEmpty(),
|
||||
rankText = ranking.rank.toString(),
|
||||
message = message,
|
||||
highlightRanges = listOf(
|
||||
FeedRankHighlight(
|
||||
start = message.lastIndexOf(rankText),
|
||||
endExclusive = message.lastIndexOf(rankText) + rankText.length
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun HomeFollowingNewsUiItem.Ranking.toRankingMessage(context: Context, 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
|
||||
return context.getString(messageResId, subject, rankText)
|
||||
}
|
||||
|
||||
class HomeFollowingNewsAdapter(
|
||||
private val onClickItem: (HomeFollowingNewsUiItem) -> Unit = {}
|
||||
) : RecyclerView.Adapter<HomeFollowingNewsAdapter.NewsViewHolder>() {
|
||||
@@ -73,39 +107,12 @@ 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.imageUrl.orEmpty(),
|
||||
rankText = ranking.rank.toString(),
|
||||
message = message,
|
||||
highlightRanges = listOf(
|
||||
FeedRankHighlight(
|
||||
start = message.indexOf(rankText),
|
||||
endExclusive = message.indexOf(rankText) + rankText.length
|
||||
)
|
||||
)
|
||||
)
|
||||
val feedItem = buildHomeFollowingRankingFeedItem(view.context, ranking)
|
||||
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(
|
||||
@@ -118,7 +125,7 @@ class HomeFollowingNewsAdapter(
|
||||
val community = item as HomeFollowingNewsUiItem.Community
|
||||
val feedItem = FeedItem.Community(
|
||||
feedId = community.newsId,
|
||||
creatorId = community.targetId.toString(),
|
||||
creatorId = "",
|
||||
creatorName = community.creatorNickname,
|
||||
creatorImageUrl = community.creatorProfileImageUrl,
|
||||
postId = community.postId.toString(),
|
||||
@@ -149,7 +156,7 @@ class HomeFollowingNewsAdapter(
|
||||
val content = item as HomeFollowingNewsUiItem.Content
|
||||
val feedItem = FeedItem.Content(
|
||||
feedId = content.newsId,
|
||||
creatorId = content.targetId.toString(),
|
||||
creatorId = "",
|
||||
creatorName = content.creatorNickname,
|
||||
creatorImageUrl = content.creatorProfileImageUrl,
|
||||
contentId = content.contentId.toString(),
|
||||
@@ -184,6 +191,7 @@ class HomeFollowingNewsAdapter(
|
||||
onImageLoaded: ((Int, Int) -> Unit)? = null
|
||||
) {
|
||||
if (url.isBlank()) {
|
||||
imageView.dispose()
|
||||
imageView.setImageDrawable(null)
|
||||
} else {
|
||||
imageView.loadUrl(url) {
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.v2.widget.feed
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class FeedAdapter(
|
||||
private val widthMode: FeedWidthMode = FeedWidthMode.FigmaFixed,
|
||||
private val horizontalItemDecorationDp: Int = 0,
|
||||
private val onClickItem: (FeedItem) -> Unit,
|
||||
private val onBindImages: (FeedImageViews, FeedItem) -> Unit = { _, _ -> }
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val items = mutableListOf<FeedItem>()
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (items[position].variant) {
|
||||
FeedVariant.Rank -> VIEW_TYPE_RANK
|
||||
FeedVariant.Live -> VIEW_TYPE_LIVE
|
||||
FeedVariant.Content -> VIEW_TYPE_CONTENT
|
||||
FeedVariant.Community -> VIEW_TYPE_COMMUNITY
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_RANK -> RankViewHolder(inflater.inflate(R.layout.view_feed_rank, parent, false) as FeedRankView, parent)
|
||||
VIEW_TYPE_LIVE -> LiveViewHolder(inflater.inflate(R.layout.view_feed_live, parent, false) as FeedLiveView, parent)
|
||||
VIEW_TYPE_CONTENT -> ContentViewHolder(
|
||||
inflater.inflate(R.layout.view_feed_content, parent, false) as FeedContentView,
|
||||
parent
|
||||
)
|
||||
VIEW_TYPE_COMMUNITY -> CommunityViewHolder(
|
||||
inflater.inflate(R.layout.view_feed_community, parent, false) as FeedCommunityView,
|
||||
parent
|
||||
)
|
||||
else -> error("Unknown viewType: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (val item = items[position]) {
|
||||
is FeedItem.Rank -> (holder as RankViewHolder).bind(item)
|
||||
is FeedItem.Live -> (holder as LiveViewHolder).bind(item)
|
||||
is FeedItem.Content -> (holder as ContentViewHolder).bind(item)
|
||||
is FeedItem.Community -> (holder as CommunityViewHolder).bind(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
fun submitItems(items: List<FeedItem>) {
|
||||
this.items.clear()
|
||||
this.items.addAll(items)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private inner class RankViewHolder(
|
||||
private val view: FeedRankView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: FeedItem.Rank) {
|
||||
view.setFeedSize(calculateSize(item.variant, parent))
|
||||
view.bind(item)
|
||||
view.setOnFeedClick(onClickItem)
|
||||
onBindImages(FeedImageViews(primary = view.imageView()), item)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class LiveViewHolder(
|
||||
private val view: FeedLiveView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: FeedItem.Live) {
|
||||
view.setFeedSize(calculateSize(item.variant, parent))
|
||||
view.bind(item)
|
||||
view.setOnFeedClick(onClickItem)
|
||||
onBindImages(FeedImageViews(profile = view.profileImageView()), item)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ContentViewHolder(
|
||||
private val view: FeedContentView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: FeedItem.Content) {
|
||||
view.setFeedSize(calculateSize(item.variant, parent))
|
||||
view.bind(item)
|
||||
view.setOnFeedClick(onClickItem)
|
||||
onBindImages(FeedImageViews(primary = view.contentImageView(), profile = view.profileImageView()), item)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class CommunityViewHolder(
|
||||
private val view: FeedCommunityView,
|
||||
private val parent: ViewGroup
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: FeedItem.Community) {
|
||||
view.setFeedSize(calculateSize(item.variant, parent))
|
||||
view.bind(item)
|
||||
view.setOnFeedClick(onClickItem)
|
||||
onBindImages(FeedImageViews(primary = view.communityImageView(), profile = view.profileImageView()), item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateSize(variant: FeedVariant, parent: ViewGroup): FeedSize {
|
||||
val parentWidthPx = parent.width.takeIf { it > 0 } ?: parent.resources.displayMetrics.widthPixels
|
||||
val availableWidthPx = parentWidthPx - parent.paddingLeft - parent.paddingRight
|
||||
val availableWidthDp = (availableWidthPx / parent.resources.displayMetrics.density).roundToInt()
|
||||
return FeedSize.from(variant, widthMode, availableWidthDp, horizontalItemDecorationDp)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VIEW_TYPE_RANK = 1
|
||||
private const val VIEW_TYPE_LIVE = 2
|
||||
private const val VIEW_TYPE_CONTENT = 3
|
||||
private const val VIEW_TYPE_COMMUNITY = 4
|
||||
}
|
||||
}
|
||||
|
||||
data class FeedImageViews(
|
||||
val primary: ImageView? = null,
|
||||
val profile: ImageView? = null
|
||||
)
|
||||
@@ -365,7 +365,7 @@
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/spacing_14"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_home_following_news_content" />
|
||||
tools:listitem="@layout/view_feed_content" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
<?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:background="@drawable/bg_feed_card"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/spacing_14">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_home_following_news_creator_profile"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:background="@drawable/bg_round_corner_999_263238"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/ic_placeholder_profile" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_news_creator_nickname"
|
||||
style="@style/Typography.Body5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
tools:text="크리에이터 이름" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_news_created_at"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/gray_500"
|
||||
tools:text="2분 전" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_news_title"
|
||||
style="@style/Typography.Heading4"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_14"
|
||||
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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_news_label"
|
||||
style="@style/Typography.Caption3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_feed_category_tag"
|
||||
android:includeFontPadding="false"
|
||||
android:paddingHorizontal="@dimen/spacing_4"
|
||||
android:paddingVertical="2dp"
|
||||
android:textColor="@color/gray_100"
|
||||
tools:text="오디오" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_12"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_news_content"
|
||||
style="@style/Typography.Body3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="4"
|
||||
android:textColor="@color/white"
|
||||
tools:text="크리에이터가 남긴 최근 소식이 노출됩니다. 긴 내용은 말줄임 처리합니다." />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_home_following_news_thumbnail"
|
||||
android:layout_width="92dp"
|
||||
android:layout_height="92dp"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:background="@drawable/bg_feed_community_image"
|
||||
android:contentDescription="@null"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/ic_launcher_background" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -1,42 +0,0 @@
|
||||
<?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:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="@dimen/spacing_8">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_news_rank"
|
||||
style="@style/Typography.Heading3"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/green_400"
|
||||
tools:text="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_news_rank_title"
|
||||
style="@style/Typography.Body4"
|
||||
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/white"
|
||||
tools:text="최근 소식 제목" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_home_following_news_rank_count"
|
||||
style="@style/Typography.Body6"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_8"
|
||||
android:includeFontPadding="false"
|
||||
android:textColor="@color/gray_500"
|
||||
tools:text="999" />
|
||||
</LinearLayout>
|
||||
@@ -309,17 +309,15 @@
|
||||
<string name="home_recommendation_section_genre_creator_suffix">\u0020creators</string>
|
||||
<string name="home_recommendation_section_cheer_creators">Creators with recent cheers</string>
|
||||
<string name="home_recommendation_section_popular_community_posts">Popular community</string>
|
||||
<string name="screen_home_following_creators_title">Following creators</string>
|
||||
<string name="screen_home_following_on_air_title">On Air</string>
|
||||
<string name="screen_home_following_recent_chats_title">Recent chats</string>
|
||||
<string name="screen_home_following_monthly_schedules_title">Monthly schedule</string>
|
||||
<string name="screen_home_following_recent_news_title">Recent news</string>
|
||||
<string name="screen_home_following_on_air">On Air</string>
|
||||
<string name="screen_home_following_photo_content">Photo</string>
|
||||
<string name="screen_home_following_ranking_rank_format">No. %1$d</string>
|
||||
<string name="screen_home_following_creator_ranking_news_message">%1$s reached %2$s in this week\'s ranking!</string>
|
||||
<string name="screen_home_following_content_ranking_news_message">%1$s reached %2$s in this week\'s ranking!</string>
|
||||
<string name="screen_home_following_empty">No following updates yet.\nFollow creators you are interested in.</string>
|
||||
<string name="screen_home_following_error">Could not load following news.</string>
|
||||
<string name="home_recommendation_activity_live">Live</string>
|
||||
<string name="home_recommendation_activity_audio">Audio</string>
|
||||
<string name="home_recommendation_activity_community">Community</string>
|
||||
|
||||
@@ -309,17 +309,15 @@
|
||||
<string name="home_recommendation_section_genre_creator_suffix">のクリエイター</string>
|
||||
<string name="home_recommendation_section_cheer_creators">最近応援が多いクリエイター</string>
|
||||
<string name="home_recommendation_section_popular_community_posts">人気コミュニティ</string>
|
||||
<string name="screen_home_following_creators_title">フォロー中のクリエイター</string>
|
||||
<string name="screen_home_following_on_air_title">On Air</string>
|
||||
<string name="screen_home_following_recent_chats_title">最近のチャット</string>
|
||||
<string name="screen_home_following_monthly_schedules_title">今月のスケジュール</string>
|
||||
<string name="screen_home_following_recent_news_title">最近のお知らせ</string>
|
||||
<string name="screen_home_following_on_air">On Air</string>
|
||||
<string name="screen_home_following_photo_content">グラビア</string>
|
||||
<string name="screen_home_following_ranking_rank_format">%1$d位</string>
|
||||
<string name="screen_home_following_creator_ranking_news_message">%1$sさんが今週のランキング%2$sに入りました!</string>
|
||||
<string name="screen_home_following_content_ranking_news_message">%1$sが今週のランキング%2$sに入りました!</string>
|
||||
<string name="screen_home_following_empty">フォロー中のお知らせはまだありません。\n気になるクリエイターをフォローしてみましょう。</string>
|
||||
<string name="screen_home_following_error">フォロー中のお知らせを読み込めませんでした。</string>
|
||||
<string name="home_recommendation_activity_live">ライブ</string>
|
||||
<string name="home_recommendation_activity_audio">オーディオ</string>
|
||||
<string name="home_recommendation_activity_community">コミュニティ</string>
|
||||
|
||||
@@ -308,17 +308,15 @@
|
||||
<string name="home_recommendation_section_genre_creator_suffix">\u0020크리에이터</string>
|
||||
<string name="home_recommendation_section_cheer_creators">최근 응원이 많은 크리에이터</string>
|
||||
<string name="home_recommendation_section_popular_community_posts">인기 커뮤니티</string>
|
||||
<string name="screen_home_following_creators_title">팔로잉 크리에이터</string>
|
||||
<string name="screen_home_following_on_air_title">On Air</string>
|
||||
<string name="screen_home_following_recent_chats_title">최근 대화</string>
|
||||
<string name="screen_home_following_monthly_schedules_title">이달의 스케줄</string>
|
||||
<string name="screen_home_following_recent_news_title">최근 소식</string>
|
||||
<string name="screen_home_following_on_air">On Air</string>
|
||||
<string name="screen_home_following_photo_content">화보</string>
|
||||
<string name="screen_home_following_ranking_rank_format">%1$d위</string>
|
||||
<string name="screen_home_following_creator_ranking_news_message">%1$s님이 이번 주 랭킹 %2$s에 올랐어요!</string>
|
||||
<string name="screen_home_following_content_ranking_news_message">%1$s 콘텐츠가 이번 주 랭킹 %2$s에 올랐어요!</string>
|
||||
<string name="screen_home_following_empty">아직 팔로잉 소식이 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.</string>
|
||||
<string name="screen_home_following_error">팔로잉 소식을 불러오지 못했습니다.</string>
|
||||
<string name="home_recommendation_activity_live">라이브</string>
|
||||
<string name="home_recommendation_activity_audio">오디오</string>
|
||||
<string name="home_recommendation_activity_community">커뮤니티</string>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.home
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.widget.ImageView
|
||||
import coil.load
|
||||
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
|
||||
import kr.co.vividnext.sodalive.v2.main.home.ui.loadHomeCreatorProfileImage
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], application = Application::class)
|
||||
class HomeCreatorProfileImageLoaderTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
if (!ImageLoaderProvider.isInitialized) {
|
||||
ImageLoaderProvider.init(RuntimeEnvironment.getApplication())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blank 프로필 이미지는 이전 Coil 요청을 해제하고 drawable을 비운다`() {
|
||||
val imageView = ImageView(RuntimeEnvironment.getApplication())
|
||||
val disposable = imageView.load(ColorDrawable(Color.RED), ImageLoaderProvider.imageLoader)
|
||||
assertFalse(disposable.isDisposed)
|
||||
|
||||
imageView.loadHomeCreatorProfileImage(" ")
|
||||
|
||||
assertTrue(disposable.isDisposed)
|
||||
assertNull(imageView.drawable)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import java.io.File
|
||||
class HomeFollowingFragmentSourceTest {
|
||||
|
||||
@Test
|
||||
fun `home layout exposes following content and section recycler ids`() {
|
||||
fun `홈 layout은 팔로잉 content와 section recycler id를 노출한다`() {
|
||||
val layout = homeMainLayoutSource()
|
||||
|
||||
assertTrue(layout.contains("@+id/nsv_home_following_content"))
|
||||
@@ -20,7 +20,7 @@ class HomeFollowingFragmentSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `home main fragment injects following viewmodel and wires adapters`() {
|
||||
fun `홈 main fragment는 팔로잉 viewmodel과 adapter를 연결한다`() {
|
||||
val source = homeMainFragmentSource()
|
||||
|
||||
assertTrue(source.contains("HomeFollowingViewModel"))
|
||||
@@ -38,7 +38,7 @@ class HomeFollowingFragmentSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following tab branch shows following content and hides other home surfaces`() {
|
||||
fun `팔로잉 탭 분기는 팔로잉 content만 표시한다`() {
|
||||
val source = homeMainFragmentSource()
|
||||
val branch = source.substringAfter("private fun showHomeTab(index: Int)")
|
||||
.substringAfter("HOME_TAB_FOLLOWING ->")
|
||||
@@ -50,7 +50,7 @@ class HomeFollowingFragmentSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following tab loads following content only once`() {
|
||||
fun `팔로잉 탭은 팔로잉 content를 최초 한 번만 로드한다`() {
|
||||
val source = homeMainFragmentSource()
|
||||
|
||||
assertTrue(source.contains("private var hasLoadedFollowing = false"))
|
||||
@@ -60,7 +60,7 @@ class HomeFollowingFragmentSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following section chevrons call callback without starting activity`() {
|
||||
fun `팔로잉 section chevron은 이동 없이 callback만 호출한다`() {
|
||||
val source = homeMainFragmentSource()
|
||||
val callback = source.substringAfter("private fun onFollowingSectionMoreClick")
|
||||
.substringBefore("\n private fun")
|
||||
@@ -77,7 +77,7 @@ class HomeFollowingFragmentSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following state binding clears or binds each following section`() {
|
||||
fun `팔로잉 상태 binding은 section을 비우거나 바인딩한다`() {
|
||||
val source = homeMainFragmentSource()
|
||||
|
||||
assertTrue(source.contains("followingStateLiveData.observe(viewLifecycleOwner)"))
|
||||
@@ -85,7 +85,10 @@ class HomeFollowingFragmentSourceTest {
|
||||
assertTrue(source.contains("bindHomeFollowingContent(state)"))
|
||||
assertTrue(source.contains("HomeFollowingUiState.Empty ->"))
|
||||
assertTrue(source.contains("bindHomeFollowingEmpty(showEmptyMessage = true)"))
|
||||
assertTrue(source.contains("HomeFollowingUiState.LoginRequired -> bindHomeFollowingEmpty(showEmptyMessage = false)"))
|
||||
val loginRequiredSource = source.substringAfter("HomeFollowingUiState.LoginRequired ->")
|
||||
.substringBefore("is HomeFollowingUiState.Error ->")
|
||||
assertTrue(loginRequiredSource.contains("bindHomeFollowingEmpty(showEmptyMessage = false)"))
|
||||
assertTrue(loginRequiredSource.contains("finishHomePullRefresh(HOME_TAB_FOLLOWING)"))
|
||||
assertTrue(source.contains("is HomeFollowingUiState.Error ->"))
|
||||
assertTrue(source.contains("if (shouldPreserveHomeContentOnPullRefreshError(HOME_TAB_FOLLOWING)) return@observe"))
|
||||
assertTrue(source.contains("bindHomeFollowingEmpty(showEmptyMessage = false)"))
|
||||
@@ -102,7 +105,7 @@ class HomeFollowingFragmentSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following empty state shows centered reusable empty string only for all empty content`() {
|
||||
fun `팔로잉 empty 상태는 다국어 빈 문구와 상태 분기를 유지한다`() {
|
||||
val layout = homeMainLayoutSource()
|
||||
val emptyText = layout.substringAfter("@+id/tv_home_following_empty")
|
||||
.substringBefore("@+id/ll_home_following_creators_section")
|
||||
@@ -112,12 +115,7 @@ class HomeFollowingFragmentSourceTest {
|
||||
val stringsJa = projectFile("app/src/main/res/values-ja/strings.xml").readText()
|
||||
|
||||
assertTrue(layout.contains("@+id/tv_home_following_empty"))
|
||||
assertTrue(emptyText.contains("android:layout_width=\"match_parent\""))
|
||||
assertTrue(emptyText.contains("android:layout_height=\"match_parent\""))
|
||||
assertTrue(emptyText.contains("android:gravity=\"center\""))
|
||||
assertTrue(emptyText.contains("android:textAlignment=\"center\""))
|
||||
assertTrue(emptyText.contains("android:text=\"@string/screen_home_following_empty\""))
|
||||
assertTrue(emptyText.contains("android:visibility=\"gone\""))
|
||||
|
||||
assertTrue(
|
||||
strings.contains(
|
||||
@@ -138,8 +136,6 @@ class HomeFollowingFragmentSourceTest {
|
||||
)
|
||||
)
|
||||
|
||||
assertTrue(source.contains("binding.tvHomeFollowingEmpty.visibility = View.GONE"))
|
||||
assertTrue(source.contains("binding.tvHomeFollowingEmpty.visibility = if (showEmptyMessage) View.VISIBLE else View.GONE"))
|
||||
assertTrue(source.contains("HomeFollowingUiState.Empty ->"))
|
||||
assertTrue(source.contains("bindHomeFollowingEmpty(showEmptyMessage = true)"))
|
||||
assertTrue(source.contains("is HomeFollowingUiState.Error ->"))
|
||||
@@ -147,7 +143,7 @@ class HomeFollowingFragmentSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following adapters bind figma required item fields`() {
|
||||
fun `팔로잉 adapter는 필수 item field를 바인딩한다`() {
|
||||
val liveAdapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingLiveAdapter.kt"
|
||||
).readText()
|
||||
@@ -183,7 +179,7 @@ class HomeFollowingFragmentSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following recent news hides chevron and reuses feed widgets by item type`() {
|
||||
fun `팔로잉 최근 소식은 chevron을 숨기고 feed widget을 재사용한다`() {
|
||||
val layout = homeMainLayoutSource()
|
||||
val recentNewsSection = layout.substringAfter("@+id/ll_home_following_recent_news_section")
|
||||
.substringBefore("</LinearLayout>")
|
||||
@@ -215,10 +211,13 @@ class HomeFollowingFragmentSourceTest {
|
||||
assertTrue(adapter.contains("FeedItem.Content"))
|
||||
assertFalse(adapter.contains("R.layout.item_home_following_news_content"))
|
||||
assertFalse(adapter.contains("R.layout.item_home_following_news_rank"))
|
||||
assertTrue(recentNewsSection.contains("tools:listitem=\"@layout/view_feed_content\""))
|
||||
assertTrue(projectFileOrNull("app/src/main/res/layout/item_home_following_news_content.xml") == null)
|
||||
assertTrue(projectFileOrNull("app/src/main/res/layout/item_home_following_news_rank.xml") == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following ranking news builds sentence message and highlights rank text`() {
|
||||
fun `팔로잉 ranking 소식은 문장 message와 순위 강조를 만든다`() {
|
||||
val adapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
|
||||
).readText()
|
||||
@@ -229,14 +228,15 @@ class HomeFollowingFragmentSourceTest {
|
||||
assertTrue(adapter.contains("context.getString("))
|
||||
assertTrue(adapter.contains("highlightRanges = listOf("))
|
||||
assertTrue(adapter.contains("FeedRankHighlight("))
|
||||
assertTrue(adapter.contains("start = message.indexOf(rankText)"))
|
||||
assertTrue(adapter.contains("endExclusive = message.indexOf(rankText) + rankText.length"))
|
||||
assertTrue(adapter.contains("start = message.lastIndexOf(rankText)"))
|
||||
assertTrue(adapter.contains("endExclusive = message.lastIndexOf(rankText) + rankText.length"))
|
||||
assertTrue(adapter.contains("R.string.screen_home_following_ranking_rank_format"))
|
||||
assertFalse(adapter.contains("message = ranking.body.ifBlank { ranking.title }"))
|
||||
assertFalse(adapter.contains("highlightRanges = emptyList()"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following recent news models use nested payload response contract`() {
|
||||
fun `팔로잉 최근 소식 model은 nested payload 응답 계약을 사용한다`() {
|
||||
val models = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingModels.kt"
|
||||
).readText()
|
||||
@@ -266,7 +266,7 @@ class HomeFollowingFragmentSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following recent news adapter binds nested payload ui variants`() {
|
||||
fun `팔로잉 최근 소식 adapter는 nested payload UI variant를 바인딩한다`() {
|
||||
val adapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
|
||||
).readText()
|
||||
@@ -289,20 +289,34 @@ class HomeFollowingFragmentSourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following creators section has no header and uses figma simple profile size`() {
|
||||
fun `팔로잉 최근 소식 feed model은 미제공 creator id를 합성하지 않는다`() {
|
||||
val adapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt"
|
||||
).readText()
|
||||
|
||||
assertFalse(adapter.contains("creatorId = community.targetId.toString()"))
|
||||
assertFalse(adapter.contains("creatorId = content.targetId.toString()"))
|
||||
assertTrue(adapter.contains("creatorId = \"\""))
|
||||
assertTrue(adapter.contains("postId = community.postId.toString()"))
|
||||
assertTrue(adapter.contains("contentId = content.contentId.toString()"))
|
||||
assertTrue(adapter.contains("view.setOnFeedClick { onClickItem(community) }"))
|
||||
assertTrue(adapter.contains("view.setOnFeedClick { onClickItem(content) }"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `팔로잉 크리에이터 섹션은 header 없이 item layout을 사용한다`() {
|
||||
val layout = homeMainLayoutSource()
|
||||
val section = layout.substringAfter("@+id/ll_home_following_creators_section")
|
||||
.substringBefore("@+id/ll_home_following_on_air_section")
|
||||
val itemLayout = projectFile("app/src/main/res/layout/item_home_following_creator.xml").readText()
|
||||
|
||||
assertFalse(section.contains("view_home_following_creators_title"))
|
||||
assertTrue(itemLayout.contains("android:layout_width=\"75dp\""))
|
||||
assertTrue(itemLayout.contains("android:layout_height=\"75dp\""))
|
||||
assertTrue(itemLayout.contains("@style/Typography.Body5"))
|
||||
assertTrue(itemLayout.contains("@+id/iv_home_following_creator_profile"))
|
||||
assertTrue(itemLayout.contains("@+id/tv_home_following_creator_nickname"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following creators list appends all button as last item`() {
|
||||
fun `팔로잉 크리에이터 목록은 전체 item을 마지막에 추가하고 이동을 연결한다`() {
|
||||
val adapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingCreatorAdapter.kt"
|
||||
).readText()
|
||||
@@ -324,48 +338,36 @@ class HomeFollowingFragmentSourceTest {
|
||||
assertTrue(fragment.contains("onClickAll = { openFollowingCreatorAll() }"))
|
||||
assertTrue(fragment.contains("private fun openFollowingCreatorAll()"))
|
||||
assertTrue(fragment.contains("startActivity(Intent(requireContext(), FollowingCreatorActivity::class.java))"))
|
||||
assertTrue(allLayout.contains("android:layout_width=\"wrap_content\""))
|
||||
assertTrue(allLayout.contains("android:layout_height=\"wrap_content\""))
|
||||
assertTrue(allLayout.contains("android:layout_height=\"75dp\""))
|
||||
assertTrue(allLayout.contains("android:paddingHorizontal=\"16dp\""))
|
||||
assertTrue(allLayout.contains("android:textColor=\"@color/soda_400\""))
|
||||
assertTrue(allLayout.contains("android:visibility=\"invisible\""))
|
||||
assertTrue(allLayout.contains("@string/screen_home_theme_all"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following recent chats section is horizontal box list`() {
|
||||
fun `팔로잉 최근 대화 섹션은 RecyclerView와 adapter를 연결한다`() {
|
||||
val layout = homeMainLayoutSource()
|
||||
val section = layout.substringAfter("@+id/rv_home_following_recent_chats")
|
||||
.substringBefore("@+id/ll_home_following_monthly_schedules_section")
|
||||
val fragment = homeMainFragmentSource()
|
||||
|
||||
assertTrue(section.contains("android:orientation=\"horizontal\""))
|
||||
assertTrue(layout.contains("@+id/rv_home_following_recent_chats"))
|
||||
assertTrue(fragment.contains("rvHomeFollowingRecentChats.apply"))
|
||||
assertTrue(fragment.contains("LinearLayoutManager.HORIZONTAL"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `following recent chat item matches figma box fields`() {
|
||||
fun `팔로잉 최근 대화 item은 필수 field와 클릭 계약을 바인딩한다`() {
|
||||
val adapter = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingChatAdapter.kt"
|
||||
).readText()
|
||||
val itemLayout = projectFile("app/src/main/res/layout/item_home_following_chat.xml").readText()
|
||||
|
||||
assertTrue(itemLayout.contains("android:layout_width=\"284dp\""))
|
||||
assertTrue(itemLayout.contains("android:layout_width=\"62dp\""))
|
||||
assertTrue(itemLayout.contains("android:layout_height=\"62dp\""))
|
||||
assertTrue(itemLayout.contains("@+id/iv_home_following_chat_creator_profile"))
|
||||
assertTrue(itemLayout.contains("@+id/tv_home_following_chat_creator_nickname"))
|
||||
assertTrue(itemLayout.contains("@+id/tv_home_following_chat_message"))
|
||||
assertTrue(itemLayout.contains("@+id/tv_home_following_chat_time"))
|
||||
assertTrue(itemLayout.contains("@+id/tv_home_following_chat_direct_badge"))
|
||||
assertTrue(itemLayout.contains("@drawable/bg_chat_direct_badge"))
|
||||
assertTrue(itemLayout.contains("@string/screen_chat_direct_badge"))
|
||||
val messageText = itemLayout.substringAfter("@+id/tv_home_following_chat_message")
|
||||
.substringBefore("@+id/tv_home_following_chat_creator_nickname")
|
||||
assertTrue(messageText.contains("android:maxLines=\"1\""))
|
||||
assertTrue(messageText.contains("android:ellipsize=\"end\""))
|
||||
assertFalse(adapter.contains("ViewGroup.LayoutParams.MATCH_PARENT"))
|
||||
assertTrue(adapter.contains("recyclerItemLayoutParams(parent)"))
|
||||
assertTrue(adapter.contains("directBadgeText.visibility = if (item.showDirectBadge)"))
|
||||
assertTrue(adapter.contains("formatChatRoomLastMessageTime(itemView.context, item.lastMessageAt)"))
|
||||
assertTrue(adapter.contains("itemView.setOnClickListener { onClickItem(item) }"))
|
||||
}
|
||||
|
||||
private fun homeMainFragmentSource(): String {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.home
|
||||
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
|
||||
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
|
||||
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomListItemResponse
|
||||
@@ -75,16 +74,6 @@ class HomeFollowingMapperTest {
|
||||
assertEquals(listOf("second", "first"), state.monthlySchedules.items.map { it.scheduleId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PHOTO_CONTENT는 photo content label로 매핑된다`() {
|
||||
val state = response(
|
||||
recentNews = listOf(photoContentNews())
|
||||
).toUiState(formatter) as HomeFollowingUiState.Content
|
||||
val item = state.recentNews.items.single() as HomeFollowingNewsUiItem.Content
|
||||
|
||||
assertEquals(R.string.screen_home_following_photo_content, item.labelResId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `recentNews nested payload는 type별 UI item으로 매핑된다`() {
|
||||
val state = response(
|
||||
@@ -121,7 +110,6 @@ class HomeFollowingMapperTest {
|
||||
val photoContent = state.recentNews.items[3] as HomeFollowingNewsUiItem.Content
|
||||
assertEquals(FollowingNewsType.PHOTO_CONTENT, photoContent.type)
|
||||
assertEquals(3002L, photoContent.contentId)
|
||||
assertEquals(R.string.screen_home_following_photo_content, photoContent.labelResId)
|
||||
|
||||
val community = state.recentNews.items[4] as HomeFollowingNewsUiItem.Community
|
||||
assertEquals(4001L, community.postId)
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.home
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import coil.load
|
||||
import kr.co.vividnext.sodalive.R
|
||||
import kr.co.vividnext.sodalive.common.ImageLoaderProvider
|
||||
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.main.home.ui.HomeFollowingNewsAdapter
|
||||
import kr.co.vividnext.sodalive.v2.main.home.ui.buildHomeFollowingRankingFeedItem
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
import java.util.Locale
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], application = Application::class)
|
||||
class HomeFollowingNewsAdapterTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
if (!ImageLoaderProvider.isInitialized) {
|
||||
ImageLoaderProvider.init(RuntimeEnvironment.getApplication())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ranking 문장은 locale별 순위 단위를 사용하고 같은 문자열을 강조한다`() {
|
||||
assertRankingText(locale = Locale.KOREAN, expectedRankText = "7위")
|
||||
assertRankingText(locale = Locale.ENGLISH, expectedRankText = "No. 7")
|
||||
assertRankingText(locale = Locale.JAPANESE, expectedRankText = "7位")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ranking 제목에 같은 순위 문자열이 있어도 삽입된 순위 문자열을 강조한다`() {
|
||||
val feedItem = buildHomeFollowingRankingFeedItem(
|
||||
localizedContext(Locale.KOREAN),
|
||||
ranking(creatorNickname = "7위 크리에이터")
|
||||
)
|
||||
|
||||
val highlight = feedItem.highlightRanges.single()
|
||||
assertEquals(feedItem.message.lastIndexOf("7위"), highlight.start)
|
||||
assertEquals("7위", feedItem.message.substring(highlight.start, highlight.endExclusive))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nullable content 이미지는 재사용된 ImageView의 이전 Coil 요청을 해제한다`() {
|
||||
val adapter = HomeFollowingNewsAdapter()
|
||||
adapter.submitItems(listOf(content(contentImageUrl = null)))
|
||||
val parent = FrameLayout(RuntimeEnvironment.getApplication()).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(360, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
val holder = adapter.onCreateViewHolder(parent, adapter.getItemViewType(0))
|
||||
val imageView = holder.itemView.findViewById<ImageView>(R.id.iv_feed_content_image)
|
||||
val disposable = imageView.load(ColorDrawable(Color.BLUE), ImageLoaderProvider.imageLoader)
|
||||
assertFalse(disposable.isDisposed)
|
||||
|
||||
adapter.onBindViewHolder(holder, 0)
|
||||
|
||||
assertTrue(disposable.isDisposed)
|
||||
assertNull(imageView.drawable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PHOTO_CONTENT는 Feed photo category label로 표시된다`() {
|
||||
val adapter = HomeFollowingNewsAdapter()
|
||||
adapter.submitItems(listOf(content(type = FollowingNewsType.PHOTO_CONTENT)))
|
||||
val parent = FrameLayout(RuntimeEnvironment.getApplication()).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(360, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
val holder = adapter.onCreateViewHolder(parent, adapter.getItemViewType(0))
|
||||
|
||||
adapter.onBindViewHolder(holder, 0)
|
||||
|
||||
val categoryText = holder.itemView.findViewById<TextView>(R.id.tv_feed_content_category)
|
||||
assertEquals(parent.context.getString(R.string.screen_home_following_photo_content), categoryText.text.toString())
|
||||
}
|
||||
|
||||
private fun assertRankingText(locale: Locale, expectedRankText: String) {
|
||||
val feedItem = buildHomeFollowingRankingFeedItem(localizedContext(locale), ranking())
|
||||
|
||||
assertEquals("7", feedItem.rankText)
|
||||
assertFalse(feedItem.message.contains("7위") && expectedRankText != "7위")
|
||||
val highlight = feedItem.highlightRanges.single()
|
||||
assertEquals(expectedRankText, feedItem.message.substring(highlight.start, highlight.endExclusive))
|
||||
}
|
||||
|
||||
private fun localizedContext(locale: Locale): Context {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val configuration = Configuration(context.resources.configuration)
|
||||
configuration.setLocale(locale)
|
||||
return context.createConfigurationContext(configuration)
|
||||
}
|
||||
|
||||
private fun ranking(creatorNickname: String = "creator") = HomeFollowingNewsUiItem.Ranking(
|
||||
newsId = "news-1",
|
||||
type = FollowingNewsType.CREATOR_RANKING,
|
||||
targetId = 1L,
|
||||
visibleFromAtUtc = "2026-07-30T00:00:00Z",
|
||||
visibleFromText = "1분 전",
|
||||
rank = 7,
|
||||
imageUrl = "https://example.com/rank.png",
|
||||
title = "ranking title",
|
||||
creatorNickname = creatorNickname
|
||||
)
|
||||
|
||||
private fun content(
|
||||
contentImageUrl: String? = "https://example.com/content.png",
|
||||
type: FollowingNewsType = FollowingNewsType.AUDIO_CONTENT
|
||||
) = HomeFollowingNewsUiItem.Content(
|
||||
newsId = "news-content",
|
||||
type = type,
|
||||
targetId = 10L,
|
||||
visibleFromAtUtc = "2026-07-30T00:00:00Z",
|
||||
visibleFromText = "1분 전",
|
||||
contentId = 10L,
|
||||
contentImageUrl = contentImageUrl,
|
||||
title = "content title",
|
||||
creatorProfileImageUrl = "https://example.com/profile.png",
|
||||
creatorNickname = "creator"
|
||||
)
|
||||
}
|
||||
@@ -56,10 +56,16 @@ class HomeFollowingViewModelTest {
|
||||
@Test
|
||||
fun `loadFollowing은 loading 후 content 상태를 발행한다`() {
|
||||
api.enqueueSuccess(response(followingCreators = listOf(creator())))
|
||||
val states = mutableListOf<HomeFollowingUiState>()
|
||||
val observer = Observer<HomeFollowingUiState> { states.add(it) }
|
||||
|
||||
viewModel.followingStateLiveData.observeForever(observer)
|
||||
viewModel.loadFollowing()
|
||||
viewModel.followingStateLiveData.removeObserver(observer)
|
||||
|
||||
val state = viewModel.followingStateLiveData.requireValue() as HomeFollowingUiState.Content
|
||||
assertTrue(states.first() is HomeFollowingUiState.Loading)
|
||||
assertTrue(states.last() is HomeFollowingUiState.Content)
|
||||
assertEquals(1L, state.followingCreators.items.single().creatorId)
|
||||
assertFalse(viewModel.isLoading.requireValue() ?: true)
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ class HomeMainFragmentLoginGuardSourceTest {
|
||||
).readText()
|
||||
|
||||
val clickSource = source.substringFrom("private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem)")
|
||||
assertTrue(clickSource.contains("val community = item as? HomeFollowingNewsUiItem.Community ?: return"))
|
||||
assertTrue(clickSource.contains("val postId = community.postId"))
|
||||
assertTrue(clickSource.contains("is HomeFollowingNewsUiItem.Community -> {"))
|
||||
assertTrue(clickSource.contains("val postId = item.postId"))
|
||||
assertBefore(
|
||||
clickSource,
|
||||
"val community = item as? HomeFollowingNewsUiItem.Community ?: return",
|
||||
"is HomeFollowingNewsUiItem.Community -> {",
|
||||
"handleCommunityAction"
|
||||
)
|
||||
assertBefore(
|
||||
@@ -75,6 +75,34 @@ class HomeMainFragmentLoginGuardSourceTest {
|
||||
assertFalse(clickSource.contains("EXTRA_COMMUNITY_POST_ID"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HomeMainFragment 팔로잉 최근 소식 CREATOR_RANKING은 크리에이터 채널로 이동한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||
).readText()
|
||||
|
||||
val clickSource = source.substringFrom("private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem)")
|
||||
assertTrue(clickSource.contains("is HomeFollowingNewsUiItem.Ranking ->"))
|
||||
assertTrue(clickSource.contains("item.type == FollowingNewsType.CREATOR_RANKING"))
|
||||
assertTrue(clickSource.contains("handleCreatorAction(CreatorActionCommand.Profile(item.targetId))"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HomeMainFragment 팔로잉 최근 소식 AUDIO_CONTENT는 오디오 상세로 이동한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||
).readText()
|
||||
|
||||
val clickSource = source.substringFrom("private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem)")
|
||||
assertTrue(clickSource.contains("is HomeFollowingNewsUiItem.Content ->"))
|
||||
assertTrue(clickSource.contains("item.type == FollowingNewsType.AUDIO_CONTENT"))
|
||||
assertTrue(
|
||||
clickSource.contains(
|
||||
"handleContentAction(ContentActionCommand.AudioDetail(audioContentId = item.contentId))"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HomeMainFragment 팔로잉 탭 선택은 로그인 가드 통과 후 탭을 전환한다`() {
|
||||
val source = projectFile(
|
||||
@@ -125,6 +153,34 @@ class HomeMainFragmentLoginGuardSourceTest {
|
||||
assertTrue(source.contains("private val liveActionCoordinator: LiveActionCoordinator by lazy"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HomeMainFragment 팔로잉 On Air 아이템은 공통 Live Action으로 입장한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||
).readText()
|
||||
|
||||
val clickSource = source.substringFrom("private fun onFollowingLiveClick(item: HomeFollowingLiveUiItem)")
|
||||
assertTrue(clickSource.contains("liveActionCoordinator.enterLiveRoom(item.liveId)"))
|
||||
assertFalse(clickSource.contains("= Unit"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HomeMainFragment 팔로잉 스케줄은 type별 공통 Action으로 이동한다`() {
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||
).readText()
|
||||
|
||||
val clickSource = source.substringFrom("private fun onFollowingScheduleClick(item: HomeFollowingScheduleUiItem)")
|
||||
assertTrue(clickSource.contains("if (item.targetId <= 0L) return"))
|
||||
assertTrue(clickSource.contains("CreatorActivityType.Live -> liveActionCoordinator.enterLiveRoom(item.targetId)"))
|
||||
assertTrue(clickSource.contains("CreatorActivityType.LiveReplay"))
|
||||
assertTrue(clickSource.contains("CreatorActivityType.Audio -> handleContentAction("))
|
||||
assertTrue(clickSource.contains("ContentActionCommand.AudioDetail(audioContentId = item.targetId)"))
|
||||
assertTrue(clickSource.contains("CreatorActivityType.Community -> handleCommunityAction("))
|
||||
assertTrue(clickSource.contains("CommunityActionCommand.PostDetail(item.targetId)"))
|
||||
assertFalse(clickSource.contains("Intent("))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `HomeMainFragment 최근 활동 LIVE route는 공통 Live와 Creator Action을 사용한다`() {
|
||||
val source = projectFile(
|
||||
|
||||
@@ -42,27 +42,6 @@ class FeedViewTest {
|
||||
assertSame(category.parent, time.parent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `content layout matches figma compact upload notice dimensions`() {
|
||||
val layout = projectFile("app/src/main/res/layout/view_feed_content.xml").readText()
|
||||
val source = projectFile(
|
||||
"app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedContentView.kt"
|
||||
).readText()
|
||||
val imageBlock = layout.substringAfter("@+id/iv_feed_content_image")
|
||||
.substringBefore("@+id/ll_feed_content_info")
|
||||
val infoBlock = layout.substringAfter("@+id/ll_feed_content_info")
|
||||
.substringBefore("</kr.co.vividnext.sodalive.v2.widget.feed.FeedContentView>")
|
||||
val titleBlock = layout.substringAfter("@+id/tv_feed_content_title")
|
||||
.substringBefore("@+id/ll_feed_content_meta")
|
||||
|
||||
assertTrue(layout.contains("android:padding=\"@dimen/spacing_14\""))
|
||||
assertTrue(imageBlock.contains("android:layout_width=\"88dp\""))
|
||||
assertTrue(imageBlock.contains("android:layout_height=\"88dp\""))
|
||||
assertTrue(infoBlock.contains("android:layout_marginStart=\"@dimen/spacing_14\""))
|
||||
assertTrue(titleBlock.contains("android:layout_marginTop=\"@dimen/spacing_6\""))
|
||||
assertTrue(source.contains("const val CONTENT_IMAGE_WIDTH_DP = 88"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `content root clipping is implemented in custom view code`() {
|
||||
val layout = projectFile("app/src/main/res/layout/view_feed_content.xml").readText()
|
||||
|
||||
Reference in New Issue
Block a user