Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
8772fc4
Start reader improvements feature branch
nbradbury Mar 25, 2026
0fe604f
Remove content-scanning fallbacks for Reader featured image
nbradbury Mar 25, 2026
ff77238
Move featured image above title and excerpt in Reader post cards
nbradbury Mar 25, 2026
5265ae1
Fix grammar in code comments
nbradbury Mar 26, 2026
89c4227
Merge remote-tracking branch 'origin/trunk' into issue/reader-feature…
nbradbury Mar 26, 2026
1269c94
Fit portrait featured images in Reader instead of cropping
nbradbury Mar 26, 2026
e15f7a2
Fix import ordering in ReaderPostNewViewHolder
nbradbury Mar 26, 2026
8bcb7ba
Merge remote-tracking branch 'origin/trunk' into issue/reader-portrai…
nbradbury Mar 27, 2026
63fc925
Fix import ordering and remove unnecessary @JvmOverloads in ImageManager
nbradbury Mar 27, 2026
22cedba
Merge remote-tracking branch 'origin/trunk' into issue/reader-portrai…
nbradbury Mar 27, 2026
bdd5c62
Redesign Reader post detail header for adaptive layout
nbradbury Mar 27, 2026
e709ea7
Simplify Reader post detail header and image transformation code
nbradbury Mar 27, 2026
db6cd2c
Fix crash in Reader post detail caused by unresolvable Material3 attr…
nbradbury Mar 27, 2026
a1eeb7f
Simplify Reader post detail: remove dead code and redundant scroll li…
nbradbury Mar 27, 2026
17a6afc
Use fastStripHtml for reading time and fix code quality issues
nbradbury Mar 27, 2026
7914ed7
Fix blog section spacing between avatar and text
nbradbury Mar 27, 2026
ebc1c46
Merge remote-tracking branch 'origin/trunk' into feature/CMM-2002-ada…
nbradbury Mar 27, 2026
80c7aa0
Hide author name when blank or same as blog name
nbradbury Mar 27, 2026
b334c3c
Show blog description instead of post excerpt in detail header
nbradbury Mar 27, 2026
4a2692a
Reorder Reader post detail header layout
nbradbury Mar 27, 2026
6892a72
Fix blog section layout spacing and alignment
nbradbury Mar 27, 2026
07aa8f8
Make subscribe button smaller and place it after blog name
nbradbury Mar 28, 2026
4691444
Fix featured image duplication after returning from comments
nbradbury Mar 28, 2026
94841fc
Simplify Reader post detail header code
nbradbury Mar 28, 2026
bcc4958
Fix detekt LongMethod violation in mapPostToUiState
nbradbury Mar 28, 2026
6cd6f18
Fix review issues: cache date formatter, move DB query off main threa…
nbradbury Mar 30, 2026
8a92cc8
Remove unused bg_rectangle_black_translucent_40 drawable
nbradbury Mar 30, 2026
c4b421c
Simplify Reader post detail header code
nbradbury Mar 30, 2026
19c9a5c
Fix detekt ReturnCount violation in buildReadingTime
nbradbury Mar 30, 2026
c3ad66f
Minor vm tweaks
nbradbury Mar 30, 2026
cc70a3e
Fix review issues: threading, thread safety, and data class cleanup
nbradbury Mar 30, 2026
af962f1
Fix detekt ReturnCount violation in buildReadingTime
nbradbury Mar 30, 2026
5e98ed9
Add "View original" button to Reader post detail header
nbradbury Mar 30, 2026
a3aee37
Move tags from post detail header to below post content
nbradbury Mar 30, 2026
4f5298a
Move likes/comments counts from header to below tags
nbradbury Mar 30, 2026
5df00c6
Remove comment count from interaction section below post content
nbradbury Mar 30, 2026
0178074
Fix featured image flickering in Reader post detail
nbradbury Mar 30, 2026
787a0fa
Remove time from publication date in Reader post detail
nbradbury Mar 30, 2026
013e033
Change "Visit site" to "View site" and open site in Reader
nbradbury Mar 30, 2026
0b170de
Show liker avatars inline before like count in Reader post detail
nbradbury Mar 30, 2026
87c62ef
Fetch liker data on initial post load for inline avatars
nbradbury Mar 30, 2026
61fa8e5
Remove inline avatar ImageViews, keep existing faces train
nbradbury Mar 30, 2026
1b88841
Remove duplicate like count text, use faces train only
nbradbury Mar 30, 2026
20a22fd
Restyle Subscribe button as unfilled pill in post detail header
nbradbury Mar 30, 2026
00a98b0
Remove unused ReaderImageScanner.getLargestImage()
nbradbury Mar 30, 2026
7cbc42b
Fix Subscribe pill clipping and soften border color
nbradbury Mar 30, 2026
5a6551e
Remove redundant tagItemsVisibility and showViewOriginal fields
nbradbury Mar 30, 2026
95b090d
Fix detekt and lint issues: unused imports, long method, unused drawable
nbradbury Mar 30, 2026
2739cf0
Show post excerpt instead of blog description in post detail header
nbradbury Mar 31, 2026
50a0d3d
Add thin divider below the post excerpt in detail header
nbradbury Mar 31, 2026
a31d8ed
Remove dead code and simplify Reader post detail branch
nbradbury Mar 31, 2026
d9ab107
Remove dead LikesClicked/CommentsClicked header actions and redundant…
nbradbury Mar 31, 2026
b0f8e7e
Fix detekt and lint issues: ReturnCount, TrimLambda, missing null ann…
nbradbury Mar 31, 2026
8696081
Always show header divider and darken excerpt text and divider color
nbradbury Mar 31, 2026
1792af8
Simplify title builder and move divider visibility to XML
nbradbury Mar 31, 2026
f17f4cf
Improve auto-generated excerpt detection with suffix check and whites…
nbradbury Mar 31, 2026
fdf27a8
Add KDoc comments to mapPostToUiState and buildExcerpt
nbradbury Mar 31, 2026
4a2a4e0
Fix detekt ReturnCount in isAutoGeneratedExcerpt
nbradbury Mar 31, 2026
c0a7f28
Fix flaky likers test by skipping onShowPost during init
nbradbury Mar 31, 2026
6d8c367
Address PR review feedback for post detail header
nbradbury Mar 31, 2026
c0e7831
Fix review feedback: tighter title, softer divider, simpler author card
nbradbury Mar 31, 2026
42c66c5
Fix divider color to match iOS and restore title lineHeight
nbradbury Mar 31, 2026
2eb76c8
Reduce post title line spacing and fix icon vertical alignment
nbradbury Mar 31, 2026
e68db5d
Fix detekt ReturnCount in buildAuthorClicked
nbradbury Mar 31, 2026
4bd5814
Removed lineSpacingMultiplier
nbradbury Apr 1, 2026
e4ec8fd
Extract author profile bottom sheet to Fragment with XML layout
nbradbury Apr 1, 2026
b994bce
Merge trunk into feature/CMM-2002-adaptive-post-details
nbradbury Apr 1, 2026
9755c64
Cancel pending excerpt truncation check before posting new one
nbradbury Apr 1, 2026
bb0e2ac
Fix Visit/View site string resources for correct scoping
nbradbury Apr 1, 2026
bb6263e
Fix review issues: Paint thread safety, data class lambdas, View Binding
nbradbury Apr 1, 2026
ec5c674
Simplify bottom sheet R import and ViewModel null handling
nbradbury Apr 1, 2026
62b9e05
Fix tiny text in author profile bottom sheet
nbradbury Apr 1, 2026
c85a766
Migrate ReaderAuthorProfileBottomSheet to Compose
nbradbury Apr 1, 2026
ca6dd8c
Revert Compose migration for author profile bottom sheet
nbradbury Apr 1, 2026
4b91df6
Fix review issues: consistent feature gating, divider, crash safety
nbradbury Apr 1, 2026
828bf2c
Fix "(Untitled)" blog name in post list and post detail
nbradbury Apr 1, 2026
a96476c
Fix "(Untitled)" blog name in post list and post detail, p2
nbradbury Apr 1, 2026
16cafc1
Simplify author row linking and fix View original alignment
nbradbury Apr 1, 2026
2839b84
Changed "View site" to "View original"
nbradbury Apr 1, 2026
62b2a06
Make sure author profile shows a url when blog name is missing
nbradbury Apr 1, 2026
d625742
Use filled Subscribe button
nbradbury Apr 1, 2026
ad65b53
Fixed image resize
nbradbury Apr 1, 2026
931e177
Fall back to "featured_image" for FP posts
nbradbury Apr 1, 2026
3ce01cd
Reverted adjustViewBounds change
nbradbury Apr 1, 2026
6be1f42
Align Reader post list horizontal margins with post detail
nbradbury Apr 1, 2026
cf5618a
Merge branch 'feature/CMM-2002-adaptive-post-details' into issue/CMM-…
nbradbury Apr 1, 2026
4ac8b9b
Merge remote-tracking branch 'origin/trunk' into issue/CMM-1997-reade…
nbradbury Apr 1, 2026
edf8982
Scope Reader photo viewer swiping to gallery images
nbradbury Apr 2, 2026
5c953ce
Fix gallery detection for WordPress-resized image URLs
nbradbury Apr 3, 2026
3aefd40
Fix duplicate gallery image and anchor-wrapped image handling
nbradbury Apr 3, 2026
28ce716
Fix image detection for non-standard formats like .bmp
nbradbury Apr 3, 2026
60b43bd
Merge remote-tracking branch 'origin/trunk' into issue/CMM-1994-reade…
nbradbury Apr 3, 2026
8efe641
Consolidate showReaderPhotoViewer overloads and remove unused suppress
nbradbury Apr 3, 2026
8dbcdd4
Fix import ordering and add clarifying comments
nbradbury Apr 3, 2026
691e57d
Fix detekt and checkstyle issues in gallery code
nbradbury Apr 3, 2026
be0cf43
Simplify gallery detection JS using Element.closest()
nbradbury Apr 3, 2026
81542a6
Extract gallery detection logic into ReaderGalleryDetector
nbradbury Apr 3, 2026
1dd9698
Add KDoc to buildDetectionJs and parseResult
nbradbury Apr 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -436,15 +436,17 @@ object ReaderActivityLauncher {
}

@JvmStatic
@JvmOverloads
@Suppress("LongParameterList")
fun showReaderPhotoViewer(
context: Context,
imageUrl: String,
content: String,
content: String?,
sourceView: View?,
imageOptions: EnumSet<PhotoViewerOption>,
startX: Int,
startY: Int
startY: Int,
galleryImageUrls: ArrayList<String>? = null
) {
if (TextUtils.isEmpty(imageUrl)) {
return
Expand All @@ -460,6 +462,12 @@ object ReaderActivityLauncher {
if (!TextUtils.isEmpty(content)) {
intent.putExtra(ReaderConstants.ARG_CONTENT, content)
}
if (!galleryImageUrls.isNullOrEmpty()) {
intent.putStringArrayListExtra(
ReaderConstants.ARG_GALLERY_IMAGE_URLS,
galleryImageUrls
)
}

if (context is Activity && sourceView != null) {
val options =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class ReaderConstants {
static final String ARG_IS_GALLERY = "is_gallery";
static final String ARG_POST_LIST_TYPE = "post_list_type";
static final String ARG_CONTENT = "content";
static final String ARG_GALLERY_IMAGE_URLS = "gallery_image_urls";
static final String ARG_IS_SINGLE_POST = "is_single_post";
static final String ARG_IS_RELATED_POST = "is_related_post";
static final String ARG_SEARCH_QUERY = "search_query";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package org.wordpress.android.ui.reader

import android.webkit.WebView
import org.json.JSONArray
import org.json.JSONException
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.AppLog.T

/**
* Detects whether a tapped image belongs to a gallery in a Reader
* post WebView, and returns the gallery's image URLs if so.
*/
class ReaderGalleryDetector {
/**
* Evaluates JS in [webView] to determine whether the image at
* [imageUrl] is inside a gallery. Calls [callback] with the
* gallery's image URLs, or null if the image is not in a gallery.
*/
fun detectGallery(
webView: WebView,
imageUrl: String,
callback: (ArrayList<String>?) -> Unit
) {
val js = buildDetectionJs(imageUrl)
webView.evaluateJavascript(js) { result ->
callback(parseResult(result))
}
}

/**
* Builds a JavaScript snippet that finds the tapped image in the
* DOM by matching its URL pathname, walks up to the nearest
* gallery container, and returns a JSON array of all image URLs
* in that gallery. The [imageUrl] is escaped to prevent
* injection before being interpolated into the script.
*/
private fun buildDetectionJs(imageUrl: String): String {
val safeUrl = imageUrl
.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\n", "")
.replace("\r", "")
return """
(function() {
try {
var tp = new URL('$safeUrl').pathname;
var strip = function(p) {
return p.replace(/-\d+x\d+(\.[^.]+)${'$'}/, '${'$'}1');
};
var sel = '.wp-block-gallery,.tiled-gallery,'
+ '.gallery,.blocks-gallery-grid';
var imgs = document.querySelectorAll('img');
for (var i = 0; i < imgs.length; i++) {
try {
var p = new URL(imgs[i].src).pathname;
if (p === tp || strip(p) === strip(tp)) {
var g = imgs[i].closest(sel);
if (!g) return null;
var urls = [];
var gi = g.querySelectorAll('img');
for (var j = 0; j < gi.length; j++) {
if (gi[j].src
&& gi[j].src.startsWith('http'))
urls.push(gi[j].src);
}
return JSON.stringify(urls);
}
} catch(e) {}
}
return null;
} catch(e) { return null; }
})()
""".trimIndent()
}

/**
* Parses the JSON string [result] returned by the gallery
* detection JavaScript. Returns the list of image URLs when
* the gallery contains more than one image, or null if the
* result is empty, not valid JSON, or a single-image gallery.
*/
private fun parseResult(result: String?): ArrayList<String>? {
if (result.isNullOrEmpty() || result == "null") return null
return try {
val json = if (result.startsWith("\"") && result.endsWith("\"")) {
result
.substring(1, result.length - 1)
.replace("\\\"", "\"")
.replace("\\\\", "\\")
} else {
result
}
val array = JSONArray(json)
// Single-image galleries fall back to all-images behavior
// so the viewer shows more context rather than a lone image.
if (array.length() <= 1) {
null
} else {
val urls = ArrayList<String>(array.length())
for (i in 0 until array.length()) {
urls.add(array.getString(i))
}
urls
}
} catch (e: JSONException) {
AppLog.e(T.READER, "Failed to parse gallery URLs: $e")
null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@
import org.wordpress.android.ui.reader.views.ReaderPhotoView.PhotoViewListener;
import org.wordpress.android.util.AniUtils;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.UrlUtils;
import org.wordpress.android.widgets.WPViewPager;
import org.wordpress.android.widgets.WPViewPagerTransformer;
import org.wordpress.android.widgets.WPViewPagerTransformer.TransformType;

import java.util.ArrayList;

/**
* Full-screen photo viewer - uses a ViewPager to enable scrolling between images in a blog
* post, but also supports viewing a single image
Expand All @@ -39,6 +42,7 @@
private boolean mIsPrivate;
private boolean mIsGallery;
private String mContent;
private ArrayList<String> mGalleryImageUrls;

Check notice

Code scanning / Android Lint

Nullable/NonNull annotation missing on field Note

Missing null annotation
private WPViewPager mViewPager;
private PhotoPagerAdapter mAdapter;
private TextView mTxtTitle;
Expand All @@ -63,11 +67,17 @@
mIsPrivate = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_PRIVATE);
mIsGallery = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_GALLERY);
mContent = savedInstanceState.getString(ReaderConstants.ARG_CONTENT);
mGalleryImageUrls = savedInstanceState.getStringArrayList(
ReaderConstants.ARG_GALLERY_IMAGE_URLS
);
} else if (getIntent() != null) {
mInitialImageUrl = getIntent().getStringExtra(ReaderConstants.ARG_IMAGE_URL);
mIsPrivate = getIntent().getBooleanExtra(ReaderConstants.ARG_IS_PRIVATE, false);
mIsGallery = getIntent().getBooleanExtra(ReaderConstants.ARG_IS_GALLERY, false);
mContent = getIntent().getStringExtra(ReaderConstants.ARG_CONTENT);
mGalleryImageUrls = getIntent().getStringArrayListExtra(
ReaderConstants.ARG_GALLERY_IMAGE_URLS
);
}

mToolbar = findViewById(R.id.toolbar);
Expand All @@ -93,24 +103,79 @@
}

private void loadImageList() {
// content will be empty when viewing a single image, otherwise content is HTML
// so parse images from it
final ReaderImageList imageList;
if (TextUtils.isEmpty(mContent)) {
if (mGalleryImageUrls != null && !mGalleryImageUrls.isEmpty()) {
// gallery-scoped: only show images from the tapped gallery
imageList = new ReaderImageList(mIsPrivate);
for (String url : mGalleryImageUrls) {
imageList.addImageUrl(url);
}
// The initial URL may be the full-size variant while the
// gallery contains a resized version (e.g. image.jpg vs
// image-800x600.jpg). Find the matching gallery URL so the
// pager starts on the right image without adding a duplicate.
if (!TextUtils.isEmpty(mInitialImageUrl)
&& !imageList.hasImageUrl(mInitialImageUrl)) {
String match = findSizeSuffixMatch(
mInitialImageUrl, mGalleryImageUrls
);
if (match != null) {
mInitialImageUrl = match;
}
}
} else if (TextUtils.isEmpty(mContent)) {
// content will be empty when viewing a single image
imageList = new ReaderImageList(mIsPrivate);
} else {
int minImageWidth = mIsGallery ? ReaderConstants.MIN_GALLERY_IMAGE_WIDTH : 0;
imageList = new ReaderImageScanner(mContent, mIsPrivate).getImageList(0, minImageWidth);
// parse all images from post HTML
int minImageWidth = mIsGallery
? ReaderConstants.MIN_GALLERY_IMAGE_WIDTH : 0;
imageList = new ReaderImageScanner(mContent, mIsPrivate)
.getImageList(0, minImageWidth);
}

// make sure initial image is in the list
if (!TextUtils.isEmpty(mInitialImageUrl) && !imageList.hasImageUrl(mInitialImageUrl)) {
if (!TextUtils.isEmpty(mInitialImageUrl)
&& !imageList.hasImageUrl(mInitialImageUrl)) {
imageList.addImageUrl(0, mInitialImageUrl);
}

getAdapter().setImageList(imageList, mInitialImageUrl);
}

/**
* Finds a gallery URL that matches the given URL after stripping
* WordPress size suffixes (e.g. -800x600) from pathnames.
*/
@Nullable
private static String findSizeSuffixMatch(
@NonNull String targetUrl,
@NonNull ArrayList<String> galleryUrls
) {
String basePath = stripSizeSuffix(
UrlUtils.removeQuery(targetUrl)
);
for (String galleryUrl : galleryUrls) {
String galleryBase = stripSizeSuffix(
UrlUtils.removeQuery(galleryUrl)
);
if (basePath.equals(galleryBase)) {
return galleryUrl;
}
}
return null;
}

/**
* Strips WordPress size suffixes (e.g. -800x600) from a URL.
* Keep in sync with the JS counterpart in
* {@code ReaderPostDetailFragment.buildGalleryDetectionJs}.
*/
@NonNull
private static String stripSizeSuffix(@NonNull String url) {
return url.replaceAll("-\\d+x\\d+(\\.[^.]+)$", "$1");
}

private void showToolbar() {
if (!isFinishing()) {
mFadeHandler.removeCallbacks(mFadeOutRunnable);
Expand Down Expand Up @@ -191,6 +256,12 @@
outState.putBoolean(ReaderConstants.ARG_IS_PRIVATE, mIsPrivate);
outState.putBoolean(ReaderConstants.ARG_IS_GALLERY, mIsGallery);
outState.putString(ReaderConstants.ARG_CONTENT, mContent);
if (mGalleryImageUrls != null) {
outState.putStringArrayList(
ReaderConstants.ARG_GALLERY_IMAGE_URLS,
mGalleryImageUrls
);
}

super.onSaveInstanceState(outState);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1459,6 +1459,52 @@ class ReaderPostDetailFragment : ViewPagerFragment(),
return true
}

private val galleryDetector = ReaderGalleryDetector()

private fun showPhotoViewerWithGalleryCheck(
imageUrl: String,
sourceView: View,
startX: Int,
startY: Int
) {
if (!isAdded || imageUrl.isEmpty() || !imageUrl.startsWith("http")) {
return
}
galleryDetector.detectGallery(readerWebView, imageUrl) { galleryUrls ->
if (!isAdded) return@detectGallery
if (galleryUrls != null) {
showPhotoViewerForGallery(imageUrl, sourceView, startX, startY, galleryUrls)
} else {
showPhotoViewer(imageUrl, sourceView, startX, startY)
}
}
}

private fun showPhotoViewerForGallery(
imageUrl: String,
sourceView: View,
startX: Int,
startY: Int,
galleryUrls: ArrayList<String>
) {
val isPrivatePost = viewModel.post?.isPrivate == true
val options = EnumSet.noneOf(PhotoViewerOption::class.java)
if (isPrivatePost) {
options.add(PhotoViewerOption.IS_PRIVATE_IMAGE)
}

ReaderActivityLauncher.showReaderPhotoViewer(
requireActivity(),
imageUrl,
null,
sourceView,
options,
startX,
startY,
galleryUrls
)
}

/*
* post slugs resolution to IDs has completed
*/
Expand Down Expand Up @@ -1890,7 +1936,8 @@ class ReaderPostDetailFragment : ViewPagerFragment(),

override fun onImageUrlClick(imageUrl: String, view: View, x: Int, y: Int): Boolean {
readerTracker.track(AnalyticsTracker.Stat.READER_ARTICLE_IMAGE_TAPPED)
return showPhotoViewer(imageUrl, view, x, y)
showPhotoViewerWithGalleryCheck(imageUrl, view, x, y)
return true
}

override fun onFileDownloadClick(fileUrl: String?): Boolean {
Expand Down
Loading
Loading