Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -20,6 +20,12 @@ internal sealed interface PageRsListEvent {

data class CopyPageUrl(val url: String) : PageRsListEvent

/** Opens the block-theme homepage in the Site Editor web view via WPWebViewActivity. */
data class OpenSiteEditor(
val url: String,
val useWpComCredentials: Boolean
) : PageRsListEvent

data class PromoteWithBlaze(
val site: SiteModel,
val page: PostModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,18 @@ internal sealed interface PageRsListItem {
) : PageRsListItem {
override val stableKey: String get() = "virtual:$kind"

enum class Kind { HOMEPAGE, POSTS_PAGE }
// HOMEPAGE / POSTS_PAGE wrap a real assigned static page; SITE_EDITOR is the block-theme
// homepage, which has no backing page and opens the Site Editor web view on tap.
enum class Kind { HOMEPAGE, POSTS_PAGE, SITE_EDITOR }
}
}

/**
* Sentinel [PageRsUiModel.remotePageId] for the synthetic SITE_EDITOR virtual row, which has no real
* page behind it. Real remote page ids are always positive, so a negative value can't collide.
*/
internal const val SITE_EDITOR_PAGE_ID = -1L

internal enum class PageRsDisplayState {
NORMAL,
FETCHING_WITH_DATA,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ internal const val MAX_INDENT_LEVEL = 3
* - When hierarchy is on, the pages identified by [pageOnFront] and [pageForPosts]
* (when set and present in [pages]) are prepended as Virtual rows and hidden from
* their normal sorted position.
* - When [showSiteEditorHomepage] is true (block-based theme + Site Editor MVP), a single
* SITE_EDITOR virtual row is prepended in place of the static HOMEPAGE row, and the
* [pageOnFront] page (if any) is still hidden from the tree. This mirrors the legacy list,
* where a block-theme homepage opens the Site Editor instead of the block editor.
* - When [applyHierarchy] is false, [pages] are wrapped as flat Real rows with
* indentLevel = 0 and no virtuals.
*/
internal fun buildRows(
pages: List<PageRsUiModel>,
applyHierarchy: Boolean,
pageOnFront: Long,
pageForPosts: Long
pageForPosts: Long,
showSiteEditorHomepage: Boolean = false
): List<PageRsListItem> {
if (!applyHierarchy) {
return pages.map { PageRsListItem.Real(it) }
Expand All @@ -29,12 +34,30 @@ internal fun buildRows(
val visible = pages.filterNot { it.remotePageId in hiddenIds }
val tree = flattenToTree(visible)
return buildList {
homepage?.let { add(PageRsListItem.Virtual(PageRsListItem.Virtual.Kind.HOMEPAGE, it)) }
if (showSiteEditorHomepage) {
add(siteEditorHomepageRow())
} else {
homepage?.let { add(PageRsListItem.Virtual(PageRsListItem.Virtual.Kind.HOMEPAGE, it)) }
}
postsPage?.let { add(PageRsListItem.Virtual(PageRsListItem.Virtual.Kind.POSTS_PAGE, it)) }
addAll(tree)
}
}

/**
* The synthetic SITE_EDITOR virtual row. It has no real page, so its visible text is rendered from
* string resources in the composable; the sentinel id lets the tap handler recognize it.
*/
private fun siteEditorHomepageRow() = PageRsListItem.Virtual(
kind = PageRsListItem.Virtual.Kind.SITE_EDITOR,
page = PageRsUiModel(
remotePageId = SITE_EDITOR_PAGE_ID,
title = "",
excerpt = "",
date = ""
)
)

internal fun flattenToTree(pages: List<PageRsUiModel>): List<PageRsListItem.Real> {
val byId = pages.associateBy { it.remotePageId }
val childrenByParent = pages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.wordpress.android.R
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.ui.ActivityLauncher
import org.wordpress.android.ui.PagePostCreationSourcesDetail.PAGE_FROM_PAGES_LIST
import org.wordpress.android.ui.WPWebViewActivity
import org.wordpress.android.ui.blaze.BlazeFlowSource
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper
Expand All @@ -30,6 +31,7 @@ import org.wordpress.android.util.extensions.clipboardManager
import org.wordpress.android.util.extensions.setContent
import org.wordpress.android.viewmodel.mlp.ModalLayoutPickerViewModel
import org.wordpress.android.viewmodel.observeEvent
import org.wordpress.android.viewmodel.wpwebview.WPWebViewSource
import javax.inject.Inject

@AndroidEntryPoint
Expand Down Expand Up @@ -109,13 +111,26 @@ class PagesRsListActivity : BaseAppCompatActivity() {
is PageRsListEvent.SharePage ->
ActivityLauncher.openShareIntent(this, event.url, event.title)
is PageRsListEvent.CopyPageUrl -> copyUrlToClipboard(event.url)
is PageRsListEvent.OpenSiteEditor -> openSiteEditor(event.url, event.useWpComCredentials)
is PageRsListEvent.PromoteWithBlaze ->
ActivityLauncher.openPromoteWithBlaze(this, event.page, BlazeFlowSource.PAGES_LIST)
is PageRsListEvent.ShowToast -> ToastUtils.showToast(this, event.messageResId)
is PageRsListEvent.Finish -> finish()
}
}

private fun openSiteEditor(url: String, useWpComCredentials: Boolean) {
if (useWpComCredentials) {
WPWebViewActivity.openUrlByUsingGlobalWPCOMCredentials(
this,
url,
WPWebViewSource.PAGE_LIST_EDIT_HOMEPAGE
)
} else {
WPWebViewActivity.openURL(this, url, WPWebViewSource.PAGE_LIST_EDIT_HOMEPAGE)
}
}

private fun copyUrlToClipboard(url: String) {
clipboardManager?.setPrimaryClip(ClipData.newPlainText(CLIPBOARD_URL_LABEL, url))
// Android 13+ shows its own confirmation UI when the clipboard changes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand All @@ -28,14 +29,19 @@ import org.greenrobot.eventbus.ThreadMode
import org.wordpress.android.R
import org.wordpress.android.analytics.AnalyticsTracker.Stat
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.generated.EditorThemeActionBuilder
import org.wordpress.android.fluxc.model.SiteHomepageSettings.ShowOnFront
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.model.post.PostStatus as FluxCPostStatus
import org.wordpress.android.fluxc.store.AccountStore
import org.wordpress.android.fluxc.store.EditorThemeStore
import org.wordpress.android.fluxc.store.EditorThemeStore.FetchEditorThemePayload
import org.wordpress.android.fluxc.store.EditorThemeStore.OnEditorThemeChanged
import org.wordpress.android.fluxc.store.PostStore
import org.wordpress.android.fluxc.store.PostStore.OnPostUploaded
import org.wordpress.android.ui.blaze.BlazeFeatureUtils
import org.wordpress.android.ui.mysite.SelectedSiteRepository
import org.wordpress.android.ui.pages.PageItem
import org.wordpress.android.ui.posts.AuthorFilterSelection
import org.wordpress.android.ui.postsrs.PostRsErrorUtils
import org.wordpress.android.ui.postsrs.SnackbarMessage
Expand All @@ -45,6 +51,7 @@ import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.NetworkUtilsWrapper
import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
import org.wordpress.android.util.config.SiteEditorMVPFeatureConfig
import org.wordpress.android.viewmodel.ResourceProvider
import rs.wordpress.cache.kotlin.ObservableMetadataCollection
import rs.wordpress.cache.kotlin.getObservablePostMetadataCollectionWithEditContext
Expand Down Expand Up @@ -74,6 +81,8 @@ internal class PagesRsListViewModel @Inject constructor(
private val accountStore: AccountStore,
private val appPrefsWrapper: AppPrefsWrapper,
private val analyticsTracker: AnalyticsTrackerWrapper,
private val editorThemeStore: EditorThemeStore,
private val siteEditorMVPFeatureConfig: SiteEditorMVPFeatureConfig,
) : ViewModel() {
private val _tabStates = MutableStateFlow<Map<PageRsListTab, PageTabUiState>>(emptyMap())
val tabStates: StateFlow<Map<PageRsListTab, PageTabUiState>> = _tabStates.asStateFlow()
Expand Down Expand Up @@ -133,12 +142,25 @@ internal class PagesRsListViewModel @Inject constructor(
)
val authorFilter: StateFlow<AuthorFilterSelection> = _authorFilter.asStateFlow()

// Whether the site's homepage uses a block-based theme. Seeded from the local cache and kept
// current via [onEditorThemeChanged]. When true (and the Site Editor MVP flag is on) the
// published tab shows a single SITE_EDITOR virtual row that opens the Site Editor web view.
private var isBlockBasedTheme = false

// Guards against a rapid double-tap on the SITE_EDITOR row launching two web views.
private var isLaunchingSiteEditor = false

init {
dispatcher.register(this)
if (site == null) {
_events.trySend(PageRsListEvent.ShowToast(R.string.blog_not_found))
_events.trySend(PageRsListEvent.Finish)
} else {
// Only the SITE_EDITOR virtual row needs the block-theme state, so skip the fetch
// entirely when the Site Editor MVP flag is off to avoid a request on every visit.
if (siteEditorMVPFeatureConfig.isEnabled()) {
refreshEditorTheme(site)
}
@OptIn(FlowPreview::class)
viewModelScope.launch {
_searchQuery
Expand Down Expand Up @@ -350,6 +372,31 @@ internal class PagesRsListViewModel @Inject constructor(
refreshAllTabs()
}

/** Seeds [isBlockBasedTheme] from the local cache and dispatches a remote refresh. */
private fun refreshEditorTheme(site: SiteModel) {
isBlockBasedTheme = editorThemeStore.getIsBlockBasedTheme(site)
dispatcher.dispatch(
EditorThemeActionBuilder.newFetchEditorThemeAction(
FetchEditorThemePayload(site, gssEnabled = true)
)
)
}

@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN_ORDERED)
fun onEditorThemeChanged(event: OnEditorThemeChanged) {
val site = this.site ?: return
val isBlockBased = event.editorTheme?.themeSupport?.isEditorThemeBlockBased()
if (site.id != event.siteId || isBlockBased == null || isBlockBased == isBlockBasedTheme) {
return
}
isBlockBasedTheme = isBlockBased
// Rebuild the published tab from cache so the SITE_EDITOR row appears/disappears.
if (collections.containsKey(PageRsListTab.PUBLISHED)) {
viewModelScope.launch { loadItemsForTab(PageRsListTab.PUBLISHED) }
}
}

/** Refreshes all currently initialized tabs. */
@MainThread
fun refreshAllTabs() {
Expand Down Expand Up @@ -453,18 +500,43 @@ internal class PagesRsListViewModel @Inject constructor(
val site = this.site
if (site == null || _isOpeningPage.value) return

if (remotePageId == SITE_EDITOR_PAGE_ID) {
openSiteEditor(site)
return
}

val page = _tabStates.value[tab]
?.pages
?.firstOrNull { it.remotePageId == remotePageId }
?.page

when {
tab == PageRsListTab.TRASHED || page?.isTrashed == true ->
_pendingConfirmation.value = PageRsListConfirmation.MoveToDraft(remotePageId)
checkNetwork() -> proceedOpenPage(site, remotePageId, page?.lastModified)
}
}

/** Opens the block-theme homepage in the Site Editor web view, matching the legacy pages list. */
private fun openSiteEditor(site: SiteModel) {
if (isLaunchingSiteEditor) return
isLaunchingSiteEditor = true
analyticsTracker.track(Stat.PAGES_EDIT_HOMEPAGE_ITEM_PRESSED, site)
val useWpComCredentials = site.isWPCom || site.isWPComAtomic || site.isPrivateWPComAtomic
_events.trySend(
PageRsListEvent.OpenSiteEditor(
url = PageItem.VirtualHomepage.Action.OpenSiteEditor.getUrl(site),
useWpComCredentials = useWpComCredentials
)
)
// The web view opens in a separate activity with no completion callback, so clear the
// guard after a short debounce: a rapid double-tap is dropped, but the row stays tappable
// when the user returns.
viewModelScope.launch {
delay(SITE_EDITOR_LAUNCH_DEBOUNCE_MS)
isLaunchingSiteEditor = false
}
}

private fun proceedOpenPage(site: SiteModel, remotePageId: Long, lastModified: String?) {
analyticsTracker.track(
Stat.PAGES_LIST_ITEM_SELECTED,
Expand Down Expand Up @@ -1111,11 +1183,13 @@ internal class PagesRsListViewModel @Inject constructor(
// and the construction-time [site] snapshot would pin stale pageOnFront /
// pageForPosts values onto the virtual rows.
val currentSite = selectedSiteRepository.getSelectedSite() ?: site
val showSiteEditorHomepage = siteEditorMVPFeatureConfig.isEnabled() && isBlockBasedTheme
val rows = buildRows(
pages = uiModels,
applyHierarchy = applyHierarchy,
pageOnFront = currentSite?.pageOnFront ?: 0L,
pageForPosts = currentSite?.pageForPosts ?: 0L
pageForPosts = currentSite?.pageForPosts ?: 0L,
showSiteEditorHomepage = showSiteEditorHomepage
).map { row -> row.withMenuActions(currentSite) }
updateTabUiState(tab) {
copy(pages = rows, isLoading = false, error = null, isAuthError = false)
Expand Down Expand Up @@ -1201,14 +1275,22 @@ internal class PagesRsListViewModel @Inject constructor(
} else {
true
}
val actions = computePageMenuActions(
status = page.status,
isHomepage = pageOnFront != 0L && page.remotePageId == pageOnFront,
isPostsPage = pageForPosts != 0L && page.remotePageId == pageForPosts,
hasPassword = page.hasPassword,
isBlazeEligibleSite = site != null && blazeFeatureUtils.isSiteBlazeEligible(site),
canManageHomepage = canManageHomepage
)
// The SITE_EDITOR virtual has no backing page, so it gets no overflow menu. Its synthetic
// page already has empty actions, so this leaves it unchanged below.
val isSiteEditor = this is PageRsListItem.Virtual &&
kind == PageRsListItem.Virtual.Kind.SITE_EDITOR
val actions = if (isSiteEditor) {
emptyList()
} else {
computePageMenuActions(
status = page.status,
isHomepage = pageOnFront != 0L && page.remotePageId == pageOnFront,
isPostsPage = pageForPosts != 0L && page.remotePageId == pageForPosts,
hasPassword = page.hasPassword,
isBlazeEligibleSite = site != null && blazeFeatureUtils.isSiteBlazeEligible(site),
canManageHomepage = canManageHomepage
)
}
if (actions == page.actions) return this
val updated = page.copy(actions = actions)
return when (this) {
Expand Down Expand Up @@ -1328,6 +1410,7 @@ internal class PagesRsListViewModel @Inject constructor(
companion object {
private const val PAGE_SIZE = 20
private const val SEARCH_DEBOUNCE_MS = 250L
private const val SITE_EDITOR_LAUNCH_DEBOUNCE_MS = 1000L
internal const val MIN_SEARCH_QUERY_LENGTH = 3
private const val THUMBNAIL_SIZE_DP = 64
private val ALL_STATUSES = PageRsListTab.entries.flatMap { it.statuses }.distinct()
Expand Down
Loading
Loading