diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt index e12f1c9d3e20..f34b31fc3e37 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt @@ -24,7 +24,6 @@ import android.view.ViewGroup import android.view.ViewStub import android.webkit.CookieManager import android.webkit.WebView -import android.widget.ImageView.ScaleType.CENTER_CROP import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.app.AlertDialog @@ -37,6 +36,7 @@ import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager @@ -48,7 +48,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.facebook.shimmer.ShimmerFrameLayout import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.behavior.HideBottomViewOnScrollBehavior import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar @@ -118,7 +117,6 @@ import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiSt import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiState.LoadingUiState import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiState.ReaderPostDetailsUiState import org.wordpress.android.ui.reader.views.ReaderIconCountView -import org.wordpress.android.ui.reader.views.ReaderPostDetailsHeaderViewUiStateBuilder import org.wordpress.android.ui.reader.views.ReaderSimplePostContainerView import org.wordpress.android.ui.reader.views.ReaderWebView import org.wordpress.android.ui.reader.views.ReaderWebView.ReaderCustomViewListener @@ -146,7 +144,6 @@ import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.extensions.setVisible import org.wordpress.android.util.helpers.SwipeToRefreshHelper import org.wordpress.android.util.image.ImageManager -import org.wordpress.android.util.image.ImageType.PHOTO import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout import org.wordpress.android.viewmodel.ContextProvider import org.wordpress.android.viewmodel.observeEvent @@ -158,7 +155,6 @@ import java.net.HttpURLConnection import java.util.EnumSet import javax.inject.Inject import com.google.android.material.R as MaterialR -import androidx.core.view.isGone import androidx.core.net.toUri import androidx.core.view.forEach @@ -251,9 +247,6 @@ class ReaderPostDetailFragment : ViewPagerFragment(), @Inject internal lateinit var imageManager: ImageManager - @Inject - lateinit var postDetailsHeaderViewUiStateBuilder: ReaderPostDetailsHeaderViewUiStateBuilder - @Inject lateinit var readerUtilsWrapper: ReaderUtilsWrapper @@ -292,36 +285,6 @@ class ReaderPostDetailFragment : ViewPagerFragment(), val isCustomViewShowing: Boolean get() = view != null && readerWebView.isCustomViewShowing - private val appBarLayoutOffsetChangedListener = - AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> - val collapsingToolbarLayout = appBarLayout - .findViewById(R.id.collapsing_toolbar) - val toolbar = appBarLayout.findViewById(R.id.toolbar_main) - - view?.context?.let { context -> - val menu: Menu = toolbar.menu - - val collapsingToolbarHeight = collapsingToolbarLayout.height - val isCollapsed = (collapsingToolbarHeight + verticalOffset) <= - collapsingToolbarLayout.scrimVisibleHeightTrigger - - val color = if (isCollapsed) { - context.getColorFromAttribute(MaterialR.attr.colorOnSurface) - } else { - ContextCompat.getColor(context, R.color.white) - } - val colorFilter = BlendModeColorFilterCompat - .createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP) - - toolbar.setTitleTextColor(color) - toolbar.navigationIcon?.colorFilter = colorFilter - - menu.forEach { - it.icon?.colorFilter = colorFilter - } - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (requireActivity().application as WordPress).component().inject(this) @@ -404,8 +367,6 @@ class ReaderPostDetailFragment : ViewPagerFragment(), appBar = view.findViewById(R.id.appbar_with_collapsing_toolbar_layout) toolBar = appBar.findViewById(R.id.toolbar_main) - appBar.addOnOffsetChangedListener(appBarLayoutOffsetChangedListener) - // Fixes collapsing toolbar layout being obscured by the status bar when drawn behind it ViewCompat.setOnApplyWindowInsetsListener(appBar) { _: View, insets: WindowInsetsCompat -> val insetTop = insets.getInsets(WindowInsetsCompat. Type. systemBars()).top @@ -430,6 +391,21 @@ class ReaderPostDetailFragment : ViewPagerFragment(), toolBar.setNavigationIcon(R.drawable.ic_arrow_left_white_24dp) toolBar.setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } } + + applyToolbarIconColors(view.context) + } + + private fun applyToolbarIconColors(context: Context) { + val color = context.getColorFromAttribute( + MaterialR.attr.colorOnSurface + ) + val colorFilter = BlendModeColorFilterCompat + .createBlendModeColorFilterCompat( + color, BlendModeCompat.SRC_ATOP + ) + toolBar.setTitleTextColor(color) + toolBar.navigationIcon?.colorFilter = colorFilter + toolBar.menu.forEach { it.icon?.colorFilter = colorFilter } } private fun initScrollView(view: View) { @@ -801,7 +777,6 @@ class ReaderPostDetailFragment : ViewPagerFragment(), binding.headerView.updatePost(state.headerUiState, getReadingPreferences()) showOrHideMoreMenu(state) - updateFeaturedImage(state.featuredImageUiState, binding) updateExcerptFooter(state.excerptFooterUiState) with(layoutFooterBinding) { @@ -933,23 +908,6 @@ class ReaderPostDetailFragment : ViewPagerFragment(), .show(childFragmentManager, ReaderLoginRequiredBottomSheetFragment.TAG) } - private fun updateFeaturedImage( - state: ReaderPostDetailsUiState.ReaderPostFeaturedImageUiState?, - binding: ReaderFragmentPostDetailBinding - ) { - val featuredImageView = binding.appbarWithCollapsingToolbarLayout.featuredImage - featuredImageView.setVisible(state != null) - state?.let { - featuredImageView.layoutParams.height = it.height - it.url?.let { url -> - imageManager.load(featuredImageView, PHOTO, url, CENTER_CROP) - featuredImageView.setOnClickListener { - viewModel.onFeaturedImageClicked(blogId = state.blogId, featuredImageUrl = url) - } - } - } - } - private fun updateExcerptFooter(state: ReaderPostDetailsUiState.ExcerptFooterUiState?) { // if we're showing just the excerpt, show a footer which links to the full post excerptFooter.setVisible(state != null) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailUiStateBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailUiStateBuilder.kt index fb25d19d4a26..84fbcea0c934 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailUiStateBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailUiStateBuilder.kt @@ -8,8 +8,6 @@ import org.wordpress.android.ui.reader.discover.ReaderPostCardActionType import org.wordpress.android.ui.reader.discover.ReaderPostUiStateBuilder import org.wordpress.android.ui.reader.models.ReaderSimplePost import org.wordpress.android.ui.reader.models.ReaderSimplePostList -import org.wordpress.android.ui.reader.utils.FeaturedImageUtils -import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import org.wordpress.android.ui.reader.utils.ThreadedCommentsUtils import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.CommentSnippetState import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.CommentSnippetState.CommentSnippetData @@ -19,7 +17,6 @@ import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.Comm import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.CommentSnippetUiState import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiState.ReaderPostDetailsUiState import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiState.ReaderPostDetailsUiState.ExcerptFooterUiState -import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiState.ReaderPostDetailsUiState.ReaderPostFeaturedImageUiState import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiState.ReaderPostDetailsUiState.RelatedPostsUiState import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiState.ReaderPostDetailsUiState.RelatedPostsUiState.ReaderRelatedPostUiState import org.wordpress.android.ui.reader.views.ReaderPostDetailsHeaderViewUiStateBuilder @@ -39,22 +36,17 @@ import org.wordpress.android.ui.utils.UiString.UiStringText import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T import org.wordpress.android.util.DateTimeUtilsWrapper -import org.wordpress.android.util.DisplayUtilsWrapper import org.wordpress.android.util.WPAvatarUtilsWrapper import org.wordpress.android.viewmodel.ContextProvider import org.wordpress.android.viewmodel.ResourceProvider import javax.inject.Inject -const val READER_POST_FEATURED_IMAGE_HEIGHT_PERCENT = 0.4 const val RELATED_POST_IMAGE_HEIGHT_WIDTH_RATION = 0.56 // 9:16 @Reusable class ReaderPostDetailUiStateBuilder @Inject constructor( private val postDetailsHeaderViewUiStateBuilder: ReaderPostDetailsHeaderViewUiStateBuilder, private val postUiStateBuilder: ReaderPostUiStateBuilder, - private val featuredImageUtils: FeaturedImageUtils, - private val readerUtilsWrapper: ReaderUtilsWrapper, - private val displayUtilsWrapper: DisplayUtilsWrapper, private val contextProvider: ContextProvider, private val htmlUtilsWrapper: HtmlUtilsWrapper, private val htmlMessageUtils: HtmlMessageUtils, @@ -76,8 +68,7 @@ class ReaderPostDetailUiStateBuilder @Inject constructor( ) = ReaderPostDetailsUiState( postId = post.postId, blogId = post.blogId, - featuredImageUiState = buildReaderPostFeaturedImageUiState(post), - headerUiState = buildPostDetailsHeaderUiState( + headerUiState = postDetailsHeaderViewUiStateBuilder.mapPostToUiState( post, onHeaderAction, ), @@ -209,32 +200,6 @@ class ReaderPostDetailUiStateBuilder @Inject constructor( onItemClicked = onItemClicked ) - private fun buildReaderPostFeaturedImageUiState(post: ReaderPost) = - post.takeIf { featuredImageUtils.shouldAddFeaturedImage(post) }?.let { - ReaderPostFeaturedImageUiState( - blogId = post.blogId, - url = buildReaderPostFeaturedImageUrl(post), - height = (displayUtilsWrapper.getWindowPixelHeight() * - READER_POST_FEATURED_IMAGE_HEIGHT_PERCENT).toInt() - ) - } - - private fun buildReaderPostFeaturedImageUrl(post: ReaderPost) = readerUtilsWrapper.getResizedImageUrl( - post.featuredImage, - displayUtilsWrapper.getDisplayPixelWidth(), - 0, - post.isPrivate, - post.isPrivateAtomic - ) - - private fun buildPostDetailsHeaderUiState( - post: ReaderPost, - onHeaderAction: (ReaderPostDetailsHeaderAction) -> Unit, - ) = postDetailsHeaderViewUiStateBuilder.mapPostToUiState( - post, - onHeaderAction, - ) - private fun buildExcerptFooterUiState(post: ReaderPost): ExcerptFooterUiState? = post.takeIf { post.shouldShowExcerpt() }?.let { ExcerptFooterUiState( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt index f0b2962f1977..53e1e2746efe 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModel.kt @@ -79,7 +79,7 @@ import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiSt import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiState.ReaderPostDetailsUiState import org.wordpress.android.ui.reader.views.uistates.CommentSnippetItemState import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderAction -import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderViewUiState.ReaderPostDetailsHeaderUiState +import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderUiState import org.wordpress.android.ui.utils.HtmlMessageUtils import org.wordpress.android.ui.utils.UiDimen import org.wordpress.android.ui.utils.UiString @@ -571,6 +571,8 @@ class ReaderPostDetailViewModel @Inject constructor( is ReaderPostDetailsHeaderAction.LikesClicked -> onLikesClicked() is ReaderPostDetailsHeaderAction.CommentsClicked -> onCommentsClicked() is ReaderPostDetailsHeaderAction.TagItemClicked -> onTagItemClicked(action.tagSlug) + is ReaderPostDetailsHeaderAction.FeaturedImageClicked -> + onFeaturedImageClicked(action.blogId, action.featuredImageUrl) } } @@ -922,7 +924,6 @@ class ReaderPostDetailViewModel @Inject constructor( data class ReaderPostDetailsUiState( val postId: Long, val blogId: Long, - val featuredImageUiState: ReaderPostFeaturedImageUiState? = null, val headerUiState: ReaderPostDetailsHeaderUiState, val excerptFooterUiState: ExcerptFooterUiState?, val moreMenuItems: List? = null, @@ -930,8 +931,6 @@ class ReaderPostDetailViewModel @Inject constructor( val localRelatedPosts: RelatedPostsUiState? = null, val globalRelatedPosts: RelatedPostsUiState? = null ) : UiState() { - data class ReaderPostFeaturedImageUiState(val blogId: Long, val url: String? = null, val height: Int) - data class ExcerptFooterUiState(val visitPostExcerptFooterLinkText: UiString? = null, val postLink: String?) data class RelatedPostsUiState( @@ -986,10 +985,15 @@ class ReaderPostDetailViewModel @Inject constructor( } fun onUserNavigateFromComments() { - // reload post from DB and update UI state + // reload post from DB (including text column for featured image + // deduplication) and update UI state val currentUiState: ReaderPostDetailsUiState? = (_uiState.value as? ReaderPostDetailsUiState) currentUiState?.let { - findPost(currentUiState.postId, currentUiState.blogId)?.let { post -> + readerPostTableWrapper.getBlogPost( + it.blogId, + it.postId, + false + )?.let { post -> this.post = post onUpdatePost(post) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.kt index 77ff6bb8ca6d..2ec49121506c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.kt @@ -4,7 +4,9 @@ import android.content.Context import android.util.AttributeSet import android.util.TypedValue import android.view.LayoutInflater +import android.widget.ImageView.ScaleType.FIT_CENTER import android.widget.LinearLayout +import android.widget.TextView import androidx.core.view.isVisible import org.wordpress.android.R import org.wordpress.android.WordPress @@ -15,13 +17,15 @@ import org.wordpress.android.ui.reader.utils.toTypeface import org.wordpress.android.ui.reader.views.uistates.FollowButtonUiState import org.wordpress.android.ui.reader.views.uistates.InteractionSectionUiState import org.wordpress.android.ui.reader.views.uistates.ReaderBlogSectionUiState -import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderViewUiState.ReaderPostDetailsHeaderUiState +import org.wordpress.android.ui.reader.views.uistates.ReaderFeaturedImageUiState +import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderUiState import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.ui.utils.UiString import org.wordpress.android.util.extensions.getDrawableResIdFromAttribute import org.wordpress.android.util.extensions.setVisible import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.image.ImageType +import org.wordpress.android.util.image.ImageType.PHOTO import javax.inject.Inject /** @@ -42,7 +46,9 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( init { (context.applicationContext as WordPress).component().inject(this) - binding = ReaderPostDetailHeaderViewBinding.inflate(LayoutInflater.from(context), this, true) + binding = ReaderPostDetailHeaderViewBinding.inflate( + LayoutInflater.from(context), this, true + ) } fun updatePost( @@ -58,16 +64,30 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( updateTitle(uiState.title, readingPreferences, themeValues) + uiHelpers.setTextOrHide( + textBlogName, + uiState.blogSectionUiState.blogName + ) + setAuthorAndDate(uiState.authorName, uiState.dateLine) - uiHelpers.setTextOrHide(layoutBlogSection.blogSectionTextBlogName, uiState.blogSectionUiState.blogName) + // Blog name is shown at the top; hide it in the blog section + layoutBlogSection.blogSectionTextBlogName.isVisible = false updateFollowButton(uiState.followButtonUiState) updateAvatars(uiState.blogSectionUiState) updateBlogSectionClick(uiState.blogSectionUiState) - updateInteractionSection(uiState.interactionSectionUiState, readingPreferences, themeValues) + uiHelpers.setTextOrHide(textReadingTime, uiState.readingTime) + updateFeaturedImage(uiState.featuredImageUiState) + uiHelpers.setTextOrHide(textExcerpt, uiState.excerpt) + + updateInteractionSection( + uiState.interactionSectionUiState, + readingPreferences, + themeValues + ) } private fun ReaderPostDetailHeaderViewBinding.updateTitle( @@ -78,7 +98,9 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( uiHelpers.setTextOrHide(textTitle, title) readingPreferences?.let { prefs -> - val fontSize = resources.getDimension(R.dimen.text_sz_double_extra_large) * prefs.fontSize.multiplier + val fontSize = resources.getDimension( + R.dimen.text_sz_double_extra_large + ) * prefs.fontSize.multiplier textTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize) textTitle.typeface = prefs.fontFamily.toTypeface() themeValues?.let { textTitle.setTextColor(it.intTextColor) } @@ -89,7 +111,11 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( state: ReaderBlogSectionUiState ) { layoutBlogSection.root.apply { - setBackgroundResource(context.getDrawableResIdFromAttribute(state.blogSectionClickData?.background ?: 0)) + setBackgroundResource( + context.getDrawableResIdFromAttribute( + state.blogSectionClickData?.background ?: 0 + ) + ) state.blogSectionClickData?.onBlogSectionClicked?.let { onClick -> setOnClickListener { onClick.invoke() } } ?: run { @@ -99,22 +125,35 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( } } - private fun ReaderPostDetailHeaderViewBinding.updateAvatars(state: ReaderBlogSectionUiState) { + private fun ReaderPostDetailHeaderViewBinding.updateAvatars( + state: ReaderBlogSectionUiState + ) { val blogAvatarImage = layoutBlogSection.blogSectionImageBlogAvatar - uiHelpers.updateVisibility(blogAvatarImage, state.avatarOrBlavatarUrl != null) + uiHelpers.updateVisibility( + blogAvatarImage, state.avatarOrBlavatarUrl != null + ) if (state.avatarOrBlavatarUrl == null) { imageManager.cancelRequestAndClearImageView(blogAvatarImage) } else { - imageManager.loadIntoCircle(blogAvatarImage, state.blavatarType, state.avatarOrBlavatarUrl) + imageManager.loadIntoCircle( + blogAvatarImage, + state.blavatarType, + state.avatarOrBlavatarUrl + ) } val authorAvatarImage = layoutBlogSection.blogSectionImageAuthorAvatar - val showAuthorsAvatar = state.authorAvatarUrl != null && state.isAuthorAvatarVisible + val showAuthorsAvatar = + state.authorAvatarUrl != null && state.isAuthorAvatarVisible uiHelpers.updateVisibility(authorAvatarImage, showAuthorsAvatar) if (!showAuthorsAvatar) { imageManager.cancelRequestAndClearImageView(authorAvatarImage) } else { - imageManager.loadIntoCircle(authorAvatarImage, ImageType.BLAVATAR_CIRCULAR, state.authorAvatarUrl) + imageManager.loadIntoCircle( + authorAvatarImage, + ImageType.BLAVATAR_CIRCULAR, + state.authorAvatarUrl + ) } } @@ -122,20 +161,41 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( followButtonUiState: FollowButtonUiState ) { headerFollowButtonContainer.setVisible(followButtonUiState.isVisible) - headerFollowProgress.setVisible(followButtonUiState.isFollowActionRunning) + headerFollowProgress.setVisible( + followButtonUiState.isFollowActionRunning + ) headerFollowButton.apply { setIsLoading(followButtonUiState.isFollowActionRunning) isEnabled = !followButtonUiState.isFollowActionRunning setIsFollowed(followButtonUiState.isFollowed) - setOnClickListener { followButtonUiState.onFollowButtonClicked?.invoke() } + setOnClickListener { + followButtonUiState.onFollowButtonClicked?.invoke() + } } } - private fun setAuthorAndDate(authorName: String?, dateLine: String) = with(binding.layoutBlogSection) { + private fun setAuthorAndDate( + authorName: String?, + dateLine: String + ) = with(binding.layoutBlogSection) { uiHelpers.setTextOrHide(blogSectionTextAuthor, authorName) uiHelpers.setTextOrHide(blogSectionTextDateline, dateLine) + } - blogSectionDotSeparator.setVisible(authorName != null) + private fun ReaderPostDetailHeaderViewBinding.updateFeaturedImage( + state: ReaderFeaturedImageUiState? + ) { + headerFeaturedImage.setVisible(state?.url != null) + state?.url?.let { url -> + imageManager.load(headerFeaturedImage, PHOTO, url, FIT_CENTER) + state.onFeaturedImageClicked?.let { onClick -> + headerFeaturedImage.setOnClickListener { + onClick(state.blogId, url) + } + } + } ?: imageManager.cancelRequestAndClearImageView( + headerFeaturedImage + ) } private fun updateInteractionSection( @@ -148,14 +208,17 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( val likeCount = state.likeCount val commentCount = state.commentCount - val likeLabel = ReaderUtils.getShortLikeLabelText(viewContext, likeCount) - .takeIf { likeCount > 0 } - val commentLabel = ReaderUtils.getShortCommentLabelText(viewContext, commentCount) - .takeIf { commentCount > 0 } + val likeLabel = + ReaderUtils.getShortLikeLabelText(viewContext, likeCount) + .takeIf { likeCount > 0 } + val commentLabel = + ReaderUtils.getShortCommentLabelText(viewContext, commentCount) + .takeIf { commentCount > 0 } uiHelpers.setTextOrHide(headerLikeCount, likeLabel) uiHelpers.setTextOrHide(headerCommentCount, commentLabel) - headerDotSeparator.isVisible = likeLabel != null && commentLabel != null + headerDotSeparator.isVisible = + likeLabel != null && commentLabel != null headerLikeCount.setOnClickListener { state.onLikesClicked() } headerCommentCount.setOnClickListener { state.onCommentsClicked() } @@ -174,11 +237,14 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( val typeface = readingPreferences.fontFamily.toTypeface() val textColor = themeValues?.intTextColor - listOf(binding.headerLikeCount, binding.headerCommentCount, binding.headerDotSeparator) - .forEach { view -> - view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize) - view.typeface = typeface - textColor?.let { view.setTextColor(it) } - } + fun applyTheme(view: TextView) { + view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize) + view.typeface = typeface + textColor?.let { view.setTextColor(it) } + } + + applyTheme(binding.headerLikeCount) + applyTheme(binding.headerCommentCount) + applyTheme(binding.headerDotSeparator) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailsHeaderViewUiStateBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailsHeaderViewUiStateBuilder.kt index 0dc940e5ae37..57038e578941 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailsHeaderViewUiStateBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailsHeaderViewUiStateBuilder.kt @@ -1,23 +1,40 @@ package org.wordpress.android.ui.reader.views import dagger.Reusable +import org.wordpress.android.R +import org.wordpress.android.datasets.ReaderBlogTableWrapper import org.wordpress.android.models.ReaderPost import org.wordpress.android.ui.reader.discover.ReaderPostTagsUiStateBuilder import org.wordpress.android.ui.reader.discover.ReaderPostUiStateBuilder +import org.wordpress.android.ui.reader.utils.FeaturedImageUtils +import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import org.wordpress.android.ui.reader.views.uistates.FollowButtonUiState import org.wordpress.android.ui.reader.views.uistates.InteractionSectionUiState -import org.wordpress.android.ui.reader.views.uistates.ReaderBlogSectionUiState import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderAction -import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderViewUiState.ReaderPostDetailsHeaderUiState +import org.wordpress.android.ui.reader.views.uistates.ReaderFeaturedImageUiState +import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderUiState +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.ui.utils.UiString.UiStringResWithParams import org.wordpress.android.ui.utils.UiString.UiStringText import org.wordpress.android.util.DateTimeUtilsWrapper +import org.wordpress.android.util.DisplayUtilsWrapper +import org.wordpress.android.util.HtmlUtils +import java.text.SimpleDateFormat +import java.util.Locale import javax.inject.Inject +import kotlin.math.ceil + +private const val WORDS_PER_MINUTE = 200 @Reusable class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( private val postUiStateBuilder: ReaderPostUiStateBuilder, private val readerPostTagsUiStateBuilder: ReaderPostTagsUiStateBuilder, private val dateTimeUtilsWrapper: DateTimeUtilsWrapper, + private val featuredImageUtils: FeaturedImageUtils, + private val readerUtilsWrapper: ReaderUtilsWrapper, + private val displayUtilsWrapper: DisplayUtilsWrapper, + private val readerBlogTableWrapper: ReaderBlogTableWrapper, ) { fun mapPostToUiState( post: ReaderPost, @@ -29,66 +46,116 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( return ReaderPostDetailsHeaderUiState( title = textTitle, - authorName = post.authorName, - tagItems = buildTagItems( - post, - onClicked = { onHeaderAction(ReaderPostDetailsHeaderAction.TagItemClicked(it)) } - ), - tagItemsVisibility = buildTagItemsVisibility(post), - blogSectionUiState = buildBlogSectionUiState( - post, - onBlogSectionClicked = { onHeaderAction(ReaderPostDetailsHeaderAction.BlogSectionClicked) } - ), - followButtonUiState = buildFollowButtonUiState( - post, - onFollowClicked = { onHeaderAction(ReaderPostDetailsHeaderAction.FollowClicked) } + authorName = post.authorName?.takeIf { + it.isNotBlank() && + !it.equals(post.blogName, ignoreCase = true) + }, + tagItems = readerPostTagsUiStateBuilder + .mapPostTagsToTagUiStates(post) { + onHeaderAction(ReaderPostDetailsHeaderAction.TagItemClicked(it)) + }, + tagItemsVisibility = post.tags.isNotEmpty(), + blogSectionUiState = postUiStateBuilder + .mapPostToBlogSectionUiState(post) { + onHeaderAction(ReaderPostDetailsHeaderAction.BlogSectionClicked) + }, + followButtonUiState = FollowButtonUiState( + onFollowButtonClicked = { + onHeaderAction(ReaderPostDetailsHeaderAction.FollowClicked) + }, + isFollowed = post.isFollowedByCurrentUser, + isVisible = true ), dateLine = buildDateLine(post), - interactionSectionUiState = buildInteractionSection( + readingTime = buildReadingTime(post), + excerpt = buildExcerpt(post), + featuredImageUiState = buildFeaturedImageUiState( post, - onLikesClicked = { onHeaderAction(ReaderPostDetailsHeaderAction.LikesClicked) }, - onCommentsClicked = { onHeaderAction(ReaderPostDetailsHeaderAction.CommentsClicked) } + onFeaturedImageClicked = { blogId, url -> + onHeaderAction( + ReaderPostDetailsHeaderAction.FeaturedImageClicked(blogId, url) + ) + } + ), + interactionSectionUiState = InteractionSectionUiState( + likeCount = post.numLikes, + commentCount = post.numReplies, + onLikesClicked = { + onHeaderAction(ReaderPostDetailsHeaderAction.LikesClicked) + }, + onCommentsClicked = { + onHeaderAction(ReaderPostDetailsHeaderAction.CommentsClicked) + } ) ) } - private fun buildBlogSectionUiState( - post: ReaderPost, - onBlogSectionClicked: () -> Unit - ): ReaderBlogSectionUiState { - return postUiStateBuilder.mapPostToBlogSectionUiState( - post, - onBlogSectionClicked - ) + private fun buildDateLine(post: ReaderPost): String { + val date = post.getDisplayDate(dateTimeUtilsWrapper) ?: return "" + return SimpleDateFormat( + DATE_FORMAT_PATTERN, Locale.getDefault() + ).format(date) } - private fun buildFollowButtonUiState( - post: ReaderPost, - onFollowClicked: () -> Unit - ): FollowButtonUiState { - return FollowButtonUiState( - onFollowButtonClicked = onFollowClicked, - isFollowed = post.isFollowedByCurrentUser, - isVisible = true + /** + * Estimates reading time by stripping HTML tags and img elements + * from the post content, counting words, and dividing by + * [WORDS_PER_MINUTE] (rounded up, minimum 1 minute). + * Returns null for excerpt-only posts or empty content. + */ + private fun buildReadingTime(post: ReaderPost): UiString? { + val text = post.takeUnless { it.shouldShowExcerpt() } + ?.text + ?.takeIf { it.isNotBlank() } + ?.let { + HtmlUtils.fastStripHtml( + it.replace(IMG_TAG_REGEX, "") + ).trim() + } + ?.takeIf { it.isNotBlank() } + ?: return null + val wordCount = text.split(WHITESPACE_REGEX).size + val minutes = ceil(wordCount.toDouble() / WORDS_PER_MINUTE) + .toInt() + .coerceAtLeast(1) + return UiStringResWithParams( + R.string.reader_reading_time, + listOf(UiStringText(minutes.toString())) ) } - private fun buildTagItems(post: ReaderPost, onClicked: (String) -> Unit) = - readerPostTagsUiStateBuilder.mapPostTagsToTagUiStates(post, onClicked) - - private fun buildTagItemsVisibility(post: ReaderPost) = post.tags.isNotEmpty() - - private fun buildDateLine(post: ReaderPost) = - dateTimeUtilsWrapper.javaDateToTimeSpan(post.getDisplayDate(dateTimeUtilsWrapper)) + private fun buildExcerpt(post: ReaderPost): UiString? { + val description = readerBlogTableWrapper + .getBlogInfo(post.blogId) + ?.description + ?.takeIf { it.isNotBlank() } + ?: return null + return UiStringText(description) + } - private fun buildInteractionSection( + private fun buildFeaturedImageUiState( post: ReaderPost, - onLikesClicked: () -> Unit, - onCommentsClicked: () -> Unit, - ) = InteractionSectionUiState( - likeCount = post.numLikes, - commentCount = post.numReplies, - onLikesClicked = onLikesClicked, - onCommentsClicked = onCommentsClicked, - ) + onFeaturedImageClicked: (Long, String) -> Unit, + ): ReaderFeaturedImageUiState? { + if (!featuredImageUtils.shouldAddFeaturedImage(post)) return null + val url = readerUtilsWrapper.getResizedImageUrl( + post.featuredImage, + displayUtilsWrapper.getDisplayPixelWidth(), + 0, + post.isPrivate, + post.isPrivateAtomic + ) + return ReaderFeaturedImageUiState( + blogId = post.blogId, + url = url, + onFeaturedImageClicked = onFeaturedImageClicked, + ) + } + + companion object { + private val IMG_TAG_REGEX = Regex("]*>") + private val WHITESPACE_REGEX = "\\s+".toRegex() + private const val DATE_FORMAT_PATTERN = + "MMM d, yyyy 'at' h:mm a" + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/ReaderPostDetailsHeaderAction.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/ReaderPostDetailsHeaderAction.kt index aea2ddfccf66..ccd943137b44 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/ReaderPostDetailsHeaderAction.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/ReaderPostDetailsHeaderAction.kt @@ -6,4 +6,8 @@ sealed interface ReaderPostDetailsHeaderAction { data class TagItemClicked(val tagSlug: String) : ReaderPostDetailsHeaderAction data object LikesClicked : ReaderPostDetailsHeaderAction data object CommentsClicked : ReaderPostDetailsHeaderAction + data class FeaturedImageClicked( + val blogId: Long, + val featuredImageUrl: String + ) : ReaderPostDetailsHeaderAction } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/ReaderPostDetailsHeaderViewUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/ReaderPostDetailsHeaderViewUiState.kt index a27318124e0a..c742c69579c1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/ReaderPostDetailsHeaderViewUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/ReaderPostDetailsHeaderViewUiState.kt @@ -3,15 +3,22 @@ package org.wordpress.android.ui.reader.views.uistates import org.wordpress.android.ui.reader.discover.interests.TagUiState import org.wordpress.android.ui.utils.UiString -sealed class ReaderPostDetailsHeaderViewUiState { - data class ReaderPostDetailsHeaderUiState( - val title: UiString?, - val authorName: String?, - val tagItems: List, - val tagItemsVisibility: Boolean, - val blogSectionUiState: ReaderBlogSectionUiState, - val followButtonUiState: FollowButtonUiState, - val dateLine: String, - val interactionSectionUiState: InteractionSectionUiState, - ) : ReaderPostDetailsHeaderViewUiState() -} +data class ReaderPostDetailsHeaderUiState( + val title: UiString?, + val authorName: String?, + val tagItems: List, + val tagItemsVisibility: Boolean, + val blogSectionUiState: ReaderBlogSectionUiState, + val followButtonUiState: FollowButtonUiState, + val dateLine: String, + val readingTime: UiString? = null, + val excerpt: UiString? = null, + val featuredImageUiState: ReaderFeaturedImageUiState? = null, + val interactionSectionUiState: InteractionSectionUiState, +) + +data class ReaderFeaturedImageUiState( + val blogId: Long, + val url: String?, + val onFeaturedImageClicked: ((Long, String) -> Unit)?, +) diff --git a/WordPress/src/main/java/org/wordpress/android/util/image/PortraitAwareCropTransformation.kt b/WordPress/src/main/java/org/wordpress/android/util/image/PortraitAwareCropTransformation.kt index 4ebec874e275..602c0a31d809 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/image/PortraitAwareCropTransformation.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/image/PortraitAwareCropTransformation.kt @@ -21,6 +21,8 @@ import java.security.MessageDigest class PortraitAwareCropTransformation( @ColorInt private val backgroundColor: Int ) : BitmapTransformation() { + private val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG) + override fun transform( pool: BitmapPool, toTransform: Bitmap, @@ -45,7 +47,6 @@ class PortraitAwareCropTransformation( val scaledWidth = toTransform.width * scale val left = (outWidth - scaledWidth) / 2f val destRect = RectF(left, 0f, left + scaledWidth, outHeight.toFloat()) - val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG) canvas.drawBitmap(toTransform, null, destRect, paint) return result diff --git a/WordPress/src/main/res/drawable/ic_clock_16dp.xml b/WordPress/src/main/res/drawable/ic_clock_16dp.xml new file mode 100644 index 000000000000..9dbfe4d1de30 --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_clock_16dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/WordPress/src/main/res/layout/appbar_with_collapsing_toolbar_layout.xml b/WordPress/src/main/res/layout/appbar_with_collapsing_toolbar_layout.xml index e8b50d2a9f0d..ceebe956eed9 100644 --- a/WordPress/src/main/res/layout/appbar_with_collapsing_toolbar_layout.xml +++ b/WordPress/src/main/res/layout/appbar_with_collapsing_toolbar_layout.xml @@ -17,19 +17,6 @@ app:scrimVisibleHeightTrigger="@dimen/scrim_visible_height_trigger" app:titleEnabled="false"> - - - - + android:layout_height="wrap_content" + android:paddingVertical="@dimen/margin_small"> @@ -32,61 +30,49 @@ style="@style/ReaderTextView.PostDetail.BlogSection.Title" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginStart="@dimen/margin_large" android:layout_marginEnd="@dimen/margin_medium" android:includeFontPadding="false" + app:layout_goneMarginStart="0dp" app:layout_constraintStart_toEndOf="@id/blog_section_image_blog_avatar" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" + app:layout_constraintTop_toTopOf="@id/blog_section_image_blog_avatar" app:layout_constraintBottom_toTopOf="@id/blog_section_text_author" + app:layout_constraintVertical_chainStyle="packed" tools:text="My Blog Name" /> - - + app:layout_constraintTop_toBottomOf="@id/blog_section_text_author" + app:layout_constraintBottom_toBottomOf="@id/blog_section_image_blog_avatar" + tools:text="Dec 18, 2025 at 3:30 PM" /> diff --git a/WordPress/src/main/res/layout/reader_post_detail_header_view.xml b/WordPress/src/main/res/layout/reader_post_detail_header_view.xml index 6dd8d802ac2f..2fef4e8d7685 100644 --- a/WordPress/src/main/res/layout/reader_post_detail_header_view.xml +++ b/WordPress/src/main/res/layout/reader_post_detail_header_view.xml @@ -7,33 +7,35 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintHorizontal_bias="0" + tools:text="My Blog Name" /> @@ -48,6 +50,14 @@ + + + + + + + + + + app:shapeAppearanceOverlay="@style/ReaderFeaturedImageShape" + tools:visibility="visible" /> + + + + + + + + + + @@ -85,7 +171,7 @@ android:importantForAccessibility="no" app:layout_constraintStart_toEndOf="@id/header_like_count" app:layout_constraintEnd_toStartOf="@id/header_comment_count" - app:layout_constraintTop_toBottomOf="@id/text_title" + app:layout_constraintTop_toBottomOf="@id/reading_time_barrier" app:layout_constrainedWidth="true" /> diff --git a/WordPress/src/main/res/values-night/reader_styles.xml b/WordPress/src/main/res/values-night/reader_styles.xml index 1c1c17ae646e..829b5eeab81f 100644 --- a/WordPress/src/main/res/values-night/reader_styles.xml +++ b/WordPress/src/main/res/values-night/reader_styles.xml @@ -10,4 +10,13 @@ @dimen/margin_medium @dimen/reader_follow_button_min_height + + diff --git a/WordPress/src/main/res/values/reader_styles.xml b/WordPress/src/main/res/values/reader_styles.xml index 025470a77efc..273987324531 100644 --- a/WordPress/src/main/res/values/reader_styles.xml +++ b/WordPress/src/main/res/values/reader_styles.xml @@ -221,6 +221,15 @@ @dimen/reader_follow_button_min_height + + + + diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderPostDetailUiStateBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderPostDetailUiStateBuilderTest.kt index 7fde3c85fcdf..661552488fc6 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderPostDetailUiStateBuilderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/ReaderPostDetailUiStateBuilderTest.kt @@ -23,14 +23,11 @@ import org.wordpress.android.models.ReaderPost import org.wordpress.android.ui.reader.discover.ReaderPostUiStateBuilder import org.wordpress.android.ui.reader.models.ReaderSimplePost import org.wordpress.android.ui.reader.models.ReaderSimplePostList -import org.wordpress.android.ui.reader.utils.FeaturedImageUtils -import org.wordpress.android.ui.reader.utils.ReaderUtilsWrapper import org.wordpress.android.ui.reader.utils.ThreadedCommentsUtils import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.CommentSnippetState.CommentSnippetData import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.CommentSnippetState.Loading import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiState.ReaderPostDetailsUiState import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiState.ReaderPostDetailsUiState.ExcerptFooterUiState -import org.wordpress.android.ui.reader.viewmodels.ReaderPostDetailViewModel.UiState.ReaderPostDetailsUiState.ReaderPostFeaturedImageUiState import org.wordpress.android.ui.reader.views.ReaderPostDetailsHeaderViewUiStateBuilder import org.wordpress.android.ui.reader.views.uistates.CommentItemType.BUTTON import org.wordpress.android.ui.reader.views.uistates.CommentItemType.COMMENT @@ -40,7 +37,6 @@ import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.ui.utils.UiString.UiStringResWithParams import org.wordpress.android.ui.utils.UiString.UiStringText import org.wordpress.android.util.DateTimeUtilsWrapper -import org.wordpress.android.util.DisplayUtilsWrapper import org.wordpress.android.util.WPAvatarUtilsWrapper import org.wordpress.android.viewmodel.ContextProvider import org.wordpress.android.viewmodel.ResourceProvider @@ -57,15 +53,6 @@ class ReaderPostDetailUiStateBuilderTest : BaseUnitTest() { @Mock lateinit var headerViewUiStateBuilder: ReaderPostDetailsHeaderViewUiStateBuilder - @Mock - lateinit var featuredImageUtils: FeaturedImageUtils - - @Mock - lateinit var readerUtilsWrapper: ReaderUtilsWrapper - - @Mock - lateinit var displayUtilsWrapper: DisplayUtilsWrapper - @Mock lateinit var resourceProvider: ResourceProvider @@ -102,22 +89,21 @@ class ReaderPostDetailUiStateBuilderTest : BaseUnitTest() { this.feedId = 2L this.blogName = "blog name" } - private val dummyOnRelatedPostItemClicked: (Long, Long, Boolean) -> Unit = { _, _, _ -> } - private val dummyonCommentSnippetClicked: (Long, Long) -> Unit = { _, _ -> } - private val dummyFeaturedImageUrl = "/image/url" + private val dummyOnRelatedPostItemClicked: (Long, Long, Boolean) -> Unit = + { _, _, _ -> } + private val dummyonCommentSnippetClicked: (Long, Long) -> Unit = + { _, _ -> } private val dummyVisitPostLinkText = "visit post" - private val dummyDisplayPixelHeight = 100 @Before fun setUp() = test { - dummyRelatedPosts = ReaderSimplePostList().apply { add(readerSimplePost) } + dummyRelatedPosts = ReaderSimplePostList().apply { + add(readerSimplePost) + } builder = ReaderPostDetailUiStateBuilder( headerViewUiStateBuilder, postUiStateBuilder, - featuredImageUtils, - readerUtilsWrapper, - displayUtilsWrapper, contextProvider, htmlUtilsWrapper, htmlMessageUtils, @@ -128,100 +114,95 @@ class ReaderPostDetailUiStateBuilderTest : BaseUnitTest() { ) } - /* READER POST FEATURED IMAGE */ - @Test - fun `given featured image should be shown, when post ui is built, then featured image exists`() = test { - val postUiState = buildPostUiState(shouldShowFeaturedImage = true) - - assertThat(postUiState.featuredImageUiState).isEqualTo( - ReaderPostFeaturedImageUiState( - blogId = dummySourceReaderPost.blogId, - url = dummyFeaturedImageUrl, - height = (dummyDisplayPixelHeight * READER_POST_FEATURED_IMAGE_HEIGHT_PERCENT).toInt() - ) - ) - } - - @Test - fun `given featured image should not be shown, when post ui is built, then featured image does not exists`() = - test { - val postUiState = buildPostUiState(shouldShowFeaturedImage = false) - - assertThat(postUiState.featuredImageUiState).isNull() - } - /* EXCERPT FOOTER */ @Test - fun `given excerpt is shown, when post ui is built, then excerpt footer exists`() = test { - val readerPost = mock() - whenever(readerPost.blogName).thenReturn("blog name") - whenever(readerPost.url).thenReturn("url") - whenever(readerPost.shouldShowExcerpt()).thenReturn(true) - - val postUiState = buildPostUiState(readerPost = readerPost) - - assertThat(postUiState.excerptFooterUiState).isEqualTo( - ExcerptFooterUiState( - visitPostExcerptFooterLinkText = UiStringText(dummyVisitPostLinkText), - postLink = readerPost.url + fun `given excerpt is shown, when post ui is built, then excerpt footer exists`() = + test { + val readerPost = mock() + whenever(readerPost.blogName).thenReturn("blog name") + whenever(readerPost.url).thenReturn("url") + whenever(readerPost.shouldShowExcerpt()).thenReturn(true) + + val postUiState = buildPostUiState(readerPost = readerPost) + + assertThat(postUiState.excerptFooterUiState).isEqualTo( + ExcerptFooterUiState( + visitPostExcerptFooterLinkText = + UiStringText(dummyVisitPostLinkText), + postLink = readerPost.url + ) ) - ) - } + } @Test - fun `given excerpt is not shown, when post ui is built, then excerpt footer does not exists`() = test { - val readerPost = mock() - whenever(readerPost.shouldShowExcerpt()).thenReturn(false) + fun `given excerpt is not shown, when post ui is built, then excerpt footer does not exists`() = + test { + val readerPost = mock() + whenever(readerPost.shouldShowExcerpt()).thenReturn(false) - val postUiState = buildPostUiState(readerPost = readerPost) + val postUiState = buildPostUiState(readerPost = readerPost) - assertThat(postUiState.excerptFooterUiState).isNull() - } + assertThat(postUiState.excerptFooterUiState).isNull() + } /* RELATED POSTS */ @Test - fun `when local related posts ui is built, then source post site name exists in header label`() = test { - val relatedPostsUiState = buildRelatedPostsUiState(isGlobal = false) - - assertThat(relatedPostsUiState.headerLabel).isEqualTo( - UiStringResWithParams( - R.string.reader_label_local_related_posts, - listOf(UiStringText(dummySourceReaderPost.blogName)) + fun `when local related posts ui is built, then source post site name exists in header label`() = + test { + val relatedPostsUiState = + buildRelatedPostsUiState(isGlobal = false) + + assertThat(relatedPostsUiState.headerLabel).isEqualTo( + UiStringResWithParams( + R.string.reader_label_local_related_posts, + listOf( + UiStringText(dummySourceReaderPost.blogName) + ) + ) ) - ) - } + } @Test - fun `when global related posts ui is built, then global related posts header label exists`() = test { - val relatedPostsUiState = buildRelatedPostsUiState(isGlobal = true) + fun `when global related posts ui is built, then global related posts header label exists`() = + test { + val relatedPostsUiState = + buildRelatedPostsUiState(isGlobal = true) - assertThat(relatedPostsUiState.headerLabel).isEqualTo(UiStringRes(R.string.reader_label_global_related_posts)) - } + assertThat(relatedPostsUiState.headerLabel).isEqualTo( + UiStringRes(R.string.reader_label_global_related_posts) + ) + } @Test - fun `given empty related posts, when related posts ui is built, then related post cards are empty`() = test { - val relatedPostsUiState = buildRelatedPostsUiState(relatedPosts = ReaderSimplePostList()) + fun `given empty related posts, when related posts ui is built, then related post cards are empty`() = + test { + val relatedPostsUiState = buildRelatedPostsUiState( + relatedPosts = ReaderSimplePostList() + ) - assertThat(relatedPostsUiState.cards).isEmpty() - } + assertThat(relatedPostsUiState.cards).isEmpty() + } @Test - fun `given related posts, when related posts ui is built, then related post cards exist`() = test { - val relatedPostsUiState = buildRelatedPostsUiState() + fun `given related posts, when related posts ui is built, then related post cards exist`() = + test { + val relatedPostsUiState = buildRelatedPostsUiState() - assertThat(relatedPostsUiState.cards).isNotEmpty - } + assertThat(relatedPostsUiState.cards).isNotEmpty + } @Test - fun `given related post with title, when related posts ui is built, then related post title exists`() = test { - val title = "title" - whenever(readerSimplePost.hasTitle()).thenReturn(true) - whenever(readerSimplePost.title).thenReturn(title) + fun `given related post with title, when related posts ui is built, then related post title exists`() = + test { + val title = "title" + whenever(readerSimplePost.hasTitle()).thenReturn(true) + whenever(readerSimplePost.title).thenReturn(title) - val relatedPostsUiState = buildRelatedPostsUiState() + val relatedPostsUiState = buildRelatedPostsUiState() - assertThat(relatedPostsUiState.cards?.first()?.title).isEqualTo(UiStringText(title)) - } + assertThat(relatedPostsUiState.cards?.first()?.title) + .isEqualTo(UiStringText(title)) + } @Test fun `given related post without title, when related posts ui is built, then related post title does not exists`() = @@ -234,15 +215,17 @@ class ReaderPostDetailUiStateBuilderTest : BaseUnitTest() { } @Test - fun `given related post with excerpt, when related posts ui is built, then excerpt exists`() = test { - val excerpt = "excerpt" - whenever(readerSimplePost.hasExcerpt()).thenReturn(true) - whenever(readerSimplePost.excerpt).thenReturn(excerpt) + fun `given related post with excerpt, when related posts ui is built, then excerpt exists`() = + test { + val excerpt = "excerpt" + whenever(readerSimplePost.hasExcerpt()).thenReturn(true) + whenever(readerSimplePost.excerpt).thenReturn(excerpt) - val relatedPostsUiState = buildRelatedPostsUiState() + val relatedPostsUiState = buildRelatedPostsUiState() - assertThat(relatedPostsUiState.cards?.first()?.excerpt).isEqualTo(UiStringText(excerpt)) - } + assertThat(relatedPostsUiState.cards?.first()?.excerpt) + .isEqualTo(UiStringText(excerpt)) + } @Test fun `given related post without excerpt, when related posts ui is built, then excerpt does not exists`() = @@ -258,21 +241,29 @@ class ReaderPostDetailUiStateBuilderTest : BaseUnitTest() { fun `given related post with featured image url, when related posts ui is built, then featured image exists`() = test { val url = "/featured/image/url" - whenever(readerSimplePost.getFeaturedImageForDisplay(any(), any())).thenReturn(url) + whenever( + readerSimplePost.getFeaturedImageForDisplay(any(), any()) + ).thenReturn(url) val relatedPostsUiState = buildRelatedPostsUiState() - assertThat(relatedPostsUiState.cards?.first()?.featuredImageUrl).isEqualTo(url) + assertThat( + relatedPostsUiState.cards?.first()?.featuredImageUrl + ).isEqualTo(url) } @Test fun `given related post without featured image url, when related posts ui is built, then featured image exists`() = test { - whenever(readerSimplePost.getFeaturedImageForDisplay(any(), any())).thenReturn(null) + whenever( + readerSimplePost.getFeaturedImageForDisplay(any(), any()) + ).thenReturn(null) val relatedPostsUiState = buildRelatedPostsUiState() - assertThat(relatedPostsUiState.cards?.first()?.featuredImageUrl).isNull() + assertThat( + relatedPostsUiState.cards?.first()?.featuredImageUrl + ).isNull() } @Test @@ -302,10 +293,16 @@ class ReaderPostDetailUiStateBuilderTest : BaseUnitTest() { whenever(context.resources).thenReturn(resources) whenever(resources.getDimensionPixelSize(anyInt())).thenReturn(10) - whenever(dateTimeUtilsWrapper.dateFromIso8601(anyString())).thenReturn(Date()) - whenever(dateTimeUtilsWrapper.javaDateToTimeSpan(anyOrNull())).thenReturn("") + whenever( + dateTimeUtilsWrapper.dateFromIso8601(anyString()) + ).thenReturn(Date()) + whenever( + dateTimeUtilsWrapper.javaDateToTimeSpan(anyOrNull()) + ).thenReturn("") - whenever(avatarUtilsWrapper.rewriteAvatarUrl(anyString(), anyInt())).thenReturn("") + whenever( + avatarUtilsWrapper.rewriteAvatarUrl(anyString(), anyInt()) + ).thenReturn("") val comment = ReaderComment().apply { authorName = "" @@ -324,7 +321,8 @@ class ReaderPostDetailUiStateBuilderTest : BaseUnitTest() { dummyonCommentSnippetClicked ) - assertThat(snippetUiState.snippetItems.first().type).isEqualTo(COMMENT) + assertThat(snippetUiState.snippetItems.first().type) + .isEqualTo(COMMENT) assertThat(snippetUiState.snippetItems[1].type).isEqualTo(BUTTON) } @@ -340,28 +338,29 @@ class ReaderPostDetailUiStateBuilderTest : BaseUnitTest() { private fun buildPostUiState( readerPost: ReaderPost? = null, - shouldShowFeaturedImage: Boolean = false ): ReaderPostDetailsUiState { val post = readerPost ?: dummySourceReaderPost if (post.shouldShowExcerpt()) { val dummyLinkHexColor = "#FFFFFF" - whenever(htmlUtilsWrapper.colorResToHtmlColor(anyOrNull(), any())).thenReturn(dummyLinkHexColor) + whenever( + htmlUtilsWrapper.colorResToHtmlColor(anyOrNull(), any()) + ).thenReturn(dummyLinkHexColor) whenever( htmlMessageUtils.getHtmlMessageFromStringFormatResId( R.string.reader_excerpt_link, - "" + post.blogName + "" + "" + + post.blogName + "" ) ).thenReturn(dummyVisitPostLinkText) } - whenever(featuredImageUtils.shouldAddFeaturedImage(any())).thenReturn(shouldShowFeaturedImage) - whenever(displayUtilsWrapper.getWindowPixelHeight()).thenReturn(dummyDisplayPixelHeight) - whenever(readerUtilsWrapper.getResizedImageUrl(any(), any(), any(), any(), any())) - .thenReturn(dummyFeaturedImageUrl) - - whenever(headerViewUiStateBuilder.mapPostToUiState(any(), any())).thenReturn(mock()) - whenever(postUiStateBuilder.mapPostToActions(any(), any())).thenReturn(mock()) + whenever( + headerViewUiStateBuilder.mapPostToUiState(any(), any()) + ).thenReturn(mock()) + whenever( + postUiStateBuilder.mapPostToActions(any(), any()) + ).thenReturn(mock()) return builder.mapPostToUiState( post = post, diff --git a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt index 99609558cf32..aa8d1be7d471 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/reader/viewmodels/ReaderPostDetailViewModelTest.kt @@ -100,7 +100,7 @@ import org.wordpress.android.ui.reader.views.uistates.InteractionSectionUiState import org.wordpress.android.ui.reader.views.uistates.ReaderBlogSectionUiState import org.wordpress.android.ui.reader.views.uistates.ReaderBlogSectionUiState.ReaderBlogSectionClickData import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderAction -import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderViewUiState.ReaderPostDetailsHeaderUiState +import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderUiState import org.wordpress.android.ui.utils.HtmlMessageUtils import org.wordpress.android.ui.utils.UiDimen.UIDimenRes import org.wordpress.android.ui.utils.UiString.UiStringRes @@ -1292,13 +1292,12 @@ class ReaderPostDetailViewModelTest : BaseUnitTest() { return ReaderPostDetailsUiState( postId = post.postId, blogId = post.blogId, - featuredImageUiState = mock(), headerUiState = ReaderPostDetailsHeaderUiState( - UiStringText(post.title), - post.authorName, - listOf(TagUiState("", "", false, mock())), - true, - ReaderBlogSectionUiState( + title = UiStringText(post.title), + authorName = post.authorName, + tagItems = listOf(TagUiState("", "", false, mock())), + tagItemsVisibility = true, + blogSectionUiState = ReaderBlogSectionUiState( postId = post.postId, blogId = post.blogId, dateLine = "", @@ -1310,13 +1309,13 @@ class ReaderPostDetailViewModelTest : BaseUnitTest() { blavatarType = BLAVATAR_CIRCULAR, blogSectionClickData = ReaderBlogSectionClickData(mock(), 0) ), - FollowButtonUiState( + followButtonUiState = FollowButtonUiState( onFollowButtonClicked = mock(), isFollowed = false, isVisible = true ), - "", - InteractionSectionUiState( + dateLine = "", + interactionSectionUiState = InteractionSectionUiState( likeCount = 42, commentCount = 13, onLikesClicked = mock(),