feat(home): 추천 시간과 프로필 표시를 보완한다
This commit is contained in:
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.base.BaseFragment
|
|||||||
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity
|
import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
|
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
|
||||||
import kr.co.vividnext.sodalive.common.ToastMessage
|
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainHomeBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainHomeBinding
|
||||||
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
|
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
|
||||||
@@ -27,7 +28,9 @@ import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationGenreCreato
|
|||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveUiModel
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostSection
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorSection
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentDebutCreatorSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentDebutCreatorSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationUiState
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationUiState
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups
|
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups
|
||||||
@@ -179,7 +182,7 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
|
|
||||||
private fun bindRecentActivitySection(section: HomeRecommendationRecentlyActiveCreatorSection) {
|
private fun bindRecentActivitySection(section: HomeRecommendationRecentlyActiveCreatorSection) {
|
||||||
binding.llHomeRecentActivitySection.visibility = section.items.toSectionVisibility()
|
binding.llHomeRecentActivitySection.visibility = section.items.toSectionVisibility()
|
||||||
recentActivityCreatorAdapter.submitItems(section.items)
|
recentActivityCreatorAdapter.submitItems(section.items.map { it.withRelativeActivityAt() })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindRecentDebutSection(section: HomeRecommendationRecentDebutCreatorSection) {
|
private fun bindRecentDebutSection(section: HomeRecommendationRecentDebutCreatorSection) {
|
||||||
@@ -210,7 +213,23 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
|
|
||||||
private fun bindPopularCommunitySection(section: HomeRecommendationPopularCommunityPostSection) {
|
private fun bindPopularCommunitySection(section: HomeRecommendationPopularCommunityPostSection) {
|
||||||
binding.llHomePopularCommunitySection.visibility = section.items.toSectionVisibility()
|
binding.llHomePopularCommunitySection.visibility = section.items.toSectionVisibility()
|
||||||
popularCommunityAdapter.submitSection(section)
|
popularCommunityAdapter.submitSection(
|
||||||
|
HomeRecommendationPopularCommunityPostSection(section.items.map { it.withRelativeCreatedAt() })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HomeRecommendationRecentlyActiveCreatorUiModel.withRelativeActivityAt():
|
||||||
|
HomeRecommendationRecentlyActiveCreatorUiModel {
|
||||||
|
return copy(activityAt = formatUtcRelativeTimeText(requireContext(), activityAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HomeRecommendationPopularCommunityPostUiModel.withRelativeCreatedAt():
|
||||||
|
HomeRecommendationPopularCommunityPostUiModel {
|
||||||
|
return copy(
|
||||||
|
item = item.copy(
|
||||||
|
createdAtText = formatUtcRelativeTimeText(requireContext(), item.createdAtText)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUpSectionTitles() {
|
private fun setUpSectionTitles() {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import android.widget.ImageView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCreatorUiModel
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCreatorUiModel
|
||||||
|
|
||||||
class HomeCheerCreatorAdapter(
|
class HomeCheerCreatorAdapter(
|
||||||
@@ -111,13 +110,8 @@ class HomeCheerCreatorAdapter(
|
|||||||
topMargin = profileView.resources.getDimensionPixelSize(R.dimen.spacing_14)
|
topMargin = profileView.resources.getDimensionPixelSize(R.dimen.spacing_14)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
profileView.findViewById<ImageView>(R.id.iv_home_genre_creator_profile).apply {
|
profileView.findViewById<ImageView>(R.id.iv_home_genre_creator_profile)
|
||||||
if (creator.profileImage.isBlank()) {
|
.loadHomeCreatorProfileImage(creator.profileImage)
|
||||||
setImageDrawable(null)
|
|
||||||
} else {
|
|
||||||
loadUrl(creator.profileImage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
profileView.findViewById<TextView>(R.id.tv_home_genre_creator_profile_nickname).text = creator.nickname
|
profileView.findViewById<TextView>(R.id.tv_home_genre_creator_profile_nickname).text = creator.nickname
|
||||||
profileView.setOnClickListener { onCreatorClick(creator) }
|
profileView.setOnClickListener { onCreatorClick(creator) }
|
||||||
creatorGrid.addView(profileView)
|
creatorGrid.addView(profileView)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.home.ui
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import coil.transform.CircleCropTransformation
|
||||||
|
import coil.transform.Transformation
|
||||||
|
import kr.co.vividnext.sodalive.extensions.loadUrl
|
||||||
|
|
||||||
|
fun homeCreatorProfileImageTransformations(): List<Transformation> = listOf(CircleCropTransformation())
|
||||||
|
|
||||||
|
fun ImageView.loadHomeCreatorProfileImage(url: String?) {
|
||||||
|
if (url.isNullOrBlank()) {
|
||||||
|
setImageDrawable(null)
|
||||||
|
} else {
|
||||||
|
loadUrl(url) {
|
||||||
|
transformations(homeCreatorProfileImageTransformations())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import android.widget.ImageView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCreatorUiModel
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCreatorUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationGenreCreatorGroupUiModel
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationGenreCreatorGroupUiModel
|
||||||
|
|
||||||
@@ -109,13 +108,8 @@ class HomeGenreCreatorAdapter(
|
|||||||
topMargin = profileView.resources.getDimensionPixelSize(R.dimen.spacing_14)
|
topMargin = profileView.resources.getDimensionPixelSize(R.dimen.spacing_14)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
profileView.findViewById<ImageView>(R.id.iv_home_genre_creator_profile).apply {
|
profileView.findViewById<ImageView>(R.id.iv_home_genre_creator_profile)
|
||||||
if (creator.profileImage.isBlank()) {
|
.loadHomeCreatorProfileImage(creator.profileImage)
|
||||||
setImageDrawable(null)
|
|
||||||
} else {
|
|
||||||
loadUrl(creator.profileImage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
profileView.findViewById<TextView>(R.id.tv_home_genre_creator_profile_nickname).text = creator.nickname
|
profileView.findViewById<TextView>(R.id.tv_home_genre_creator_profile_nickname).text = creator.nickname
|
||||||
profileView.setOnClickListener { onCreatorClick(creator) }
|
profileView.setOnClickListener { onCreatorClick(creator) }
|
||||||
creatorGrid.addView(profileView)
|
creatorGrid.addView(profileView)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.widget.ImageView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.extensions.loadUrl
|
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorUiModel
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorUiModel
|
||||||
|
|
||||||
class HomeRecentActivityCreatorAdapter(
|
class HomeRecentActivityCreatorAdapter(
|
||||||
@@ -42,7 +41,7 @@ class HomeRecentActivityCreatorAdapter(
|
|||||||
private val nicknameText = itemView.findViewById<TextView>(R.id.tv_home_recent_activity_nickname)
|
private val nicknameText = itemView.findViewById<TextView>(R.id.tv_home_recent_activity_nickname)
|
||||||
|
|
||||||
fun bind(item: HomeRecommendationRecentlyActiveCreatorUiModel) {
|
fun bind(item: HomeRecommendationRecentlyActiveCreatorUiModel) {
|
||||||
profileImage.loadUrl(item.profileImage)
|
profileImage.loadHomeCreatorProfileImage(item.profileImage)
|
||||||
item.activityLabelResId?.let { labelResId ->
|
item.activityLabelResId?.let { labelResId ->
|
||||||
activityTypeText.setText(labelResId)
|
activityTypeText.setText(labelResId)
|
||||||
activityTypeText.visibility = View.VISIBLE
|
activityTypeText.visibility = View.VISIBLE
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ import android.widget.GridLayout
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import coil.transform.CircleCropTransformation
|
||||||
import androidx.core.widget.NestedScrollView
|
import androidx.core.widget.NestedScrollView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
|
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
|
||||||
import kr.co.vividnext.sodalive.settings.event.EventItem
|
import kr.co.vividnext.sodalive.settings.event.EventItem
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.data.HomeActiveCreatorItem
|
import kr.co.vividnext.sodalive.v2.main.home.data.HomeActiveCreatorItem
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.data.HomeBannerItem
|
import kr.co.vividnext.sodalive.v2.main.home.data.HomeBannerItem
|
||||||
@@ -51,6 +53,7 @@ import kr.co.vividnext.sodalive.v2.main.home.ui.HomeGenreCreatorAdapter
|
|||||||
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeLiveAdapter
|
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeLiveAdapter
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.ui.HomePopularCommunityAdapter
|
import kr.co.vividnext.sodalive.v2.main.home.ui.HomePopularCommunityAdapter
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentDebutCreatorAdapter
|
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentDebutCreatorAdapter
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.home.ui.homeCreatorProfileImageTransformations
|
||||||
import kr.co.vividnext.sodalive.v2.widget.AudioContentCardView
|
import kr.co.vividnext.sodalive.v2.widget.AudioContentCardView
|
||||||
import kr.co.vividnext.sodalive.v2.widget.AudioContentTag
|
import kr.co.vividnext.sodalive.v2.widget.AudioContentTag
|
||||||
import kr.co.vividnext.sodalive.v2.widget.TextTabBarView
|
import kr.co.vividnext.sodalive.v2.widget.TextTabBarView
|
||||||
@@ -68,6 +71,7 @@ import org.robolectric.RobolectricTestRunner
|
|||||||
import org.robolectric.Shadows.shadowOf
|
import org.robolectric.Shadows.shadowOf
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
@Config(sdk = [28], application = Application::class)
|
@Config(sdk = [28], application = Application::class)
|
||||||
@@ -729,6 +733,47 @@ class HomeMainFragmentLayoutTest {
|
|||||||
assertEquals("https://example.com/audio.m4a", item.toUiModel().item.audioUrl)
|
assertEquals("https://example.com/audio.m4a", item.toUiModel().item.audioUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `home relative time formatter converts active creator utc timestamp to relative time text`() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val defaultTimeZone = TimeZone.getDefault()
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"))
|
||||||
|
try {
|
||||||
|
val oneHourAgoUtc = System.currentTimeMillis() - 60 * 60_000L
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.character_comment_time_hours, 1),
|
||||||
|
formatUtcRelativeTimeText(context, oneHourAgoUtc.toString())
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
TimeZone.setDefault(defaultTimeZone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `home relative time formatter converts popular community createdAt to relative time text`() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val defaultTimeZone = TimeZone.getDefault()
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"))
|
||||||
|
try {
|
||||||
|
val twoMinutesAgoUtc = System.currentTimeMillis() - 2 * 60_000L
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
context.getString(R.string.character_comment_time_minutes, 2),
|
||||||
|
formatUtcRelativeTimeText(context, twoMinutesAgoUtc.toString())
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
TimeZone.setDefault(defaultTimeZone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `home creator profile image loading uses circle crop transformation`() {
|
||||||
|
val transformations = homeCreatorProfileImageTransformations()
|
||||||
|
|
||||||
|
assertTrue(transformations.single() is CircleCropTransformation)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `home recommendation mapper uses changed response fields`() {
|
fun `home recommendation mapper uses changed response fields`() {
|
||||||
val live = HomeLiveItem(
|
val live = HomeLiveItem(
|
||||||
@@ -1069,6 +1114,24 @@ class HomeMainFragmentLayoutTest {
|
|||||||
assertTrue(source.contains("followCreators(genreSectionKey"))
|
assertTrue(source.contains("followCreators(genreSectionKey"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `home recommendation viewmodel does not keep android context`() {
|
||||||
|
val viewModelSource = homeRecommendationViewModelSource()
|
||||||
|
val appDiSource = appDiSource()
|
||||||
|
|
||||||
|
assertFalse(viewModelSource.contains("android.content.Context"))
|
||||||
|
assertFalse(viewModelSource.contains("private val context"))
|
||||||
|
assertTrue(appDiSource.contains("viewModel { HomeRecommendationViewModel(get()) }"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `home main fragment formats recommendation timestamps at ui binding boundary`() {
|
||||||
|
val source = homeMainFragmentSource()
|
||||||
|
|
||||||
|
assertTrue(source.contains("formatUtcRelativeTimeText(requireContext(), activityAt)"))
|
||||||
|
assertTrue(source.contains("formatUtcRelativeTimeText(requireContext(), item.createdAtText)"))
|
||||||
|
}
|
||||||
|
|
||||||
private fun TextView.clickableSpanCount(): Int {
|
private fun TextView.clickableSpanCount(): Int {
|
||||||
val spanned = text as? Spanned ?: return 0
|
val spanned = text as? Spanned ?: return 0
|
||||||
return spanned.getSpans(0, spanned.length, ClickableSpan::class.java).size
|
return spanned.getSpans(0, spanned.length, ClickableSpan::class.java).size
|
||||||
@@ -1087,6 +1150,22 @@ class HomeMainFragmentLayoutTest {
|
|||||||
).readText()
|
).readText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun homeRecommendationViewModelSource(): String {
|
||||||
|
val projectRoot = java.io.File("..").canonicalFile
|
||||||
|
return java.io.File(
|
||||||
|
projectRoot,
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeRecommendationViewModel.kt"
|
||||||
|
).readText()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appDiSource(): String {
|
||||||
|
val projectRoot = java.io.File("..").canonicalFile
|
||||||
|
return java.io.File(
|
||||||
|
projectRoot,
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt"
|
||||||
|
).readText()
|
||||||
|
}
|
||||||
|
|
||||||
private fun inflateView(layoutResId: Int): View {
|
private fun inflateView(layoutResId: Int): View {
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
return LayoutInflater.from(context).inflate(layoutResId, null, false)
|
return LayoutInflater.from(context).inflate(layoutResId, null, false)
|
||||||
@@ -1155,7 +1234,10 @@ class HomeMainFragmentLayoutTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun popularCommunityData(audioUrl: String?): HomePopularCommunityPostItem {
|
private fun popularCommunityData(
|
||||||
|
audioUrl: String?,
|
||||||
|
createdAt: String = "2분 전"
|
||||||
|
): HomePopularCommunityPostItem {
|
||||||
return HomePopularCommunityPostItem(
|
return HomePopularCommunityPostItem(
|
||||||
postId = 1L,
|
postId = 1L,
|
||||||
creatorId = 1L,
|
creatorId = 1L,
|
||||||
@@ -1165,7 +1247,7 @@ class HomeMainFragmentLayoutTest {
|
|||||||
audioUrl = audioUrl,
|
audioUrl = audioUrl,
|
||||||
content = "본문",
|
content = "본문",
|
||||||
price = 0,
|
price = 0,
|
||||||
createdAt = "2분 전",
|
createdAt = createdAt,
|
||||||
likeCount = 6L,
|
likeCount = 6L,
|
||||||
commentCount = 5L,
|
commentCount = 5L,
|
||||||
existOrdered = false
|
existOrdered = false
|
||||||
|
|||||||
Reference in New Issue
Block a user