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
@@ -0,0 +1,115 @@
package org.wordpress.android.ui.pagesrs

import kotlinx.coroutines.CancellationException
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.generated.SiteActionBuilder
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.rest.wpapi.rs.WpApiClientProvider
import rs.wordpress.api.kotlin.WpRequestResult
import uniffi.wp_api.SiteSettingsUpdateParams
import javax.inject.Inject

/**
* Updates a site's homepage settings (page shown on front / page for posts) through the
* wordpress-rs `/wp/v2/settings` endpoint, so it works on both WP.com and self-hosted
* application-password sites — unlike FluxC's SiteOptionsStore, which is WP.com-only.
*
* Mirrors SiteOptionsStore's semantics: the update is rejected unless the site shows a
* static page on front, and assigning a page to one slot clears it from the other (core's
* settings endpoint does not auto-clear). Current values are read live from the server
* rather than from [SiteModel], whose homepage fields aren't reliably populated for
* self-hosted sites. On success the shared [SiteModel] is updated in place and
* re-dispatched so the rest of the app (virtual rows, My Site, the legacy list) stays
* in sync.
*/
internal class PageRsHomepageSettings @Inject constructor(
private val wpApiClientProvider: WpApiClientProvider,
private val dispatcher: Dispatcher,
) {
sealed interface Result {
data object Success : Result

/** The site's theme shows latest posts on front, so homepage pages can't be set. */
data object StaticHomepageDisabled : Result

data class Error(val message: String?) : Result
}

suspend fun setHomepage(site: SiteModel, pageId: Long): Result =
update(site, HomepageTarget.PAGE_ON_FRONT, pageId)

suspend fun setPostsPage(site: SiteModel, pageId: Long): Result =
update(site, HomepageTarget.PAGE_FOR_POSTS, pageId)

@Suppress("TooGenericExceptionCaught", "ReturnCount")
private suspend fun update(site: SiteModel, target: HomepageTarget, pageId: Long): Result {
try {
val client = wpApiClientProvider.getWpApiClient(site)

val current = when (
val response = client.request { it.siteSettings().retrieveWithEditContext() }
) {
is WpRequestResult.Success -> response.response.data
else -> return response.toError()
}

val params = computeHomepageUpdateParams(
showOnFront = current.showOnFront,
currentPageOnFront = current.pageOnFront.toLong(),
currentPageForPosts = current.pageForPosts.toLong(),
target = target,
pageId = pageId
) ?: return Result.StaticHomepageDisabled

val updated = when (
val response = client.request { it.siteSettings().update(params) }
) {
is WpRequestResult.Success -> response.response.data
else -> return response.toError()
}

site.pageOnFront = updated.pageOnFront.toLong()
site.pageForPosts = updated.pageForPosts.toLong()
site.showOnFront = updated.showOnFront
dispatcher.dispatch(SiteActionBuilder.newUpdateSiteAction(site))
return Result.Success
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
return Result.Error(e.message)
}
}

private fun WpRequestResult<*>.toError() = Result.Error(
(this as? WpRequestResult.WpError<*>)?.errorMessage
)
}

internal enum class HomepageTarget { PAGE_ON_FRONT, PAGE_FOR_POSTS }

/**
* Builds the settings update for assigning [pageId] to [target], or returns null when the
* site doesn't show a static page on front. Both homepage fields are always sent: the page
* being assigned, and the other slot — cleared when it currently holds the same page.
*/
internal fun computeHomepageUpdateParams(
showOnFront: String,
currentPageOnFront: Long,
currentPageForPosts: Long,
target: HomepageTarget,
pageId: Long
): SiteSettingsUpdateParams? {
if (showOnFront != SHOW_ON_FRONT_PAGE) return null
return when (target) {
HomepageTarget.PAGE_ON_FRONT -> SiteSettingsUpdateParams(
pageOnFront = pageId.toULong(),
pageForPosts = (if (currentPageForPosts == pageId) 0L else currentPageForPosts).toULong()
)
HomepageTarget.PAGE_FOR_POSTS -> SiteSettingsUpdateParams(
pageOnFront = (if (currentPageOnFront == pageId) 0L else currentPageOnFront).toULong(),
pageForPosts = pageId.toULong()
)
}
}

private const val SHOW_ON_FRONT_PAGE = "page"
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ internal sealed interface PageRsListEvent {

data object CreateNewPage : PageRsListEvent

data class ViewPage(val url: String) : PageRsListEvent

data class SharePage(
val url: String,
val title: String
) : PageRsListEvent

data class CopyPageUrl(val url: String) : PageRsListEvent

data class PromoteWithBlaze(
val site: SiteModel,
val page: PostModel
) : PageRsListEvent

data class ShowToast(
val messageResId: Int
) : PageRsListEvent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.wordpress.android.ui.pagesrs

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.wordpress.android.R
import org.wordpress.android.ui.postsrs.PostRsDateFormatter
Expand All @@ -11,6 +12,31 @@ import uniffi.wp_api.PostStatus
import uniffi.wp_mobile.FullEntityAnyPostWithEditContext
import uniffi.wp_mobile.PostItemState

/** A destructive or status-changing action awaiting user confirmation in a dialog. */
internal sealed interface PageRsListConfirmation {
data class Trash(val pageId: Long) : PageRsListConfirmation
data class Delete(val pageId: Long, val pageTitle: String) : PageRsListConfirmation
data class MoveToDraft(val pageId: Long) : PageRsListConfirmation
}

internal data class PageRsConfirmationDialogState(
val pending: PageRsListConfirmation? = null,
val onConfirm: () -> Unit = {},
val onDismiss: () -> Unit = {}
)

/** State for the "Set Parent" bottom sheet. [candidates] excludes the page and its descendants. */
internal data class PageRsParentPickerState(
val pageId: Long,
val currentParentId: Long,
val candidates: List<PageRsParentCandidate>
)

internal data class PageRsParentCandidate(
val id: Long,
val title: String
)

internal data class PageTabUiState(
val pages: List<PageRsListItem> = emptyList(),
val isLoading: Boolean = false,
Expand Down Expand Up @@ -58,14 +84,41 @@ internal data class PageRsUiModel(
val excerpt: String,
val date: String,
val lastModified: String = "",
val link: String = "",
val hasPassword: Boolean = false,
val status: PostStatus? = null,
@StringRes val statusLabelResId: Int = 0,
val authorId: Long = 0L,
val authorDisplayName: String? = null,
val isTrashed: Boolean = false,
val actions: List<PageRsMenuAction> = emptyList(),
val badges: List<Int> = emptyList(),
val displayState: PageRsDisplayState = PageRsDisplayState.NORMAL
)

internal enum class PageRsMenuAction(
@StringRes val labelResId: Int,
@DrawableRes val iconResId: Int,
val isDestructive: Boolean = false
) {
VIEW(R.string.pages_view, R.drawable.gb_ic_external),
SET_PARENT(R.string.set_parent, R.drawable.gb_ic_pages_set_as_parent),
SET_AS_HOMEPAGE(R.string.pages_set_as_homepage, R.drawable.gb_ic_home_page_24dp),
SET_AS_POSTS_PAGE(R.string.pages_set_as_posts_page, R.drawable.ic_posts_white_24dp),
PUBLISH_NOW(R.string.pages_publish_now, R.drawable.gb_ic_globe),
MOVE_TO_DRAFT(R.string.pages_move_to_draft, R.drawable.gb_ic_move_to),
DUPLICATE(R.string.button_copy, R.drawable.gb_ic_copy),
SHARE(R.string.button_share, R.drawable.gb_ic_share),
COPY_URL(R.string.page_rs_copy_url, R.drawable.ic_attachment_link),
BLAZE(R.string.pages_promote_with_blaze, R.drawable.ic_blaze_flame_24dp),
TRASH(R.string.pages_move_to_trash, R.drawable.gb_ic_trash, isDestructive = true),
DELETE_PERMANENTLY(
R.string.pages_delete_permanently,
R.drawable.gb_ic_trash,
isDestructive = true
),
}

internal fun PostItemState.toPageUiModel(
pageId: Long,
showStatus: Boolean = false
Expand Down Expand Up @@ -111,6 +164,9 @@ private fun FullEntityAnyPostWithEditContext.toPageUiModel(
).let { HtmlUtils.fastStripHtml(it).trim() },
date = PostRsDateFormatter.format(page.dateGmt, page.status),
lastModified = DateTimeUtils.iso8601UTCFromDate(page.modifiedGmt),
link = page.link,
hasPassword = !page.password.isNullOrEmpty(),
status = page.status,
statusLabelResId = if (showStatus) page.status.toLabel() else 0,
authorId = page.author ?: 0L,
isTrashed = page.status is PostStatus.Trash,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.wordpress.android.ui.pagesrs

import uniffi.wp_api.PostStatus

/**
* Computes the overflow-menu actions for a page row, keyed off the page's own status (not the
* tab) so search results — which mix statuses — get the right menu. Mirrors the visibility
* rules of the legacy pages list (CreatePageListItemActionsUseCase): the homepage cannot be
* trashed or drafted, "Set as Homepage / Posts Page" only appear when [canManageHomepage]
* (for WP.com sites that means manage-options capability plus a static page on front; for
* self-hosted sites those fields aren't synced, so the actions are offered and verified at
* execution time), and Blaze requires an eligible site and a non-password-protected
* published page.
*/
@Suppress("LongParameterList")
internal fun computePageMenuActions(
status: PostStatus?,
isHomepage: Boolean,
isPostsPage: Boolean,
hasPassword: Boolean,
isBlazeEligibleSite: Boolean,
canManageHomepage: Boolean
): List<PageRsMenuAction> = when (status) {
is PostStatus.Publish, is PostStatus.Private -> buildList {
add(PageRsMenuAction.VIEW)
add(PageRsMenuAction.SET_PARENT)
if (canManageHomepage && !isHomepage) add(PageRsMenuAction.SET_AS_HOMEPAGE)
if (canManageHomepage && !isPostsPage) add(PageRsMenuAction.SET_AS_POSTS_PAGE)
if (!isHomepage) add(PageRsMenuAction.MOVE_TO_DRAFT)
add(PageRsMenuAction.DUPLICATE)
add(PageRsMenuAction.SHARE)
add(PageRsMenuAction.COPY_URL)
if (isBlazeEligibleSite && !hasPassword && status is PostStatus.Publish) {
add(PageRsMenuAction.BLAZE)
}
if (!isHomepage) add(PageRsMenuAction.TRASH)
}
is PostStatus.Draft, is PostStatus.Pending -> buildList {
add(PageRsMenuAction.VIEW)
add(PageRsMenuAction.SET_PARENT)
add(PageRsMenuAction.PUBLISH_NOW)
add(PageRsMenuAction.DUPLICATE)
add(PageRsMenuAction.SHARE)
add(PageRsMenuAction.COPY_URL)
add(PageRsMenuAction.TRASH)
}
is PostStatus.Future -> buildList {
add(PageRsMenuAction.VIEW)
add(PageRsMenuAction.SET_PARENT)
add(PageRsMenuAction.SHARE)
add(PageRsMenuAction.COPY_URL)
add(PageRsMenuAction.MOVE_TO_DRAFT)
add(PageRsMenuAction.TRASH)
}
is PostStatus.Trash -> listOf(
PageRsMenuAction.MOVE_TO_DRAFT,
PageRsMenuAction.DELETE_PERMANENTLY
)
is PostStatus.Any, is PostStatus.Custom, null -> emptyList()
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.wordpress.android.ui.pagesrs

import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.viewModels
import androidx.compose.runtime.collectAsState
Expand All @@ -12,16 +14,19 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
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.blaze.BlazeFlowSource
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper
import org.wordpress.android.ui.main.BaseAppCompatActivity
import org.wordpress.android.ui.mlp.ModalLayoutPickerFragment
import org.wordpress.android.ui.mlp.ModalLayoutPickerFragment.Companion.MODAL_LAYOUT_PICKER_TAG
import org.wordpress.android.ui.pagesrs.screens.PagesRsListScreen
import org.wordpress.android.util.ToastUtils
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
Expand All @@ -48,6 +53,8 @@ class PagesRsListActivity : BaseAppCompatActivity() {
val isOpeningPage by viewModel.isOpeningPage.collectAsState()
val searchQuery by viewModel.searchQuery.collectAsState()
val authorFilter by viewModel.authorFilter.collectAsState()
val pendingConfirmation by viewModel.pendingConfirmation.collectAsState()
val parentPicker by viewModel.parentPicker.collectAsState()
AppThemeM3 {
PagesRsListScreen(
tabStates = tabStates,
Expand All @@ -57,6 +64,12 @@ class PagesRsListActivity : BaseAppCompatActivity() {
authorFilter = authorFilter,
isAuthorFilterSupported = viewModel.isAuthorFilterSupported,
avatarUrl = viewModel.avatarUrl,
confirmationDialog = PageRsConfirmationDialogState(
pending = pendingConfirmation,
onConfirm = viewModel::onConfirmPendingAction,
onDismiss = viewModel::onDismissPendingAction
),
parentPicker = parentPicker,
snackbarMessages = viewModel.snackbarMessages,
onSearchOpen = viewModel::onSearchOpen,
onSearchQueryChanged = viewModel::onSearchQueryChanged,
Expand All @@ -68,6 +81,9 @@ class PagesRsListActivity : BaseAppCompatActivity() {
onLoadMore = viewModel::loadMorePages,
onNavigateBack = { onBackPressedDispatcher.onBackPressed() },
onPageClick = viewModel::openPage,
onPageMenuAction = viewModel::onPageMenuAction,
onParentSelected = viewModel::onParentSelected,
onParentPickerDismissed = viewModel::onParentPickerDismissed,
onAddNewPage = viewModel::onAddNewPage
)
}
Expand All @@ -87,11 +103,25 @@ class PagesRsListActivity : BaseAppCompatActivity() {
is PageRsListEvent.EditPage ->
ActivityLauncher.editPostOrPageForResult(this, event.site, event.page)
is PageRsListEvent.CreateNewPage -> startCreatePageFlow()
is PageRsListEvent.ViewPage -> ActivityLauncher.openUrlExternal(this, event.url)
is PageRsListEvent.SharePage ->
ActivityLauncher.openShareIntent(this, event.url, event.title)
is PageRsListEvent.CopyPageUrl -> copyUrlToClipboard(event.url)
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 copyUrlToClipboard(url: String) {
clipboardManager?.setPrimaryClip(ClipData.newPlainText(CLIPBOARD_URL_LABEL, url))
// Android 13+ shows its own confirmation UI when the clipboard changes.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
ToastUtils.showToast(this, R.string.media_edit_copy_url_toast)
}
}

private fun startCreatePageFlow() {
if (mlpViewModel.canShowModalLayoutPicker() &&
jetpackFeatureRemovalPhaseHelper.shouldShowTemplateSelectionInPages()
Expand Down Expand Up @@ -126,6 +156,8 @@ class PagesRsListActivity : BaseAppCompatActivity() {
}

companion object {
private const val CLIPBOARD_URL_LABEL = "Page URL"

fun createIntent(context: Context) = Intent(context, PagesRsListActivity::class.java)
}
}
Loading
Loading