From 8772fc46930a9b4b7953cad618844ab7a77a61f5 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 25 Mar 2026 10:21:25 -0400 Subject: [PATCH 01/20] Start reader improvements feature branch Co-Authored-By: Claude Opus 4.6 From 0fe604f13f940d2014aa9951f477545003f2ce6a Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Wed, 25 Mar 2026 10:29:34 -0400 Subject: [PATCH 02/20] Remove content-scanning fallbacks for Reader featured image Only use editorial.image, featured_image, or featured_media.uri as featured image sources instead of scanning post content for suitable images or videos. Co-Authored-By: Claude Opus 4.6 --- .../wordpress/android/models/ReaderPost.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java index 53330a207211..fb8e1e0cc409 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java +++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java @@ -8,8 +8,6 @@ import org.wordpress.android.ui.Organization; import org.wordpress.android.ui.reader.ReaderConstants; import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId; -import org.wordpress.android.ui.reader.utils.ReaderIframeScanner; -import org.wordpress.android.ui.reader.utils.ReaderImageScanner; import org.wordpress.android.ui.reader.utils.ReaderUtils; import org.wordpress.android.util.DateTimeUtilsWrapper; import org.wordpress.android.util.HtmlUtils; @@ -204,21 +202,6 @@ public static ReaderPost fromJson(JSONObject json) { } } - // if the post doesn't have a featured image but it contains an IMG tag, check whether - // we can find a suitable image from the content - if (!post.hasFeaturedImage() && post.hasImages()) { - post.mFeaturedImage = new ReaderImageScanner(post.mText, post.isPrivate) - .getLargestImage(ReaderConstants.MIN_FEATURED_IMAGE_WIDTH); - } - - // if there's no featured image or featured video and the post contains an iframe, scan - // the content for a suitable featured video - if (!post.hasFeaturedImage() - && !post.hasFeaturedVideo() - && post.getText().contains(" Date: Wed, 25 Mar 2026 11:10:35 -0400 Subject: [PATCH 03/20] Move featured image above title and excerpt in Reader post cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder ConstraintLayout constraints so the visual hierarchy is: blog header → featured image → title → excerpt → interactions → footer Co-Authored-By: Claude Opus 4.6 --- .../src/main/res/layout/reader_cardview_post_new.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/WordPress/src/main/res/layout/reader_cardview_post_new.xml b/WordPress/src/main/res/layout/reader_cardview_post_new.xml index 40ad290a02be..39a1c294286b 100644 --- a/WordPress/src/main/res/layout/reader_cardview_post_new.xml +++ b/WordPress/src/main/res/layout/reader_cardview_post_new.xml @@ -36,7 +36,7 @@ android:layout_marginTop="@dimen/margin_medium" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/layout_blog_section" + app:layout_constraintTop_toBottomOf="@id/reader_card_images_bottom_barrier" app:layout_constraintBottom_toTopOf="@+id/text_excerpt" tools:text="This the tile of this post - Lorem ipsum dolor sit amet, consectetur adipiscing elit" /> @@ -63,7 +63,7 @@ app:layout_constraintDimensionRatio="16:9" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/text_excerpt" + app:layout_constraintTop_toBottomOf="@id/layout_blog_section" tools:src="@color/blue_light" /> @@ -112,7 +112,7 @@ app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/reader_card_dot_separator" - app:layout_constraintTop_toBottomOf="@id/reader_card_images_bottom_barrier" + app:layout_constraintTop_toBottomOf="@id/text_excerpt" app:layout_constrainedWidth="true" tools:text="15 likes" /> @@ -127,7 +127,7 @@ android:importantForAccessibility="no" app:layout_constraintStart_toEndOf="@id/reader_card_like_count" app:layout_constraintEnd_toStartOf="@id/reader_card_comment_count" - app:layout_constraintTop_toBottomOf="@id/reader_card_images_bottom_barrier" + app:layout_constraintTop_toBottomOf="@id/text_excerpt" app:layout_constrainedWidth="true" /> From 5265ae12c6d2a536c3501ccc50e36e4d5bb32082 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 26 Mar 2026 09:11:47 -0400 Subject: [PATCH 04/20] Fix grammar in code comments Co-Authored-By: Claude Opus 4.6 --- .../main/java/org/wordpress/android/models/ReaderCardType.java | 2 +- .../org/wordpress/android/ui/reader/utils/ReaderImageScanner.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderCardType.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderCardType.java index a35d83737512..7b0706513ffe 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/ReaderCardType.java +++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderCardType.java @@ -47,7 +47,7 @@ && new ReaderImageScanner(post.getText(), post.isPrivate) } /* - * returns true if the post's content is 100 characters or less + * returns true if the post's content is 100 characters or fewer */ private static boolean hasMinContent(@NonNull ReaderPost post) { if (post.getExcerpt().length() > MIN_CONTENT_CHARS) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.kt index 707e98960d0b..6d9315ccdc9c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/ReaderImageScanner.kt @@ -62,7 +62,7 @@ class ReaderImageScanner(private val content: String, private val isPrivate: Boo } /* - * returns true if there at least `minImageCount` images in the post content that are at + * returns true if there is at least `minImageCount` images in the post content that are at * least `minImageWidth` in size */ fun hasUsableImageCount(minImageCount: Int, minImageWidth: Int): Boolean { From 1269c944b9f501b84d7b720d142eae07986dce8d Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 26 Mar 2026 15:17:39 -0400 Subject: [PATCH 05/20] Fit portrait featured images in Reader instead of cropping When a Reader post has a portrait featured image, the image is now scaled to fit the container height and centered horizontally with a gray background fill, instead of being center-cropped which often cuts off heads and other important content. Co-Authored-By: Claude Opus 4.6 --- .../viewholders/ReaderPostNewViewHolder.kt | 23 ++++-- .../android/util/image/ImageManager.kt | 32 +++++++++ .../image/PortraitAwareCropTransformation.kt | 70 +++++++++++++++++++ .../src/main/res/values-night/colors.xml | 1 + WordPress/src/main/res/values/colors.xml | 1 + 5 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/util/image/PortraitAwareCropTransformation.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderPostNewViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderPostNewViewHolder.kt index 44279354ac64..fb30fd59043d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderPostNewViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderPostNewViewHolder.kt @@ -2,6 +2,7 @@ package org.wordpress.android.ui.reader.discover.viewholders import android.view.Gravity import android.view.View +import androidx.core.content.ContextCompat import android.view.ViewGroup import androidx.appcompat.widget.ListPopupWindow import androidx.core.view.isVisible @@ -154,11 +155,18 @@ class ReaderPostNewViewHolder( if (state.featuredImageUrl == null) { imageManager.cancelRequestAndClearImageView(imageFeatured) } else { - imageManager.loadImageWithCorners( + imageManager.loadImageWithCornersPortraitAware( imageFeatured, PHOTO_ROUNDED_CORNERS, state.featuredImageUrl, - uiHelpers.getPxOfUiDimen(WordPress.getContext(), state.featuredImageCornerRadius) + uiHelpers.getPxOfUiDimen( + WordPress.getContext(), + state.featuredImageCornerRadius + ), + ContextCompat.getColor( + imageFeatured.context, + R.color.reader_featured_image_background + ) ) } } @@ -183,11 +191,18 @@ class ReaderPostNewViewHolder( state.fullVideoUrl?.let { videoUrl -> ReaderVideoUtils.retrieveVideoThumbnailUrl(videoUrl, object : VideoThumbnailUrlListener { override fun showThumbnail(thumbnailUrl: String) { - imageManager.loadImageWithCorners( + imageManager.loadImageWithCornersPortraitAware( imageFeatured, PHOTO_ROUNDED_CORNERS, thumbnailUrl, - uiHelpers.getPxOfUiDimen(WordPress.getContext(), state.featuredImageCornerRadius) + uiHelpers.getPxOfUiDimen( + WordPress.getContext(), + state.featuredImageCornerRadius + ), + ContextCompat.getColor( + imageFeatured.context, + R.color.reader_featured_image_background + ) ) } diff --git a/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt b/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt index 10338de2b94b..f79d3de75d26 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt @@ -13,6 +13,7 @@ import android.text.TextUtils import android.util.Base64 import android.widget.ImageView import android.widget.ImageView.ScaleType +import androidx.annotation.ColorInt import android.widget.ImageView.ScaleType.CENTER import android.widget.ImageView.ScaleType.FIT_CENTER import android.widget.ImageView.ScaleType.FIT_END @@ -274,6 +275,37 @@ class ImageManager @Inject constructor( .clearOnDetach() } + /** + * Loads an image from the "imgUrl" into the ImageView with a corner radius. + * For portrait images (height > width), fits the full image height and centers + * horizontally with a background color fill. For landscape/square images, + * applies standard CenterCrop. + */ + @JvmOverloads + fun loadImageWithCornersPortraitAware( + imageView: ImageView, + imageType: ImageType, + imgUrl: String, + cornerRadius: Int, + @ColorInt backgroundColor: Int, + requestListener: RequestListener? = null + ) { + val context = imageView.context + if (!context.isAvailable()) return + + Glide.with(context) + .load(imgUrl) + .transform( + PortraitAwareCropTransformation(backgroundColor), + RoundedCorners(cornerRadius) + ) + .addFallback(imageType) + .addPlaceholder(imageType) + .attachRequestListener(requestListener) + .into(imageView) + .clearOnDetach() + } + /** * Loads an image from the "imgUrl" into the ImageView. Adds a placeholder and an error placeholder depending * on the ImageType. Attaches the ResultListener so the client can manually show/hide progress and error 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 new file mode 100644 index 000000000000..4ebec874e275 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/image/PortraitAwareCropTransformation.kt @@ -0,0 +1,70 @@ +package org.wordpress.android.util.image + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import androidx.annotation.ColorInt +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import com.bumptech.glide.load.resource.bitmap.TransformationUtils +import java.security.MessageDigest + +/** + * A Glide BitmapTransformation that handles portrait images differently + * from landscape/square images: + * + * - Landscape/square: standard CenterCrop (existing behavior) + * - Portrait (height > width): scales to fit the target height, centers + * horizontally, and fills the remaining space with [backgroundColor] + */ +class PortraitAwareCropTransformation( + @ColorInt private val backgroundColor: Int +) : BitmapTransformation() { + override fun transform( + pool: BitmapPool, + toTransform: Bitmap, + outWidth: Int, + outHeight: Int + ): Bitmap { + if (toTransform.height <= toTransform.width) { + return TransformationUtils.centerCrop( + pool, toTransform, outWidth, outHeight + ) + } + + // Portrait: fit to height, center horizontally + val result = pool.get( + outWidth, outHeight, toTransform.config ?: Bitmap.Config.ARGB_8888 + ) + + val canvas = Canvas(result) + canvas.drawColor(backgroundColor) + + val scale = outHeight.toFloat() / toTransform.height.toFloat() + 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 + } + + override fun equals(other: Any?): Boolean { + return other is PortraitAwareCropTransformation && + backgroundColor == other.backgroundColor + } + + override fun hashCode(): Int = ID.hashCode() + backgroundColor + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update((ID + backgroundColor).toByteArray(CHARSET)) + } + + companion object { + private const val ID = + "org.wordpress.android.util.image" + + ".PortraitAwareCropTransformation" + } +} diff --git a/WordPress/src/main/res/values-night/colors.xml b/WordPress/src/main/res/values-night/colors.xml index 9ed50581ce5f..c8fa2e74e7af 100644 --- a/WordPress/src/main/res/values-night/colors.xml +++ b/WordPress/src/main/res/values-night/colors.xml @@ -23,6 +23,7 @@ @color/background_dark + @color/gray_80 @color/gray_10 diff --git a/WordPress/src/main/res/values/colors.xml b/WordPress/src/main/res/values/colors.xml index e1b0f698ae14..feeb1480717b 100644 --- a/WordPress/src/main/res/values/colors.xml +++ b/WordPress/src/main/res/values/colors.xml @@ -37,6 +37,7 @@ @color/white + @color/neutral_5 #ffededed From e15f7a2790e1229cd5af25b8d934cb99e6835848 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Thu, 26 Mar 2026 16:36:10 -0400 Subject: [PATCH 06/20] Fix import ordering in ReaderPostNewViewHolder Co-Authored-By: Claude Opus 4.6 --- .../ui/reader/discover/viewholders/ReaderPostNewViewHolder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderPostNewViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderPostNewViewHolder.kt index fb30fd59043d..e572f73afeca 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderPostNewViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderPostNewViewHolder.kt @@ -2,9 +2,9 @@ package org.wordpress.android.ui.reader.discover.viewholders import android.view.Gravity import android.view.View -import androidx.core.content.ContextCompat import android.view.ViewGroup import androidx.appcompat.widget.ListPopupWindow +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import org.wordpress.android.R import org.wordpress.android.WordPress From 63fc925920beae165a9138a743029d1159b589d0 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Fri, 27 Mar 2026 05:41:15 -0400 Subject: [PATCH 07/20] Fix import ordering and remove unnecessary @JvmOverloads in ImageManager Co-Authored-By: Claude Opus 4.6 --- .../main/java/org/wordpress/android/util/image/ImageManager.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt b/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt index f79d3de75d26..b0063ee7b9b1 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/image/ImageManager.kt @@ -13,7 +13,6 @@ import android.text.TextUtils import android.util.Base64 import android.widget.ImageView import android.widget.ImageView.ScaleType -import androidx.annotation.ColorInt import android.widget.ImageView.ScaleType.CENTER import android.widget.ImageView.ScaleType.FIT_CENTER import android.widget.ImageView.ScaleType.FIT_END @@ -21,6 +20,7 @@ import android.widget.ImageView.ScaleType.FIT_START import android.widget.ImageView.ScaleType.FIT_XY import android.widget.ImageView.ScaleType.MATRIX import android.widget.TextView +import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toDrawable @@ -281,7 +281,6 @@ class ImageManager @Inject constructor( * horizontally with a background color fill. For landscape/square images, * applies standard CenterCrop. */ - @JvmOverloads fun loadImageWithCornersPortraitAware( imageView: ImageView, imageType: ImageType, From bdd5c6251f17d3a99c56cd3b51e6419d0ade8591 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Fri, 27 Mar 2026 08:48:44 -0400 Subject: [PATCH 08/20] Redesign Reader post detail header for adaptive layout Move featured image from collapsing AppBar to inline in the header view with adaptive aspect ratio. Restructure blog section so site name, author, and date each appear on their own line with absolute date+time format. Add reading time indicator and excerpt display to the header. Co-Authored-By: Claude Opus 4.6 --- .../ui/reader/ReaderPostDetailFragment.kt | 42 +--- .../reader/ReaderPostDetailUiStateBuilder.kt | 27 -- .../viewmodels/ReaderPostDetailViewModel.kt | 4 +- .../views/ReaderPostDetailHeaderView.kt | 136 +++++++++-- ...aderPostDetailsHeaderViewUiStateBuilder.kt | 117 ++++++++- .../uistates/ReaderPostDetailsHeaderAction.kt | 4 + .../ReaderPostDetailsHeaderViewUiState.kt | 9 + .../src/main/res/drawable/ic_clock_16dp.xml | 10 + .../appbar_with_collapsing_toolbar_layout.xml | 13 - .../layout/reader_blog_section_view_new.xml | 48 +--- .../layout/reader_post_detail_header_view.xml | 71 +++++- WordPress/src/main/res/values/strings.xml | 1 + WordPress/src/main/res/values/styles.xml | 5 + .../ReaderPostDetailUiStateBuilderTest.kt | 231 +++++++++--------- .../ReaderPostDetailViewModelTest.kt | 17 +- 15 files changed, 457 insertions(+), 278 deletions(-) create mode 100644 WordPress/src/main/res/drawable/ic_clock_16dp.xml 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..e8f4a369980c 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 @@ -48,7 +47,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 @@ -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 @@ -293,25 +290,18 @@ class ReaderPostDetailFragment : ViewPagerFragment(), get() = view != null && readerWebView.isCustomViewShowing private val appBarLayoutOffsetChangedListener = - AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> - val collapsingToolbarLayout = appBarLayout - .findViewById(R.id.collapsing_toolbar) + AppBarLayout.OnOffsetChangedListener { appBarLayout, _ -> 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 color = context.getColorFromAttribute( + MaterialR.attr.colorOnSurface + ) val colorFilter = BlendModeColorFilterCompat - .createBlendModeColorFilterCompat(color, BlendModeCompat.SRC_ATOP) + .createBlendModeColorFilterCompat( + color, BlendModeCompat.SRC_ATOP + ) toolbar.setTitleTextColor(color) toolbar.navigationIcon?.colorFilter = colorFilter @@ -801,7 +791,6 @@ class ReaderPostDetailFragment : ViewPagerFragment(), binding.headerView.updatePost(state.headerUiState, getReadingPreferences()) showOrHideMoreMenu(state) - updateFeaturedImage(state.featuredImageUiState, binding) updateExcerptFooter(state.excerptFooterUiState) with(layoutFooterBinding) { @@ -933,23 +922,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..59c40f38870c 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,7 +68,6 @@ class ReaderPostDetailUiStateBuilder @Inject constructor( ) = ReaderPostDetailsUiState( postId = post.postId, blogId = post.blogId, - featuredImageUiState = buildReaderPostFeaturedImageUiState(post), headerUiState = buildPostDetailsHeaderUiState( post, onHeaderAction, @@ -209,24 +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, 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..c205388f03d7 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 @@ -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,7 +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?) 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..46b4337bd1b6 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,6 +4,7 @@ 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 androidx.core.view.isVisible import org.wordpress.android.R @@ -15,6 +16,7 @@ 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.ReaderFeaturedImageUiState import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderViewUiState.ReaderPostDetailsHeaderUiState import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.ui.utils.UiString @@ -22,6 +24,7 @@ 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 +45,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( @@ -60,14 +65,25 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( setAuthorAndDate(uiState.authorName, uiState.dateLine) - uiHelpers.setTextOrHide(layoutBlogSection.blogSectionTextBlogName, uiState.blogSectionUiState.blogName) + uiHelpers.setTextOrHide( + layoutBlogSection.blogSectionTextBlogName, + uiState.blogSectionUiState.blogName + ) updateFollowButton(uiState.followButtonUiState) updateAvatars(uiState.blogSectionUiState) updateBlogSectionClick(uiState.blogSectionUiState) - updateInteractionSection(uiState.interactionSectionUiState, readingPreferences, themeValues) + updateReadingTime(uiState.readingTime) + updateFeaturedImage(uiState.featuredImageUiState) + updateExcerpt(uiState.excerpt) + + updateInteractionSection( + uiState.interactionSectionUiState, + readingPreferences, + themeValues + ) } private fun ReaderPostDetailHeaderViewBinding.updateTitle( @@ -78,7 +94,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 +107,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 +121,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 +157,65 @@ 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.updateReadingTime( + readingTime: UiString? + ) { + if (readingTime != null) { + textReadingTime.text = + uiHelpers.getTextOfUiString(root.context, readingTime) + textReadingTime.setVisible(true) + } else { + textReadingTime.setVisible(false) + } + } + + 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 ReaderPostDetailHeaderViewBinding.updateExcerpt( + excerpt: UiString? + ) { + if (excerpt != null) { + textExcerpt.text = + uiHelpers.getTextOfUiString(root.context, excerpt) + textExcerpt.setVisible(true) + } else { + textExcerpt.setVisible(false) + } } private fun updateInteractionSection( @@ -148,14 +228,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 +257,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) } - } + listOf( + binding.headerLikeCount, + binding.headerCommentCount, + binding.headerDotSeparator + ).forEach { view -> + view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize) + view.typeface = typeface + textColor?.let { view.setTextColor(it) } + } } } 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..85feed99f18c 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,39 @@ package org.wordpress.android.ui.reader.views +import androidx.core.text.HtmlCompat import dagger.Reusable +import org.wordpress.android.R 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.ReaderFeaturedImageUiState import org.wordpress.android.ui.reader.views.uistates.ReaderPostDetailsHeaderViewUiState.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 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, ) { fun mapPostToUiState( post: ReaderPost, @@ -32,22 +48,50 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( authorName = post.authorName, tagItems = buildTagItems( post, - onClicked = { onHeaderAction(ReaderPostDetailsHeaderAction.TagItemClicked(it)) } + onClicked = { + onHeaderAction( + ReaderPostDetailsHeaderAction.TagItemClicked(it) + ) + } ), tagItemsVisibility = buildTagItemsVisibility(post), blogSectionUiState = buildBlogSectionUiState( post, - onBlogSectionClicked = { onHeaderAction(ReaderPostDetailsHeaderAction.BlogSectionClicked) } + onBlogSectionClicked = { + onHeaderAction( + ReaderPostDetailsHeaderAction.BlogSectionClicked + ) + } ), followButtonUiState = buildFollowButtonUiState( post, - onFollowClicked = { onHeaderAction(ReaderPostDetailsHeaderAction.FollowClicked) } + onFollowClicked = { + onHeaderAction(ReaderPostDetailsHeaderAction.FollowClicked) + } ), dateLine = buildDateLine(post), + readingTime = buildReadingTime(post), + excerpt = buildExcerpt(post), + featuredImageUiState = buildFeaturedImageUiState( + post, + onFeaturedImageClicked = { blogId, url -> + onHeaderAction( + ReaderPostDetailsHeaderAction.FeaturedImageClicked( + blogId, url + ) + ) + } + ), interactionSectionUiState = buildInteractionSection( post, - onLikesClicked = { onHeaderAction(ReaderPostDetailsHeaderAction.LikesClicked) }, - onCommentsClicked = { onHeaderAction(ReaderPostDetailsHeaderAction.CommentsClicked) } + onLikesClicked = { + onHeaderAction(ReaderPostDetailsHeaderAction.LikesClicked) + }, + onCommentsClicked = { + onHeaderAction( + ReaderPostDetailsHeaderAction.CommentsClicked + ) + } ) ) } @@ -73,13 +117,66 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( ) } - private fun buildTagItems(post: ReaderPost, onClicked: (String) -> Unit) = - readerPostTagsUiStateBuilder.mapPostTagsToTagUiStates(post, onClicked) + 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): String { + val date = post.getDisplayDate(dateTimeUtilsWrapper) ?: return "" + val format = SimpleDateFormat( + "MMM d, yyyy 'at' h:mm a", Locale.getDefault() + ) + return format.format(date) + } + + private fun buildReadingTime(post: ReaderPost): UiString? { + if (post.shouldShowExcerpt()) return null + val content = post.text + if (content.isNullOrBlank()) return null - private fun buildTagItemsVisibility(post: ReaderPost) = post.tags.isNotEmpty() + val text = HtmlCompat.fromHtml( + content.replace(Regex("]*>"), ""), + HtmlCompat.FROM_HTML_MODE_LEGACY + ).toString().trim() - private fun buildDateLine(post: ReaderPost) = - dateTimeUtilsWrapper.javaDateToTimeSpan(post.getDisplayDate(dateTimeUtilsWrapper)) + if (text.isBlank()) return null + val wordCount = text.split("\\s+".toRegex()).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 buildExcerpt(post: ReaderPost): UiString? { + if (!post.hasExcerpt()) return null + return UiStringText(post.excerpt) + } + + private fun buildFeaturedImageUiState( + post: ReaderPost, + 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, + ) + } private fun buildInteractionSection( post: ReaderPost, 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..b5d191036518 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 @@ -12,6 +12,15 @@ sealed class ReaderPostDetailsHeaderViewUiState { val blogSectionUiState: ReaderBlogSectionUiState, val followButtonUiState: FollowButtonUiState, val dateLine: String, + val readingTime: UiString? = null, + val excerpt: UiString? = null, + val featuredImageUiState: ReaderFeaturedImageUiState? = null, val interactionSectionUiState: InteractionSectionUiState, ) : ReaderPostDetailsHeaderViewUiState() + + data class ReaderFeaturedImageUiState( + val blogId: Long, + val url: String? = null, + val onFeaturedImageClicked: ((Long, String) -> Unit)? = null, + ) } 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..d808864e2aaf --- /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"> @@ -36,57 +35,36 @@ android:includeFontPadding="false" app:layout_constraintStart_toEndOf="@id/blog_section_image_blog_avatar" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toTopOf="@id/blog_section_text_author" + app:layout_constraintTop_toTopOf="@id/blog_section_image_blog_avatar" + app:layout_constraintBottom_toBottomOf="@id/blog_section_image_blog_avatar" tools:text="My Blog Name" /> - - + app:layout_constraintTop_toBottomOf="@id/blog_section_text_author" + 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..7db8f5e27c57 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 @@ -10,7 +10,7 @@ + tools:text="Post Title: This could be a quite big title" /> + + + + + + + + + + + + + @@ -85,7 +144,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/excerpt_barrier" app:layout_constrainedWidth="true" /> diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 87e4e307caa9..b507f8eed640 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -2457,6 +2457,7 @@ What would you like to find? More from %s More on WordPress.com + %s min read Visit site Choose your interests View comments diff --git a/WordPress/src/main/res/values/styles.xml b/WordPress/src/main/res/values/styles.xml index 3fce89677ffc..f6da3ceb4914 100644 --- a/WordPress/src/main/res/values/styles.xml +++ b/WordPress/src/main/res/values/styles.xml @@ -1664,4 +1664,9 @@ rounded @dimen/blavatar_sz_radius + + 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..8956c5ddfabe 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 @@ -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(), From e709ea72a86725e036190c5a10e3cd4bca2ed8a6 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Fri, 27 Mar 2026 09:25:51 -0400 Subject: [PATCH 09/20] Simplify Reader post detail header and image transformation code - Extract Regex and date format constants in header UI state builder - Replace verbose null-check patterns with setTextOrHide helper - Hoist Paint allocation to class property in PortraitAwareCropTransformation - Remove unnecessary list allocation in applyInteractionSectionTheme Co-Authored-By: Claude Opus 4.6 --- .../views/ReaderPostDetailHeaderView.kt | 27 ++++++------------- ...aderPostDetailsHeaderViewUiStateBuilder.kt | 18 ++++++++----- .../image/PortraitAwareCropTransformation.kt | 3 ++- 3 files changed, 22 insertions(+), 26 deletions(-) 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 46b4337bd1b6..3996dcb9d255 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 @@ -6,6 +6,7 @@ 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 @@ -181,13 +182,7 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( private fun ReaderPostDetailHeaderViewBinding.updateReadingTime( readingTime: UiString? ) { - if (readingTime != null) { - textReadingTime.text = - uiHelpers.getTextOfUiString(root.context, readingTime) - textReadingTime.setVisible(true) - } else { - textReadingTime.setVisible(false) - } + uiHelpers.setTextOrHide(textReadingTime, readingTime) } private fun ReaderPostDetailHeaderViewBinding.updateFeaturedImage( @@ -209,13 +204,7 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( private fun ReaderPostDetailHeaderViewBinding.updateExcerpt( excerpt: UiString? ) { - if (excerpt != null) { - textExcerpt.text = - uiHelpers.getTextOfUiString(root.context, excerpt) - textExcerpt.setVisible(true) - } else { - textExcerpt.setVisible(false) - } + uiHelpers.setTextOrHide(textExcerpt, excerpt) } private fun updateInteractionSection( @@ -257,14 +246,14 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( val typeface = readingPreferences.fontFamily.toTypeface() val textColor = themeValues?.intTextColor - listOf( - binding.headerLikeCount, - binding.headerCommentCount, - binding.headerDotSeparator - ).forEach { view -> + 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 85feed99f18c..826367b23790 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 @@ -127,10 +127,9 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( private fun buildDateLine(post: ReaderPost): String { val date = post.getDisplayDate(dateTimeUtilsWrapper) ?: return "" - val format = SimpleDateFormat( - "MMM d, yyyy 'at' h:mm a", Locale.getDefault() - ) - return format.format(date) + return SimpleDateFormat( + DATE_FORMAT_PATTERN, Locale.getDefault() + ).format(date) } private fun buildReadingTime(post: ReaderPost): UiString? { @@ -139,12 +138,12 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( if (content.isNullOrBlank()) return null val text = HtmlCompat.fromHtml( - content.replace(Regex("]*>"), ""), + content.replace(IMG_TAG_REGEX, ""), HtmlCompat.FROM_HTML_MODE_LEGACY ).toString().trim() if (text.isBlank()) return null - val wordCount = text.split("\\s+".toRegex()).size + val wordCount = text.split(WHITESPACE_REGEX).size val minutes = ceil(wordCount.toDouble() / WORDS_PER_MINUTE) .toInt() .coerceAtLeast(1) @@ -188,4 +187,11 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( onLikesClicked = onLikesClicked, onCommentsClicked = onCommentsClicked, ) + + 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/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 From db6cd2c03d928f8167424ca7f5342a97a117c6e7 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Fri, 27 Mar 2026 10:42:23 -0400 Subject: [PATCH 10/20] Fix crash in Reader post detail caused by unresolvable Material3 attribute Replace ?attr/colorOnSurfaceVariant (Material3-only) with compatible alternatives since the reading preferences ContextThemeWrapper uses Theme.MaterialComponents.DayNight which lacks this attribute. Co-Authored-By: Claude Opus 4.6 --- WordPress/src/main/res/drawable/ic_clock_16dp.xml | 2 +- .../src/main/res/layout/reader_post_detail_header_view.xml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/WordPress/src/main/res/drawable/ic_clock_16dp.xml b/WordPress/src/main/res/drawable/ic_clock_16dp.xml index d808864e2aaf..9dbfe4d1de30 100644 --- a/WordPress/src/main/res/drawable/ic_clock_16dp.xml +++ b/WordPress/src/main/res/drawable/ic_clock_16dp.xml @@ -3,7 +3,7 @@ android:height="16dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?attr/colorOnSurfaceVariant"> + android:tint="?attr/colorOnSurface"> 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 7db8f5e27c57..60d7dea3b0b6 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 @@ -103,7 +103,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/margin_medium" - android:textColor="?attr/colorOnSurfaceVariant" android:visibility="gone" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" From a1eeb7f4f29832a0b0ca216e6e1238e9c65bfc4f Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Fri, 27 Mar 2026 11:38:00 -0400 Subject: [PATCH 11/20] Simplify Reader post detail: remove dead code and redundant scroll listener - Remove unused postDetailsHeaderViewUiStateBuilder injection from fragment - Replace per-scroll toolbar coloring with one-time setup in initAppBar - Inline trivial buildPostDetailsHeaderUiState delegation - Remove unnecessary default values in ReaderFeaturedImageUiState Co-Authored-By: Claude Opus 4.6 --- .../ui/reader/ReaderPostDetailFragment.kt | 44 +++++++------------ .../reader/ReaderPostDetailUiStateBuilder.kt | 10 +---- .../ReaderPostDetailsHeaderViewUiState.kt | 4 +- 3 files changed, 18 insertions(+), 40 deletions(-) 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 e8f4a369980c..f2549299e996 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 @@ -116,7 +116,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 @@ -248,9 +247,6 @@ class ReaderPostDetailFragment : ViewPagerFragment(), @Inject internal lateinit var imageManager: ImageManager - @Inject - lateinit var postDetailsHeaderViewUiStateBuilder: ReaderPostDetailsHeaderViewUiStateBuilder - @Inject lateinit var readerUtilsWrapper: ReaderUtilsWrapper @@ -289,29 +285,6 @@ class ReaderPostDetailFragment : ViewPagerFragment(), val isCustomViewShowing: Boolean get() = view != null && readerWebView.isCustomViewShowing - private val appBarLayoutOffsetChangedListener = - AppBarLayout.OnOffsetChangedListener { appBarLayout, _ -> - val toolbar = appBarLayout.findViewById(R.id.toolbar_main) - - view?.context?.let { context -> - val menu: Menu = toolbar.menu - val color = context.getColorFromAttribute( - MaterialR.attr.colorOnSurface - ) - 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) @@ -394,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 @@ -420,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) { 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 59c40f38870c..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 @@ -68,7 +68,7 @@ class ReaderPostDetailUiStateBuilder @Inject constructor( ) = ReaderPostDetailsUiState( postId = post.postId, blogId = post.blogId, - headerUiState = buildPostDetailsHeaderUiState( + headerUiState = postDetailsHeaderViewUiStateBuilder.mapPostToUiState( post, onHeaderAction, ), @@ -200,14 +200,6 @@ class ReaderPostDetailUiStateBuilder @Inject constructor( onItemClicked = onItemClicked ) - 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/views/uistates/ReaderPostDetailsHeaderViewUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/uistates/ReaderPostDetailsHeaderViewUiState.kt index b5d191036518..44b5bf8fb689 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 @@ -20,7 +20,7 @@ sealed class ReaderPostDetailsHeaderViewUiState { data class ReaderFeaturedImageUiState( val blogId: Long, - val url: String? = null, - val onFeaturedImageClicked: ((Long, String) -> Unit)? = null, + val url: String?, + val onFeaturedImageClicked: ((Long, String) -> Unit)?, ) } From 17a6afc83a6a52f5089e5b0b3ef685b5143f6004 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Fri, 27 Mar 2026 12:19:51 -0400 Subject: [PATCH 12/20] Use fastStripHtml for reading time and fix code quality issues Replace HtmlCompat.fromHtml() with lighter HtmlUtils.fastStripHtml() for word counting, fix detekt ReturnCount and checkstyle empty-line violations. Co-Authored-By: Claude Opus 4.6 --- .../viewmodels/ReaderPostDetailViewModel.kt | 1 - ...aderPostDetailsHeaderViewUiStateBuilder.kt | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) 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 c205388f03d7..7a13b97c8def 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 @@ -931,7 +931,6 @@ class ReaderPostDetailViewModel @Inject constructor( val localRelatedPosts: RelatedPostsUiState? = null, val globalRelatedPosts: RelatedPostsUiState? = null ) : UiState() { - data class ExcerptFooterUiState(val visitPostExcerptFooterLinkText: UiString? = null, val postLink: String?) data class RelatedPostsUiState( 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 826367b23790..cb90fa3f433e 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,6 +1,5 @@ package org.wordpress.android.ui.reader.views -import androidx.core.text.HtmlCompat import dagger.Reusable import org.wordpress.android.R import org.wordpress.android.models.ReaderPost @@ -19,6 +18,7 @@ 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 @@ -132,17 +132,23 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( ).format(date) } + /** + * 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? { - if (post.shouldShowExcerpt()) return null - val content = post.text - if (content.isNullOrBlank()) return null - - val text = HtmlCompat.fromHtml( - content.replace(IMG_TAG_REGEX, ""), - HtmlCompat.FROM_HTML_MODE_LEGACY - ).toString().trim() - - if (text.isBlank()) return null + 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() From 7914ed7fa4cf894996b14ba667d1eed8d4d32ba2 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Fri, 27 Mar 2026 12:38:33 -0400 Subject: [PATCH 13/20] Fix blog section spacing between avatar and text Chain blog name and author vertically against the avatar, add marginStart with goneMarginStart so text aligns flush when there is no avatar. Co-Authored-By: Claude Opus 4.6 --- .../src/main/res/layout/reader_blog_section_view_new.xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/res/layout/reader_blog_section_view_new.xml b/WordPress/src/main/res/layout/reader_blog_section_view_new.xml index 747c351a64bd..76713b04f84d 100644 --- a/WordPress/src/main/res/layout/reader_blog_section_view_new.xml +++ b/WordPress/src/main/res/layout/reader_blog_section_view_new.xml @@ -10,7 +10,6 @@ Date: Fri, 27 Mar 2026 12:48:13 -0400 Subject: [PATCH 14/20] Hide author name when blank or same as blog name Co-Authored-By: Claude Opus 4.6 --- .../views/ReaderPostDetailsHeaderViewUiStateBuilder.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 cb90fa3f433e..1e358734d456 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 @@ -45,7 +45,10 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( return ReaderPostDetailsHeaderUiState( title = textTitle, - authorName = post.authorName, + authorName = post.authorName?.takeIf { + it.isNotBlank() && + !it.equals(post.blogName, ignoreCase = true) + }, tagItems = buildTagItems( post, onClicked = { From b334c3cfe61cf0aaa9ae010be68022e38e12cfa2 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Fri, 27 Mar 2026 12:52:35 -0400 Subject: [PATCH 15/20] Show blog description instead of post excerpt in detail header Co-Authored-By: Claude Opus 4.6 --- .../views/ReaderPostDetailsHeaderViewUiStateBuilder.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 1e358734d456..2ede69d34663 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 @@ -2,6 +2,7 @@ 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 @@ -34,6 +35,7 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( private val featuredImageUtils: FeaturedImageUtils, private val readerUtilsWrapper: ReaderUtilsWrapper, private val displayUtilsWrapper: DisplayUtilsWrapper, + private val readerBlogTableWrapper: ReaderBlogTableWrapper, ) { fun mapPostToUiState( post: ReaderPost, @@ -163,8 +165,12 @@ class ReaderPostDetailsHeaderViewUiStateBuilder @Inject constructor( } private fun buildExcerpt(post: ReaderPost): UiString? { - if (!post.hasExcerpt()) return null - return UiStringText(post.excerpt) + val description = readerBlogTableWrapper + .getBlogInfo(post.blogId) + ?.description + ?.takeIf { it.isNotBlank() } + ?: return null + return UiStringText(description) } private fun buildFeaturedImageUiState( From 4a2692acca518412a66c257b532de78efdaac71e Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Fri, 27 Mar 2026 13:18:52 -0400 Subject: [PATCH 16/20] Reorder Reader post detail header layout Move blog name to top, post title second, avatar with author and date below title, then featured image, blog description (up to 3 lines), and reading time. Co-Authored-By: Claude Opus 4.6 --- .../views/ReaderPostDetailHeaderView.kt | 9 +- .../layout/reader_blog_section_view_new.xml | 13 +-- .../layout/reader_post_detail_header_view.xml | 88 +++++++++++-------- 3 files changed, 66 insertions(+), 44 deletions(-) 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 3996dcb9d255..1d4b769c57fc 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 @@ -64,13 +64,16 @@ class ReaderPostDetailHeaderView @JvmOverloads constructor( updateTitle(uiState.title, readingPreferences, themeValues) - setAuthorAndDate(uiState.authorName, uiState.dateLine) - uiHelpers.setTextOrHide( - layoutBlogSection.blogSectionTextBlogName, + textBlogName, uiState.blogSectionUiState.blogName ) + setAuthorAndDate(uiState.authorName, uiState.dateLine) + + // Blog name is shown at the top; hide it in the blog section + layoutBlogSection.blogSectionTextBlogName.isVisible = false + updateFollowButton(uiState.followButtonUiState) updateAvatars(uiState.blogSectionUiState) diff --git a/WordPress/src/main/res/layout/reader_blog_section_view_new.xml b/WordPress/src/main/res/layout/reader_blog_section_view_new.xml index 76713b04f84d..e9edc561fe53 100644 --- a/WordPress/src/main/res/layout/reader_blog_section_view_new.xml +++ b/WordPress/src/main/res/layout/reader_blog_section_view_new.xml @@ -46,15 +46,17 @@ style="@style/ReaderTextView.PostDetail.BlogSection.Subtitle.Primary" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/margin_small" + android:layout_marginStart="@dimen/margin_large" android:includeFontPadding="false" android:maxLines="1" android:ellipsize="end" android:textAlignment="viewStart" - app:layout_constraintStart_toStartOf="@id/blog_section_text_blog_name" + app:layout_goneMarginStart="0dp" + app:layout_constraintStart_toEndOf="@id/blog_section_image_blog_avatar" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/blog_section_text_blog_name" - app:layout_constraintBottom_toBottomOf="@id/blog_section_image_blog_avatar" + app:layout_constraintTop_toTopOf="@id/blog_section_image_blog_avatar" + app:layout_constraintBottom_toTopOf="@id/blog_section_text_dateline" + app:layout_constraintVertical_chainStyle="packed" tools:text="Author Name" /> 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 60d7dea3b0b6..8a501243d34a 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,19 +7,17 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + + app:layout_constraintTop_toTopOf="parent" + tools:text="My Blog Name" /> + - - + - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/text_title" /> - + + + + + + @@ -143,7 +159,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/excerpt_barrier" + app:layout_constraintTop_toBottomOf="@id/reading_time_barrier" app:layout_constrainedWidth="true" /> From 6892a7241ba204ebec9acccc5ca251bd1f74dff8 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Fri, 27 Mar 2026 18:17:54 -0400 Subject: [PATCH 17/20] Fix blog section layout spacing and alignment Anchor dateline start directly to avatar so it stays positioned when author name is hidden. Add barrier below blog name and follow button so the post title clears both. Co-Authored-By: Claude Opus 4.6 --- .../src/main/res/layout/reader_blog_section_view_new.xml | 4 +++- .../main/res/layout/reader_post_detail_header_view.xml | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/WordPress/src/main/res/layout/reader_blog_section_view_new.xml b/WordPress/src/main/res/layout/reader_blog_section_view_new.xml index e9edc561fe53..628926952eee 100644 --- a/WordPress/src/main/res/layout/reader_blog_section_view_new.xml +++ b/WordPress/src/main/res/layout/reader_blog_section_view_new.xml @@ -68,7 +68,9 @@ android:layout_marginEnd="@dimen/margin_medium" android:includeFontPadding="false" android:maxLines="1" - app:layout_constraintStart_toStartOf="@id/blog_section_text_author" + android:layout_marginStart="@dimen/margin_large" + app:layout_goneMarginStart="0dp" + app:layout_constraintStart_toEndOf="@id/blog_section_image_blog_avatar" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/blog_section_text_author" app:layout_constraintBottom_toBottomOf="@id/blog_section_image_blog_avatar" 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 8a501243d34a..976aa5c2d02f 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 @@ -45,6 +45,13 @@ + + From 07aa8f838acb085900a1f1515eaa97be7fd1bde0 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Sat, 28 Mar 2026 07:04:24 -0400 Subject: [PATCH 18/20] Make subscribe button smaller and place it after blog name Co-Authored-By: Claude Opus 4.6 --- .../main/res/layout/reader_post_detail_header_view.xml | 9 +++++++-- WordPress/src/main/res/values-night/reader_styles.xml | 9 +++++++++ WordPress/src/main/res/values/reader_styles.xml | 9 +++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) 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 976aa5c2d02f..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 @@ -13,24 +13,29 @@ style="@style/ReaderTextView.PostDetail.BlogSection.Title" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/margin_medium" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/header_follow_button_container" app:layout_constraintTop_toTopOf="parent" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintHorizontal_bias="0" tools:text="My Blog Name" /> 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 + +