diff --git a/.circleci/config.yml b/.circleci/config.yml index 32f6b9ffcae8..4fb8accf2ff6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,7 +21,7 @@ commands: steps: - run: name: Setup gradle.properties - command: cp gradle.properties-example gradle.properties && cp libs/utils/WordPressUtils/gradle.properties-example libs/utils/WordPressUtils/gradle.properties + command: cp gradle.properties-example gradle.properties update-gradle-memory: parameters: jvmargs: @@ -130,11 +130,6 @@ jobs: environment: SUPPRESS_GUTENBERG_MOBILE_JS_BUNDLE_BUILD: 1 command: SUPPRESS_GUTENBERG_MOBILE_JS_BUNDLE_BUILD=1 ./gradlew testVanillaRelease --stacktrace --no-daemon - - run: - name: Test WordPressUtils - environment: - SUPPRESS_GUTENBERG_MOBILE_JS_BUNDLE_BUILD: 1 - command: cd libs/utils && ./gradlew --stacktrace testReleaseUnitTest - run: name: Test WordPressProcessors environment: @@ -606,12 +601,6 @@ workflows: filters: branches: ignore: /pull\/[0-9]+/ - - WordPressUtils Connected Tests: - requires: - - gutenberg-bundle-build - filters: - branches: - ignore: /pull\/[0-9]+/ - Connected Tests: requires: - gutenberg-bundle-build diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 0176053041d0..563207e41dbb 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,7 +1,11 @@ 16.4 ----- * [*] My Site: Fixes crash on rotation while editing site title [https://github.com/wordpress-mobile/WordPress-Android/pull/13505] - +* [**] Posts List: Adds duplicate post functionality [https://github.com/wordpress-mobile/WordPress-Android/pull/13521] +* [*] Block Editor: Fix Gallery block uploads when the editor is closed [https://github.com/wordpress-mobile/WordPress-Android/pull/13570] +* [**] Block Editor: Fixed an issue where a block would disappear when deleting all of the text inside without requiring the extra backspace to remove the block. [https://github.com/wordpress-mobile/gutenberg-mobile/pull/2877] +* [***] Block Editor: New Block: File [https://github.com/wordpress-mobile/gutenberg-mobile/pull/2835] + 16.3 ----- * [***] Site Creation: Adds an option to pick a home page design when creating a WordPress.com site. diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 4d02d3b45f54..f4b4888e6e2c 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -54,9 +54,9 @@ android { if (project.hasProperty("versionName")) { versionName project.property("versionName") } else { - versionName "alpha-261" + versionName "alpha-262" } - versionCode 965 + versionCode 967 minSdkVersion rootProject.minSdkVersion targetSdkVersion rootProject.targetSdkVersion @@ -98,9 +98,9 @@ android { dimension "buildType" // Only set the release version if one isn't provided if (!project.hasProperty("versionName")) { - versionName "16.3-rc-2" + versionName "16.3" } - versionCode 964 + versionCode 968 buildConfigField "boolean", "ME_ACTIVITY_AVAILABLE", "false" buildConfigField "boolean", "TENOR_AVAILABLE", "false" buildConfigField "long", "REMOTE_CONFIG_FETCH_INTERVAL", "3600" @@ -313,21 +313,28 @@ dependencies { implementation("$gradle.ext.fluxCBinaryPath:fluxc:$fluxCVersion") { exclude group: "com.android.volley" + exclude group: 'org.wordpress', module: 'utils' } implementation ('com.github.indexos.media-for-mobile:android:43a9026f0973a2f0a74fa813132f6a16f7499c3a') - implementation project(path:':libs:utils:WordPressUtils') - testImplementation project(path:':libs:utils:WordPressUtils') - debugImplementation project(path:':libs:utils:WordPressUtils') + implementation "$gradle.ext.wputilsBinaryPath:$wordPressUtilsVersion" + testImplementation "$gradle.ext.wputilsBinaryPath:$wordPressUtilsVersion" + debugImplementation "$gradle.ext.wputilsBinaryPath:$wordPressUtilsVersion" implementation (project(path:':libs:networking:WordPressNetworking')) { exclude group: "com.android.volley" + exclude group: 'org.wordpress', module: 'utils' + } + implementation (project(path:':libs:analytics:WordPressAnalytics')) { + exclude group: 'org.wordpress', module: 'utils' + } + implementation (project(path:':libs:editor:WordPressEditor')) { + exclude group: 'org.wordpress', module: 'utils' } - implementation project(path:':libs:analytics:WordPressAnalytics') - implementation project(path:':libs:editor:WordPressEditor') implementation (project(path:':libs:login:WordPressLoginFlow')) { exclude group: "com.github.wordpress-mobile.WordPress-FluxC-Android", module: "fluxc" exclude group: 'com.github.bumptech.glide' + exclude group: 'org.wordpress', module: 'utils' } implementation (group: 'com.zendesk', name: 'support', version: '5.0.1') { @@ -363,12 +370,14 @@ dependencies { implementation "org.jsoup:jsoup:1.10.3" implementation 'androidx.emoji:emoji:1.0.0' + + // Enables accessibility checks in Espresso + androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.3.0-alpha05' } configurations.all { // Exclude packaged wordpress sub projects, force the use of the source project // (eg. use :libs:utils:WordPressUtils instead of 'org.wordpress:utils') - exclude group: 'org.wordpress', module: 'utils' exclude group: 'org.wordpress', module: 'analytics' } diff --git a/WordPress/lint-baseline.xml b/WordPress/lint-baseline.xml index bb383a8dd07e..3fd4cc6ad0cc 100644 --- a/WordPress/lint-baseline.xml +++ b/WordPress/lint-baseline.xml @@ -84,22 +84,6 @@ column="20"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/WordPress/src/androidTest/java/org/wordpress/android/support/BaseTest.java b/WordPress/src/androidTest/java/org/wordpress/android/support/BaseTest.java index 77794f35878d..27235a3441ef 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/support/BaseTest.java +++ b/WordPress/src/androidTest/java/org/wordpress/android/support/BaseTest.java @@ -3,6 +3,7 @@ import android.app.Instrumentation; import androidx.test.core.app.ApplicationProvider; +import androidx.test.espresso.accessibility.AccessibilityChecks; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; @@ -13,8 +14,12 @@ import com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.DateOffset; import com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.HandlebarsHelper; import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult; +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType; import org.apache.commons.lang3.LocaleUtils; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Rule; import org.wordpress.android.R; @@ -36,6 +41,9 @@ import java.util.TimeZone; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesTypes; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.is; import static org.wordpress.android.BuildConfig.E2E_SELF_HOSTED_USER_SITE_ADDRESS; import static org.wordpress.android.BuildConfig.E2E_WP_COM_USER_EMAIL; import static org.wordpress.android.BuildConfig.E2E_WP_COM_USER_PASSWORD; @@ -53,6 +61,12 @@ public void setup() { mMockedAppComponent = DaggerAppComponentTest.builder() .application(mAppContext) .build(); + + Matcher nonErrorLevelMatcher = + Matchers.allOf(matchesTypes( + anyOf(is(AccessibilityCheckResultType.INFO), is(AccessibilityCheckResultType.WARNING)))); + AccessibilityChecks.enable().setRunChecksFromRootView(true).setThrowExceptionForErrors(false) + .setSuppressingResultMatcher(nonErrorLevelMatcher); } @Rule diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java index c783377c1a18..7625f916ebbb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -13,6 +13,9 @@ import androidx.core.app.TaskStackBuilder; import androidx.fragment.app.Fragment; +import com.wordpress.stories.compose.frame.FrameSaveNotifier; +import com.wordpress.stories.compose.frame.StorySaveEvents.StorySaveResult; + import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; @@ -44,6 +47,7 @@ import org.wordpress.android.ui.history.HistoryDetailActivity; import org.wordpress.android.ui.history.HistoryDetailContainerFragment; import org.wordpress.android.ui.history.HistoryListItem.Revision; +import org.wordpress.android.ui.jetpack.backup.BackupDownloadActivity; import org.wordpress.android.ui.main.MeActivity; import org.wordpress.android.ui.main.SitePickerActivity; import org.wordpress.android.ui.main.SitePickerAdapter.SitePickerMode; @@ -98,6 +102,7 @@ import java.util.Map; import static com.wordpress.stories.util.BundleUtilsKt.KEY_STORY_INDEX; +import static com.wordpress.stories.util.BundleUtilsKt.KEY_STORY_SAVE_RESULT; import static org.wordpress.android.analytics.AnalyticsTracker.ACTIVITY_LOG_ACTIVITY_ID_KEY; import static org.wordpress.android.analytics.AnalyticsTracker.Stat.POST_LIST_ACCESS_ERROR; import static org.wordpress.android.analytics.AnalyticsTracker.Stat.READER_ARTICLE_DETAIL_REBLOGGED; @@ -1048,6 +1053,22 @@ public static void viewJetpackSecuritySettings(Activity activity, SiteModel site activity.startActivity(intent); } + public static void viewStories(Activity activity, SiteModel site, StorySaveResult event) { + Intent intent = new Intent(activity, StoryComposerActivity.class); + intent.putExtra(KEY_STORY_SAVE_RESULT, event); + intent.putExtra(WordPress.SITE, site); + + // we need to have a way to cancel the related error notification when the user comes + // from tapping on MANAGE on the snackbar (otherwise they'll be able to discard the + // errored story but the error notification will remain existing in the system dashboard) + intent.setAction(String.valueOf(FrameSaveNotifier.getNotificationIdForError( + StoryComposerActivity.BASE_FRAME_MEDIA_ERROR_NOTIFICATION_ID, + event.getStoryIndex() + ))); + + activity.startActivity(intent); + } + public static void viewJetpackSecuritySettingsForResult(Activity activity, SiteModel site) { AnalyticsTracker.track(Stat.SITE_SETTINGS_JETPACK_SECURITY_SETTINGS_VIEWED); Intent intent = new Intent(activity, JetpackSecuritySettingsActivity.class); @@ -1309,4 +1330,9 @@ public static void viewPagesInNewStack(Context context) { intent.putExtra(WPMainActivity.ARG_OPEN_PAGE, WPMainActivity.ARG_PAGES); context.startActivity(intent); } + + public static void showBackupDownload(Activity activity) { + Intent intent = new Intent(activity, BackupDownloadActivity.class); + activity.startActivity(intent); + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/WPWebViewActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/WPWebViewActivity.java index 7bba7e188078..ee245e409e32 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/WPWebViewActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/WPWebViewActivity.java @@ -680,7 +680,12 @@ protected void configureWebView() { WebViewClient webViewClient = createWebViewClient(allowedURL); mWebView.setWebViewClient(webViewClient); - mWebView.setWebChromeClient(new WPWebChromeClient(this, (ProgressBar) findViewById(R.id.progress_bar))); + mWebView.setWebChromeClient(new WPWebChromeClient( + this, + mWebView, + R.drawable.media_movieclip, + (ProgressBar) findViewById(R.id.progress_bar) + )); } protected WebViewClient createWebViewClient(List allowedURL) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/ActivityLogNavigationEvents.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/ActivityLogNavigationEvents.kt new file mode 100644 index 000000000000..f94c470c1c44 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/ActivityLogNavigationEvents.kt @@ -0,0 +1,8 @@ +package org.wordpress.android.ui.activitylog + +import org.wordpress.android.ui.activitylog.list.ActivityLogListItem + +sealed class ActivityLogNavigationEvents { + data class ShowBackupDownload(val event: ActivityLogListItem.Event) : ActivityLogNavigationEvents() + data class ShowRestore(val event: ActivityLogListItem.Event) : ActivityLogNavigationEvents() +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogAdapter.kt index 0005ff9643f3..64cdda6d2973 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogAdapter.kt @@ -11,7 +11,8 @@ import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.ViewType class ActivityLogAdapter( private val itemClickListener: (ActivityLogListItem) -> Unit, - private val rewindClickListener: (ActivityLogListItem) -> Unit + private val rewindClickListener: (ActivityLogListItem) -> Unit, + private val secondaryActionClickListener: (ActivityLogListItem.SecondaryAction, ActivityLogListItem) -> Boolean ) : Adapter() { private val list = mutableListOf() @@ -58,7 +59,8 @@ class ActivityLogAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActivityLogViewHolder { return when (viewType) { ViewType.PROGRESS.id -> ProgressItemViewHolder(parent) - ViewType.EVENT.id -> EventItemViewHolder(parent, itemClickListener, rewindClickListener) + ViewType.EVENT.id -> EventItemViewHolder( + parent, itemClickListener, rewindClickListener, secondaryActionClickListener) ViewType.HEADER.id -> HeaderItemViewHolder(parent) ViewType.FOOTER.id -> FooterItemViewHolder(parent) ViewType.LOADING.id -> LoadingItemViewHolder(parent) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListActivity.kt index 3e8776686e5b..c1d1745414a4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListActivity.kt @@ -5,15 +5,21 @@ import android.os.Bundle import android.view.MenuItem import kotlinx.android.synthetic.main.toolbar_main.* import org.wordpress.android.R +import org.wordpress.android.WordPress import org.wordpress.android.ui.LocaleAwareActivity import org.wordpress.android.ui.RequestCodes import org.wordpress.android.ui.posts.BasicFragmentDialog +import org.wordpress.android.util.BackupFeatureConfig import org.wordpress.android.viewmodel.activitylog.ACTIVITY_LOG_REWIND_ID_KEY +import javax.inject.Inject -class ActivityLogListActivity : LocaleAwareActivity(), BasicFragmentDialog.BasicDialogPositiveClickInterface, +class ActivityLogListActivity : LocaleAwareActivity(), + BasicFragmentDialog.BasicDialogPositiveClickInterface, BasicFragmentDialog.BasicDialogNegativeClickInterface { + @Inject lateinit var backupFeatureConfig: BackupFeatureConfig override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + (application as WordPress).component().inject(this) setContentView(R.layout.activity_log_list_activity) @@ -46,6 +52,7 @@ class ActivityLogListActivity : LocaleAwareActivity(), BasicFragmentDialog.Basic } override fun onNegativeClicked(instanceTag: String) { + // Unused } private fun passRewindConfirmation(rewindId: String) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListFragment.kt index bd04d2e6f5bc..8fb2f5e83daa 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListFragment.kt @@ -21,6 +21,8 @@ import org.wordpress.android.WordPress import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.ActivityLauncher +import org.wordpress.android.ui.activitylog.ActivityLogNavigationEvents.ShowBackupDownload +import org.wordpress.android.ui.activitylog.ActivityLogNavigationEvents.ShowRestore import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterFragment import org.wordpress.android.ui.posts.BasicFragmentDialog import org.wordpress.android.ui.utils.UiHelpers @@ -159,6 +161,15 @@ class ActivityLogListFragment : Fragment() { viewModel.moveToTop.observe(this, Observer { log_list_view.scrollToPosition(0) }) + + viewModel.navigationEvents.observe(viewLifecycleOwner, Observer { + it.applyIfNotHandled { + when (this) { + is ShowBackupDownload -> ActivityLauncher.showBackupDownload(requireActivity()) + // todo: annmarie replace with the ActivityLauncher for showing restore details + is ShowRestore -> displayRewindDialog(event) } + } + }) } private fun displayRewindDialog(item: ActivityLogListItem.Event) { @@ -221,10 +232,17 @@ class ActivityLogListFragment : Fragment() { viewModel.onActionButtonClicked(item) } + private fun onSecondaryActionClicked( + secondaryAction: ActivityLogListItem.SecondaryAction, + item: ActivityLogListItem + ): Boolean { + return viewModel.onSecondaryActionClicked(secondaryAction, item) + } + private fun setEvents(events: List) { val adapter: ActivityLogAdapter if (log_list_view.adapter == null) { - adapter = ActivityLogAdapter(this::onItemClicked, this::onItemButtonClicked) + adapter = ActivityLogAdapter(this::onItemClicked, this::onItemButtonClicked, this::onSecondaryActionClicked) log_list_view.adapter = adapter } else { adapter = log_list_view.adapter as ActivityLogAdapter diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListItem.kt index 1f4761acbf6b..3920c618603c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListItem.kt @@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes import org.wordpress.android.R import org.wordpress.android.fluxc.model.activity.ActivityLogModel import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.Icon.HISTORY +import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.Icon.MORE import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.ViewType.EVENT import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.ViewType.FOOTER import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.ViewType.HEADER @@ -30,15 +31,16 @@ sealed class ActivityLogListItem(val type: ViewType) { val rewindId: String?, val date: Date, override val isButtonVisible: Boolean, - val buttonIcon: Icon = HISTORY, - val isProgressBarVisible: Boolean = false + val buttonIcon: Icon, + val isProgressBarVisible: Boolean = false, + val showMoreMenu: Boolean = false ) : ActivityLogListItem(EVENT), IActionableItem { val formattedDate: String = date.toFormattedDateString() val formattedTime: String = date.toFormattedTimeString() val icon = Icon.fromValue(gridIcon) val status = Status.fromValue(eventStatus) - constructor(model: ActivityLogModel, rewindDisabled: Boolean = false) : this( + constructor(model: ActivityLogModel, rewindDisabled: Boolean = false, backupFeatureEnabled: Boolean) : this( model.activityID, model.summary, model.content?.text ?: "", @@ -47,7 +49,10 @@ sealed class ActivityLogListItem(val type: ViewType) { model.rewindable ?: false, model.rewindID, model.published, - isButtonVisible = !rewindDisabled && model.rewindable ?: false) + isButtonVisible = !rewindDisabled && model.rewindable ?: false, + buttonIcon = if (backupFeatureEnabled) MORE else HISTORY, + showMoreMenu = backupFeatureEnabled + ) override fun longId(): Long = activityId.hashCode().toLong() } @@ -110,6 +115,7 @@ sealed class ActivityLogListItem(val type: ViewType) { THEMES("themes", R.drawable.ic_themes_white_24dp), TRASH("trash", R.drawable.ic_trash_white_24dp), USER("user", R.drawable.ic_user_white_24dp), + MORE("more", R.drawable.ic_ellipsis_vertical_white_24dp), DEFAULT("", R.drawable.ic_notice_white_24dp); companion object { @@ -117,4 +123,9 @@ sealed class ActivityLogListItem(val type: ViewType) { fun fromValue(value: String?) = map[value] ?: DEFAULT } } + + enum class SecondaryAction(val itemId: Long) { + RESTORE(0), + DOWNLOAD_BACKUP(1); + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListItemMenuAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListItemMenuAdapter.kt new file mode 100644 index 000000000000..b103e8e2b606 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListItemMenuAdapter.kt @@ -0,0 +1,79 @@ +package org.wordpress.android.ui.activitylog.list + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import org.wordpress.android.R +import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.SecondaryAction +import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.SecondaryAction.DOWNLOAD_BACKUP +import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.SecondaryAction.RESTORE +import org.wordpress.android.util.ColorUtils.setImageResourceWithTint +import org.wordpress.android.util.getColorResIdFromAttribute + +class ActivityLogListItemMenuAdapter( + context: Context +) : BaseAdapter() { + private val inflater: LayoutInflater = LayoutInflater.from(context) + private val items: List = SecondaryAction.values().toList() + override fun getCount(): Int { + return items.size + } + + override fun getItem(position: Int): Any { + return items[position] + } + + override fun getItemId(position: Int): Long { + return items[position].itemId + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val holder: ActivityListItemMenuHolder + val view: View + if (convertView == null) { + view = inflater.inflate(R.layout.activity_log_list_item_menu_item, parent, false) + holder = ActivityListItemMenuHolder(view) + view.tag = holder + } else { + view = convertView + holder = view.tag as ActivityListItemMenuHolder + } + + val textRes: Int + val iconRes: Int + val colorRes = view.context.getColorResIdFromAttribute(R.attr.wpColorOnSurfaceMedium) + when (items[position]) { + RESTORE -> { + textRes = R.string.activity_log_item_menu_restore_label + iconRes = R.drawable.ic_history_white_24dp + } + DOWNLOAD_BACKUP -> { + textRes = R.string.activity_log_item_menu_download_backup_label + iconRes = R.drawable.ic_get_app_24dp + } + } + holder.text.setText(textRes) + holder.text.setTextColor( + AppCompatResources.getColorStateList( + view.context, + colorRes + ) + ) + setImageResourceWithTint( + holder.icon, + iconRes, + colorRes + ) + return view + } + + internal inner class ActivityListItemMenuHolder(view: View) { + val text: TextView = view.findViewById(R.id.text) + val icon: ImageView = view.findViewById(R.id.image) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt index cac254251141..357dcff4a0fe 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt @@ -1,19 +1,24 @@ package org.wordpress.android.ui.activitylog.list import android.os.Bundle +import android.view.Gravity import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView +import android.widget.ListPopupWindow import android.widget.TextView import org.wordpress.android.R +import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.Event +import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.SecondaryAction import org.wordpress.android.util.ColorUtils import org.wordpress.android.util.getColorResIdFromAttribute class EventItemViewHolder( parent: ViewGroup, private val itemClickListener: (ActivityLogListItem) -> Unit, - private val rewindClickListener: (ActivityLogListItem) -> Unit + private val rewindClickListener: (ActivityLogListItem) -> Unit, + private val secondaryActionClickListener: (SecondaryAction, ActivityLogListItem) -> Boolean ) : ActivityLogViewHolder(parent, R.layout.activity_log_list_event_item) { private val summary: TextView = itemView.findViewById(R.id.action_summary) private val text: TextView = itemView.findViewById(R.id.action_text) @@ -31,16 +36,17 @@ class EventItemViewHolder( } } - fun bind(activity: ActivityLogListItem.Event) { + fun bind(activity: Event) { summary.text = activity.title text.text = activity.description + val colorRes = if (activity.showMoreMenu) R.attr.wpColorOnSurfaceMedium else R.attr.colorPrimary + ColorUtils.setImageResourceWithTint( + actionButton, + activity.buttonIcon.drawable, + actionButton.context.getColorResIdFromAttribute(colorRes) + ) if (activity.isButtonVisible) { - ColorUtils.setImageResourceWithTint( - actionButton, - activity.buttonIcon.drawable, - actionButton.context.getColorResIdFromAttribute(R.attr.colorPrimary) - ) actionButton.visibility = View.VISIBLE } else { actionButton.visibility = View.GONE @@ -53,7 +59,25 @@ class EventItemViewHolder( } actionButton.setOnClickListener { - rewindClickListener(activity) + if (activity.showMoreMenu) { + renderMoreMenu(activity, it) + } else { + rewindClickListener(activity) + } } } + + private fun renderMoreMenu(event: ActivityLogListItem, v: View) { + val popup = ListPopupWindow(v.context) + popup.width = v.context.resources.getDimensionPixelSize(R.dimen.menu_item_width) + popup.setAdapter(ActivityLogListItemMenuAdapter(v.context)) + popup.setDropDownGravity(Gravity.END) + popup.anchorView = v + popup.isModal = true + popup.setOnItemClickListener { _, _, _, id -> + popup.dismiss() + secondaryActionClickListener(SecondaryAction.values()[id.toInt()], event) + } + popup.show() + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt index 1b058d5ffeb5..7e33e79ce4a0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/ActivityLogTypeFilterViewModel.kt @@ -70,7 +70,9 @@ class ActivityLogTypeFilterViewModel @Inject constructor( private suspend fun buildContentUiState(activityTypes: List): Content { return withContext(bgDispatcher) { // TODO malinjir replace the hardcoded header title - val headerListItem = ListItemUiState.SectionHeader(UiStringText("Test")) + val headerListItem = ListItemUiState.SectionHeader( + UiStringRes(R.string.activity_log_activity_type_filter_header) + ) // TODO malinjir replace "it.toString()" with activity type name val activityTypeListItems: List = activityTypes .map { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/DummyActivityTypesProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/DummyActivityTypesProvider.kt index 88401d640dde..52289eed24d4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/DummyActivityTypesProvider.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/filter/DummyActivityTypesProvider.kt @@ -13,10 +13,15 @@ class DummyActivityTypesProvider @Inject constructor() { delay(1000) return DummyAvailableActivityTypesResponse( false, listOf( - DummyActivityType(1, "Dummy Users"), - DummyActivityType(2, "Dummy Backup"), - DummyActivityType(3, "Dummy Comments"), - DummyActivityType(4, "Dummy Posts") + DummyActivityType(1, "Users"), + DummyActivityType(2, "Backup"), + DummyActivityType(3, "Comments"), + DummyActivityType(4, "Posts"), + DummyActivityType(5, "Pages"), + DummyActivityType(6, "Plugins"), + DummyActivityType(7, "Scans"), + DummyActivityType(8, "Media"), + DummyActivityType(9, "Widgets") ) ) } @@ -26,5 +31,9 @@ class DummyActivityTypesProvider @Inject constructor() { val activityTypes: List = listOf() ) - data class DummyActivityType(val id: Int, val name: String) + data class DummyActivityType(val id: Int, val name: String) { + override fun toString(): String { + return "$name ($id) (dummy)" + } + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/BackupAvailableItemsProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/BackupAvailableItemsProvider.kt new file mode 100644 index 000000000000..de6d1c6feb7c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/BackupAvailableItemsProvider.kt @@ -0,0 +1,61 @@ +package org.wordpress.android.ui.jetpack + +import androidx.annotation.StringRes +import org.wordpress.android.R +import org.wordpress.android.ui.jetpack.BackupAvailableItemsProvider.BackupAvailableItemType.CONTENTS +import org.wordpress.android.ui.jetpack.BackupAvailableItemsProvider.BackupAvailableItemType.MEDIA_UPLOADS +import org.wordpress.android.ui.jetpack.BackupAvailableItemsProvider.BackupAvailableItemType.ROOTS +import org.wordpress.android.ui.jetpack.BackupAvailableItemsProvider.BackupAvailableItemType.SQLS +import org.wordpress.android.ui.jetpack.BackupAvailableItemsProvider.BackupAvailableItemType.THEMES +import org.wordpress.android.ui.jetpack.BackupAvailableItemsProvider.BackupAvailableItemType.PLUGINS +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This class provides the available item choices for Jetpack Backup Download + */ +@Singleton +class BackupAvailableItemsProvider @Inject constructor() { + fun getAvailableItems(): List { + return listOf( + BackupAvailableItem( + THEMES, + R.string.backup_item_themes + ), + BackupAvailableItem( + PLUGINS, + R.string.backup_item_plugins + ), + BackupAvailableItem( + MEDIA_UPLOADS, + R.string.backup_item_media_uploads + ), + BackupAvailableItem( + SQLS, + R.string.backup_item_sqls + ), + BackupAvailableItem( + ROOTS, + R.string.backup_item_roots + ), + BackupAvailableItem( + CONTENTS, + R.string.backup_item_contents + ) + ) + } + + data class BackupAvailableItem( + val availableItemType: BackupAvailableItemType, + @StringRes val labelResId: Int + ) + + enum class BackupAvailableItemType { + THEMES, + PLUGINS, + MEDIA_UPLOADS, + SQLS, + ROOTS, + CONTENTS + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/BackupDownloadActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/BackupDownloadActivity.kt index 7a820790af41..6cc327cb7b6f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/BackupDownloadActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/BackupDownloadActivity.kt @@ -1,6 +1,7 @@ package org.wordpress.android.ui.jetpack.backup import android.os.Bundle +import android.view.MenuItem import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import kotlinx.android.synthetic.main.backup_download_activity.* @@ -29,6 +30,14 @@ class BackupDownloadActivity : LocaleAwareActivity() { setupViewModel() } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + return super.onOptionsItemSelected(item) + } + private fun setupViewModel() { viewModel = ViewModelProvider(this, viewModelFactory) .get(BackupDownloadViewModel::class.java) @@ -36,7 +45,7 @@ class BackupDownloadActivity : LocaleAwareActivity() { // todo: annmarie temporary start val fragment = BackupDownloadDetailsFragment.newInstance() - showFragment(fragment, BackupDownloadDetailsFragment.TAG) + showFragment(fragment, BackupDownloadDetailsFragment.TAG, slideIn = false, isRootFragment = true) } // todo: annmarie - decision pt: have activity/frag pairs or use the replace fragment approach diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsAdapter.kt new file mode 100644 index 000000000000..62335f4ca729 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsAdapter.kt @@ -0,0 +1,66 @@ +package org.wordpress.android.ui.jetpack.backup.details + +import android.view.ViewGroup +import androidx.annotation.MainThread +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import org.wordpress.android.ui.jetpack.backup.details.BackupDownloadDetailsViewModel.ListItemUiState +import org.wordpress.android.ui.utils.UiHelpers + +class BackupDownloadDetailsAdapter(private val uiHelpers: UiHelpers) : + RecyclerView.Adapter() { + private val items = mutableListOf() + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BackupDownloadDetailsViewHolder { + return BackupDownloadDetailsViewHolder.BackupDownloadDetailsListItemViewHolder(parent, uiHelpers) + } + + override fun getItemCount(): Int = items.size + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun onBindViewHolder(holder: BackupDownloadDetailsViewHolder, position: Int) { + holder.onBind(items[position]) + } + + @MainThread + fun update(newItems: List) { + val diffResult = DiffUtil.calculateDiff( + BackupDownloadDetailsListDiffUtils( + items.toList(), + newItems + ) + ) + items.clear() + items.addAll(newItems) + diffResult.dispatchUpdatesTo(this) + } + + private class BackupDownloadDetailsListDiffUtils( + val oldItems: List, + val newItems: List + ) : DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + if (oldItem::class != newItem::class) { + return false + } + + return oldItem.label == newItem.label + } + + override fun getOldListSize(): Int = oldItems.size + + override fun getNewListSize(): Int = newItems.size + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldItems[oldItemPosition] == newItems[newItemPosition] + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsFragment.kt index dbdc4979681a..249ee0c0144d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsFragment.kt @@ -5,14 +5,23 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.backup_download_details_fragment.* import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.ui.jetpack.backup.BackupDownloadViewModel +import org.wordpress.android.ui.jetpack.backup.details.BackupDownloadDetailsViewModel.UiState.Content +import org.wordpress.android.ui.jetpack.backup.details.BackupDownloadDetailsViewModel.UiState.Loading +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.ToastUtils import javax.inject.Inject class BackupDownloadDetailsFragment : Fragment() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + @Inject lateinit var uiHelpers: UiHelpers private lateinit var parentViewModel: BackupDownloadViewModel private lateinit var viewModel: BackupDownloadDetailsViewModel override fun onCreateView( @@ -29,24 +38,39 @@ class BackupDownloadDetailsFragment : Fragment() { val nonNullActivity = requireActivity() (nonNullActivity.application as WordPress).component().inject(this) + initRecyclerView() + initViewModel() + } + + private fun initRecyclerView() { + recycler_view.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + initAdapter() + } + + private fun initAdapter() { + recycler_view.adapter = BackupDownloadDetailsAdapter(uiHelpers) + } + + private fun initViewModel() { parentViewModel = ViewModelProvider(requireActivity(), viewModelFactory) .get(BackupDownloadViewModel::class.java) viewModel = ViewModelProvider(this, viewModelFactory) .get(BackupDownloadDetailsViewModel::class.java) - setupViews() - setupObservers() - // todo: annmarie - if something needs to be passed to VM, do it on start - viewModel.start() - } + viewModel.uiState.observe(viewLifecycleOwner, Observer { uiState -> + when (uiState) { + is Loading -> ToastUtils.showToast(requireContext(), "Implement loading") + is Content -> showContent(uiState) + is Error -> ToastUtils.showToast(requireContext(), "Implement Error") + } + }) - private fun setupViews() { - // TODO: annmarie implement setupViews + viewModel.start() } - private fun setupObservers() { - // TODO: annmarie implement setupObservers + private fun showContent(content: Content) { + ((recycler_view.adapter) as BackupDownloadDetailsAdapter).update(content.items) } companion object { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsViewHolder.kt new file mode 100644 index 000000000000..5f9ed168dd28 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsViewHolder.kt @@ -0,0 +1,32 @@ +package org.wordpress.android.ui.jetpack.backup.details + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.backup_details_list_item.view.* +import org.wordpress.android.R +import org.wordpress.android.ui.jetpack.backup.details.BackupDownloadDetailsViewModel.ListItemUiState +import org.wordpress.android.ui.utils.UiHelpers + +sealed class BackupDownloadDetailsViewHolder( + @LayoutRes layout: Int, + parent: ViewGroup +) : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(layout, parent, false)) { + abstract fun onBind(uiState: ListItemUiState) + + class BackupDownloadDetailsListItemViewHolder( + parentView: ViewGroup, + private val uiHelpers: UiHelpers + ) : BackupDownloadDetailsViewHolder(R.layout.backup_details_list_item, parentView) { + private val container = itemView.item_container + private val checkbox = itemView.checkbox + private val label = itemView.checkbox_label + + override fun onBind(uiState: ListItemUiState) { + uiHelpers.setTextOrHide(label, uiState.label) + checkbox.isChecked = uiState.checked + container.setOnClickListener { uiState.onClick.invoke() } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsViewModel.kt index a1f5e0e65a7d..5b9b1e7165df 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/details/BackupDownloadDetailsViewModel.kt @@ -1,15 +1,97 @@ package org.wordpress.android.ui.jetpack.backup.details -import androidx.lifecycle.ViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.wordpress.android.R +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.modules.UI_THREAD +import org.wordpress.android.ui.jetpack.BackupAvailableItemsProvider +import org.wordpress.android.ui.jetpack.BackupAvailableItemsProvider.BackupAvailableItem +import org.wordpress.android.ui.jetpack.BackupAvailableItemsProvider.BackupAvailableItemType +import org.wordpress.android.ui.jetpack.backup.details.BackupDownloadDetailsViewModel.UiState.Content +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject +import javax.inject.Named -class BackupDownloadDetailsViewModel @Inject constructor() : ViewModel() { +class BackupDownloadDetailsViewModel @Inject constructor( + private val backupAvailableItemsProvider: BackupAvailableItemsProvider, + @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, + @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher +) : ScopedViewModel(mainDispatcher) { private var isStarted: Boolean = false + + private val _uiState = MutableLiveData() + val uiState: LiveData = _uiState + fun start() { - if (isStarted) { - return + if (isStarted) return + isStarted = true + + // todo: annmarie - this is a temp name until useCase has been added for record dets :) + getData() + } + + private fun getData() { + launch { + val availableItems = backupAvailableItemsProvider.getAvailableItems() + _uiState.value = buildContentUiState(availableItems) } + } - isStarted = true + private suspend fun buildContentUiState(items: List): Content { + return withContext(bgDispatcher) { + val availableItemsListItems: List = items.map { + ListItemUiState( + availableItemType = it.availableItemType, + label = UiStringRes(it.labelResId), + checked = true, + onClick = { onItemClicked(it.availableItemType) } + ) + } + // todo: annmarie - swap out the placeholder for date from record + Content( + description = UiStringRes(R.string.backup_download_details_description), + items = availableItemsListItems + ) + } } + + private fun onItemClicked(backupAvailableItemType: BackupAvailableItemType) { + // todo: annmarie update the checkboxes - keep a running list of selected checkboxes, so + // they can be persisted on rotation + (_uiState.value as? Content)?.let { content -> + val updatedList = content.items.map { itemUiState -> + if (itemUiState.availableItemType == backupAvailableItemType) { + itemUiState.copy(checked = !itemUiState.checked) + } else { + itemUiState + } + } + _uiState.postValue(content.copy(items = updatedList)) + } + } + + sealed class UiState { + // todo: annmarie - add error/loading states - what SHOULD happen if I can't get the record? + data class Error(val message: String) : UiState() + + data class Loading(val message: String) : UiState() + + data class Content( + val description: UiString, + val items: List + ) : UiState() + } + + data class ListItemUiState( + val availableItemType: BackupAvailableItemType, + val label: UiString, + val checked: Boolean = false, + val onClick: (() -> Unit) + ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.kt index 9291cbe1764f..220ce55870d1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MySiteFragment.kt @@ -22,12 +22,10 @@ import androidx.fragment.app.Fragment import com.google.android.material.appbar.AppBarLayout import com.google.android.material.snackbar.Snackbar import com.wordpress.stories.compose.frame.FrameSaveNotifier.Companion.buildSnackbarErrorMessage -import com.wordpress.stories.compose.frame.FrameSaveNotifier.Companion.getNotificationIdForError import com.wordpress.stories.compose.frame.StorySaveEvents.Companion.allErrorsInResult import com.wordpress.stories.compose.frame.StorySaveEvents.StorySaveProcessStart import com.wordpress.stories.compose.frame.StorySaveEvents.StorySaveResult import com.wordpress.stories.compose.story.StoryRepository.getStoryAtIndex -import com.wordpress.stories.util.KEY_STORY_SAVE_RESULT import com.yalantis.ucrop.UCrop import com.yalantis.ucrop.UCrop.Options import com.yalantis.ucrop.UCropActivity @@ -99,7 +97,6 @@ import org.wordpress.android.ui.PagePostCreationSourcesDetail.STORY_FROM_MY_SITE import org.wordpress.android.ui.RequestCodes import org.wordpress.android.ui.TextInputDialogFragment import org.wordpress.android.ui.accounts.LoginActivity -import org.wordpress.android.ui.comments.CommentsListFragment.CommentStatusCriteria.ALL import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistrationPurpose.CTA_DOMAIN_CREDIT_REDEMPTION import org.wordpress.android.ui.domains.DomainRegistrationResultFragment import org.wordpress.android.ui.main.WPMainActivity.OnScrollToTopListener @@ -126,7 +123,6 @@ import org.wordpress.android.ui.quickstart.QuickStartMySitePrompts.Companion.isT import org.wordpress.android.ui.quickstart.QuickStartNoticeDetails import org.wordpress.android.ui.stories.StoriesMediaPickerResultHandler import org.wordpress.android.ui.stories.StoriesTrackerHelper -import org.wordpress.android.ui.stories.StoryComposerActivity import org.wordpress.android.ui.themes.ThemeBrowserUtils import org.wordpress.android.ui.uploads.UploadService import org.wordpress.android.ui.uploads.UploadService.UploadErrorEvent @@ -787,8 +783,6 @@ class MySiteFragment : Fragment(), ActivityLauncher.viewBlogStats(activity, selectedSite) } RequestCodes.SITE_PICKER -> if (resultCode == Activity.RESULT_OK) { - // reset comments status filter - AppPrefs.setCommentsStatusFilter(ALL) // reset domain credit flag - it will be checked in onSiteChanged isDomainCreditAvailable = false } @@ -1201,37 +1195,21 @@ class MySiteFragment : Fragment(), uploadUtilsWrapper.showSnackbarError( requireActivity().findViewById(R.id.coordinator), snackbarMessage, - string.story_saving_failed_quick_action_manage, - View.OnClickListener { view: View? -> - val intent = Intent( - requireActivity(), - StoryComposerActivity::class.java - ) - intent.putExtra(KEY_STORY_SAVE_RESULT, event) - intent.putExtra(WordPress.SITE, selectedSite) - - // we need to have a way to cancel the related error notification when the user comes - // from tapping on MANAGE on the snackbar (otherwise they'll be able to discard the - // errored story but the error notification will remain existing in the system dashboard) - intent.action = getNotificationIdForError( - StoryComposerActivity.BASE_FRAME_MEDIA_ERROR_NOTIFICATION_ID, - event.storyIndex - ).toString() + "" - - // TODO WPSTORIES add TRACKS: the putExtra described here below for NOTIFICATION_TYPE - // is meant to be used for tracking purposes. Use it! - // TODO add NotificationType.MEDIA_SAVE_ERROR param later when integrating with WPAndroid - // val notificationType = NotificationType.MEDIA_SAVE_ERROR - // notificationIntent.putExtra(ARG_NOTIFICATION_TYPE, notificationType) - - storiesTrackerHelper.trackStorySaveResultEvent( - event, - STORY_SAVE_ERROR_SNACKBAR_MANAGE_TAPPED + string.story_saving_failed_quick_action_manage + ) { + // TODO WPSTORIES add TRACKS: the putExtra described here below for NOTIFICATION_TYPE + // is meant to be used for tracking purposes. Use it! + // TODO add NotificationType.MEDIA_SAVE_ERROR param later when integrating with WPAndroid + // val notificationType = NotificationType.MEDIA_SAVE_ERROR + // notificationIntent.putExtra(ARG_NOTIFICATION_TYPE, notificationType) + + storiesTrackerHelper.trackStorySaveResultEvent( + event, + STORY_SAVE_ERROR_SNACKBAR_MANAGE_TAPPED - ) - startActivity(intent) - } - ) + ) + ActivityLauncher.viewStories(requireActivity(), selectedSite, event) + } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index 535da295bb77..af1ca2022637 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -143,6 +143,7 @@ import static org.wordpress.android.login.LoginAnalyticsListener.CreatedAccountSource.EMAIL; import static org.wordpress.android.push.NotificationsProcessingService.ARG_NOTIFICATION_TYPE; import static org.wordpress.android.ui.JetpackConnectionSource.NOTIFICATIONS; +import static org.wordpress.android.ui.comments.CommentsListFragment.CommentStatusCriteria.ALL; /** * Main activity which hosts sites, reader, me and notifications pages @@ -1055,6 +1056,7 @@ public void onClick(View v) { QuickStartUtils.cancelQuickStartReminder(this); AppPrefs.setQuickStartNoticeRequired(false); AppPrefs.setLastSkippedQuickStartTask(null); + AppPrefs.setCommentsStatusFilter(ALL); // reset comments status filter mPrivateAtomicCookie.clearCookie(); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/DomainRegistrationHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/DomainRegistrationHandler.kt new file mode 100644 index 000000000000..1eb99c12c7bd --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/DomainRegistrationHandler.kt @@ -0,0 +1,58 @@ +package org.wordpress.android.ui.mysite + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +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.store.SiteStore.OnPlansFetched +import org.wordpress.android.ui.plans.isDomainCreditAvailable +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AppLog.T.DOMAIN_REGISTRATION +import org.wordpress.android.util.SiteUtilsWrapper +import javax.inject.Inject + +class DomainRegistrationHandler +@Inject constructor( + private val dispatcher: Dispatcher, + private val selectedSiteRepository: SelectedSiteRepository, + private val siteUtils: SiteUtilsWrapper +) { + private val _sitePlansFetched = MutableLiveData() + val isDomainCreditAvailable: LiveData = MediatorLiveData().apply { + addSource(selectedSiteRepository.selectedSiteChange) { + it?.let { site -> + if (shouldFetchPlans(site)) { + fetchPlans(site) + } else { + postValue(false) + } + } + } + addSource(_sitePlansFetched) { event -> + if (event.isError) { + AppLog.e(DOMAIN_REGISTRATION, "An error occurred while fetching plans : " + event.error.message) + } else if (selectedSiteRepository.getSelectedSite()?.id == event.site.id) { + postValue(isDomainCreditAvailable(event.plans)) + } + } + } + + init { + dispatcher.register(this) + } + + fun clear() { + dispatcher.unregister(this) + } + + private fun shouldFetchPlans(site: SiteModel) = !siteUtils.onFreePlan(site) && !siteUtils.hasCustomDomain(site) + + private fun fetchPlans(site: SiteModel) = dispatcher.dispatch(SiteActionBuilder.newFetchPlansAction(site)) + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onPlansFetched(event: OnPlansFetched) = _sitePlansFetched.postValue(event) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/DomainRegistrationViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/DomainRegistrationViewHolder.kt new file mode 100644 index 000000000000..89ab6b13babb --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/DomainRegistrationViewHolder.kt @@ -0,0 +1,14 @@ +package org.wordpress.android.ui.mysite + +import android.view.ViewGroup +import kotlinx.android.synthetic.main.domain_registration_block.view.* +import org.wordpress.android.R +import org.wordpress.android.ui.mysite.MySiteItem.DomainRegistrationBlock + +class DomainRegistrationViewHolder( + parent: ViewGroup +) : MySiteItemViewHolder(parent, R.layout.domain_registration_block) { + fun bind(item: DomainRegistrationBlock) = itemView.apply { + my_site_register_domain_cta.setOnClickListener { item.onClick.click() } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/ImprovedMySiteFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/ImprovedMySiteFragment.kt index 5c631a6a578e..694c60dfa3e5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/ImprovedMySiteFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/ImprovedMySiteFragment.kt @@ -25,30 +25,37 @@ import org.wordpress.android.WordPress import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.RequestCodes import org.wordpress.android.ui.TextInputDialogFragment +import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistrationPurpose.CTA_DOMAIN_CREDIT_REDEMPTION +import org.wordpress.android.ui.domains.DomainRegistrationResultFragment.Companion.RESULT_REGISTERED_DOMAIN_EMAIL import org.wordpress.android.ui.main.utils.MeGravatarLoader -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.ConnectJetpackForStats -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenActivityLog -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenAdmin -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenComments -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenCropActivity -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenJetpackSettings -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenMeScreen -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenMedia -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenMediaPicker -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenPages -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenPeople -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenPlan -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenPlugins -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenPosts -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenScan -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenSharing -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenSite -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenSitePicker -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenSiteSettings -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenStats -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenThemes -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.StartWPComLoginForJetpackStats import org.wordpress.android.ui.mysite.SiteIconUploadHandler.ItemUploadedModel +import org.wordpress.android.ui.mysite.SiteNavigationAction.AddNewStory +import org.wordpress.android.ui.mysite.SiteNavigationAction.AddNewStoryWithMediaIds +import org.wordpress.android.ui.mysite.SiteNavigationAction.AddNewStoryWithMediaUris +import org.wordpress.android.ui.mysite.SiteNavigationAction.ConnectJetpackForStats +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenActivityLog +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenAdmin +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenComments +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenCropActivity +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenDomainRegistration +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenJetpackSettings +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenMeScreen +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenMedia +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenMediaPicker +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenPages +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenPeople +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenPlan +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenPlugins +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenPosts +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenScan +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenSharing +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenSite +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenSitePicker +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenSiteSettings +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenStats +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenStories +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenThemes +import org.wordpress.android.ui.mysite.SiteNavigationAction.StartWPComLoginForJetpackStats import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.photopicker.MediaPickerConstants import org.wordpress.android.ui.photopicker.MediaPickerLauncher @@ -200,6 +207,32 @@ class ImprovedMySiteFragment : Fragment(), is ConnectJetpackForStats -> ActivityLauncher.viewConnectJetpackForStats(activity, action.site) is StartWPComLoginForJetpackStats -> ActivityLauncher.loginForJetpackStats(this) is OpenJetpackSettings -> ActivityLauncher.viewJetpackSecuritySettings(activity, action.site) + is OpenStories -> ActivityLauncher.viewStories(activity, action.site, action.event) + is AddNewStory -> + ActivityLauncher.addNewStoryForResult( + activity, + action.site, + action.source + ) + is AddNewStoryWithMediaIds -> + ActivityLauncher.addNewStoryWithMediaIdsForResult( + activity, + action.site, + action.source, + action.mediaIds.toLongArray() + ) + is AddNewStoryWithMediaUris -> + ActivityLauncher.addNewStoryWithMediaUrisForResult( + activity, + action.site, + action.source, + action.mediaUris.toTypedArray() + ) + is OpenDomainRegistration -> ActivityLauncher.viewDomainRegistrationActivityForResult( + activity, + action.site, + CTA_DOMAIN_CREDIT_REDEMPTION + ) } } }) @@ -313,6 +346,10 @@ class ImprovedMySiteFragment : Fragment(), } } } + RequestCodes.STORIES_PHOTO_PICKER, + RequestCodes.PHOTO_PICKER -> if (resultCode == Activity.RESULT_OK) { + viewModel.handleStoriesPhotoPickerResult(data) + } UCrop.REQUEST_CROP -> { if (resultCode == UCrop.RESULT_ERROR) { AppLog.e( @@ -323,6 +360,9 @@ class ImprovedMySiteFragment : Fragment(), } viewModel.handleCropResult(UCrop.getOutput(data), resultCode == Activity.RESULT_OK) } + RequestCodes.DOMAIN_REGISTRATION -> if (resultCode == Activity.RESULT_OK) { + viewModel.handleSuccessfulDomainRegistrationResult(data.getStringExtra(RESULT_REGISTERED_DOMAIN_EMAIL)) + } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt index 6fb953d6faa8..afd4bf9e3017 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapter.kt @@ -4,10 +4,12 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView.Adapter import org.wordpress.android.ui.mysite.MySiteItem.CategoryHeader +import org.wordpress.android.ui.mysite.MySiteItem.DomainRegistrationBlock import org.wordpress.android.ui.mysite.MySiteItem.ListItem import org.wordpress.android.ui.mysite.MySiteItem.QuickActionsBlock import org.wordpress.android.ui.mysite.MySiteItem.SiteInfoBlock import org.wordpress.android.ui.mysite.MySiteItem.Type.CATEGORY_HEADER +import org.wordpress.android.ui.mysite.MySiteItem.Type.DOMAIN_REGISTRATION_BLOCK import org.wordpress.android.ui.mysite.MySiteItem.Type.LIST_ITEM import org.wordpress.android.ui.mysite.MySiteItem.Type.QUICK_ACTIONS_BLOCK import org.wordpress.android.ui.mysite.MySiteItem.Type.SITE_INFO_BLOCK @@ -28,6 +30,7 @@ class MySiteAdapter(val imageManager: ImageManager, val uiHelpers: UiHelpers) : return when (viewType) { SITE_INFO_BLOCK.ordinal -> MySiteInfoViewHolder(parent, imageManager) QUICK_ACTIONS_BLOCK.ordinal -> QuickActionsViewHolder(parent) + DOMAIN_REGISTRATION_BLOCK.ordinal -> DomainRegistrationViewHolder(parent) CATEGORY_HEADER.ordinal -> MySiteCategoryViewHolder(parent, uiHelpers) LIST_ITEM.ordinal -> MySiteListItemViewHolder(parent, uiHelpers) else -> throw IllegalArgumentException("Unexpected view type") @@ -38,6 +41,7 @@ class MySiteAdapter(val imageManager: ImageManager, val uiHelpers: UiHelpers) : when (holder) { is MySiteInfoViewHolder -> holder.bind(items[position] as SiteInfoBlock) is QuickActionsViewHolder -> holder.bind(items[position] as QuickActionsBlock) + is DomainRegistrationViewHolder -> holder.bind(items[position] as DomainRegistrationBlock) is MySiteCategoryViewHolder -> holder.bind(items[position] as CategoryHeader) is MySiteListItemViewHolder -> holder.bind(items[position] as ListItem) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt index 8d58fc040942..760ab2b7becd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteAdapterDiffCallback.kt @@ -3,6 +3,7 @@ package org.wordpress.android.ui.mysite import androidx.recyclerview.widget.DiffUtil import org.apache.commons.lang3.NotImplementedException import org.wordpress.android.ui.mysite.MySiteItem.CategoryHeader +import org.wordpress.android.ui.mysite.MySiteItem.DomainRegistrationBlock import org.wordpress.android.ui.mysite.MySiteItem.ListItem import org.wordpress.android.ui.mysite.MySiteItem.QuickActionsBlock import org.wordpress.android.ui.mysite.MySiteItem.SiteInfoBlock @@ -21,6 +22,7 @@ class MySiteAdapterDiffCallback( return oldItem.type == updatedItem.type && when { oldItem is SiteInfoBlock && updatedItem is SiteInfoBlock -> true oldItem is QuickActionsBlock && updatedItem is QuickActionsBlock -> true + oldItem is DomainRegistrationBlock && updatedItem is DomainRegistrationBlock -> true oldItem is CategoryHeader && updatedItem is CategoryHeader -> oldItem.title == updatedItem.title oldItem is ListItem && updatedItem is ListItem -> oldItem.primaryText == updatedItem.primaryText else -> throw NotImplementedException("Diff not implemented yet") diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteItem.kt index 182464a635ce..7746e096d653 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteItem.kt @@ -2,6 +2,7 @@ package org.wordpress.android.ui.mysite import androidx.annotation.DrawableRes import org.wordpress.android.ui.mysite.MySiteItem.Type.CATEGORY_HEADER +import org.wordpress.android.ui.mysite.MySiteItem.Type.DOMAIN_REGISTRATION_BLOCK import org.wordpress.android.ui.mysite.MySiteItem.Type.LIST_ITEM import org.wordpress.android.ui.mysite.MySiteItem.Type.QUICK_ACTIONS_BLOCK import org.wordpress.android.ui.mysite.MySiteItem.Type.SITE_INFO_BLOCK @@ -12,6 +13,7 @@ sealed class MySiteItem(val type: Type) { enum class Type { SITE_INFO_BLOCK, QUICK_ACTIONS_BLOCK, + DOMAIN_REGISTRATION_BLOCK, CATEGORY_HEADER, LIST_ITEM } @@ -39,6 +41,8 @@ sealed class MySiteItem(val type: Type) { val showPages: Boolean = true ) : MySiteItem(QUICK_ACTIONS_BLOCK) + data class DomainRegistrationBlock(val onClick: ListItemInteraction) : MySiteItem(DOMAIN_REGISTRATION_BLOCK) + data class CategoryHeader(val title: UiString) : MySiteItem(CATEGORY_HEADER) data class ListItem( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt index 659c8cd16178..518f8bc15d7b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt @@ -1,5 +1,6 @@ package org.wordpress.android.ui.mysite +import android.content.Intent import android.net.Uri import android.text.TextUtils import androidx.annotation.StringRes @@ -8,6 +9,9 @@ import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.launch import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAIN_CREDIT_PROMPT_SHOWN +import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAIN_CREDIT_REDEMPTION_SUCCESS +import org.wordpress.android.analytics.AnalyticsTracker.Stat.DOMAIN_CREDIT_REDEMPTION_TAPPED import org.wordpress.android.analytics.AnalyticsTracker.Stat.MY_SITE_ICON_CROPPED import org.wordpress.android.analytics.AnalyticsTracker.Stat.MY_SITE_ICON_GALLERY_PICKED import org.wordpress.android.analytics.AnalyticsTracker.Stat.MY_SITE_ICON_REMOVED @@ -22,6 +26,7 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD +import org.wordpress.android.ui.PagePostCreationSourcesDetail.STORY_FROM_MY_SITE import org.wordpress.android.ui.mysite.ListItemAction.ACTIVITY_LOG import org.wordpress.android.ui.mysite.ListItemAction.ADMIN import org.wordpress.android.ui.mysite.ListItemAction.COMMENTS @@ -38,31 +43,33 @@ import org.wordpress.android.ui.mysite.ListItemAction.SITE_SETTINGS import org.wordpress.android.ui.mysite.ListItemAction.STATS import org.wordpress.android.ui.mysite.ListItemAction.THEMES import org.wordpress.android.ui.mysite.ListItemAction.VIEW_SITE +import org.wordpress.android.ui.mysite.MySiteItem.DomainRegistrationBlock import org.wordpress.android.ui.mysite.MySiteItem.QuickActionsBlock -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.ConnectJetpackForStats -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenActivityLog -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenAdmin -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenComments -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenCropActivity -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenJetpackSettings -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenMeScreen -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenMedia -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenMediaPicker -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenPages -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenPeople -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenPlan -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenPlugins -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenPosts -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenScan -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenSharing -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenSite -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenSitePicker -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenSiteSettings -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenStats -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.OpenThemes -import org.wordpress.android.ui.mysite.MySiteViewModel.NavigationAction.StartWPComLoginForJetpackStats import org.wordpress.android.ui.mysite.SiteDialogModel.AddSiteIconDialogModel import org.wordpress.android.ui.mysite.SiteDialogModel.ChangeSiteIconDialogModel +import org.wordpress.android.ui.mysite.SiteNavigationAction.ConnectJetpackForStats +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenActivityLog +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenAdmin +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenComments +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenCropActivity +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenDomainRegistration +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenJetpackSettings +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenMeScreen +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenMedia +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenMediaPicker +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenPages +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenPeople +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenPlan +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenPlugins +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenPosts +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenScan +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenSharing +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenSite +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenSitePicker +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenSiteSettings +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenStats +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenThemes +import org.wordpress.android.ui.mysite.SiteNavigationAction.StartWPComLoginForJetpackStats import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.photopicker.PhotoPickerActivity.PhotoPickerMediaSource import org.wordpress.android.ui.photopicker.PhotoPickerActivity.PhotoPickerMediaSource.ANDROID_CAMERA @@ -80,6 +87,7 @@ import org.wordpress.android.util.UriWrapper import org.wordpress.android.util.WPMediaUtilsWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.distinct +import org.wordpress.android.util.getEmailValidationMessage import org.wordpress.android.util.merge import org.wordpress.android.viewmodel.ContextProvider import org.wordpress.android.viewmodel.Event @@ -102,26 +110,29 @@ class MySiteViewModel private val mediaUtilsWrapper: MediaUtilsWrapper, private val fluxCUtilsWrapper: FluxCUtilsWrapper, private val contextProvider: ContextProvider, - private val siteIconUploadHandler: SiteIconUploadHandler + private val siteIconUploadHandler: SiteIconUploadHandler, + private val siteStoriesHandler: SiteStoriesHandler, + private val domainRegistrationHandler: DomainRegistrationHandler ) : ScopedViewModel(mainDispatcher) { private val _currentAccountAvatarUrl = MutableLiveData() private val _onSnackbarMessage = MutableLiveData>() private val _onTechInputDialogShown = MutableLiveData>() private val _onBasicDialogShown = MutableLiveData>() - private val _onNavigation = MutableLiveData>() + private val _onNavigation = MutableLiveData>() private val _onMediaUpload = MutableLiveData>() - val onSnackbarMessage = _onSnackbarMessage as LiveData> + val onSnackbarMessage = merge(_onSnackbarMessage, siteStoriesHandler.onSnackbar) val onTextInputDialogShown = _onTechInputDialogShown as LiveData> val onBasicDialogShown = _onBasicDialogShown as LiveData> - val onNavigation = _onNavigation as LiveData> + val onNavigation = merge(_onNavigation, siteStoriesHandler.onNavigation) val onMediaUpload = _onMediaUpload as LiveData> val onUploadedItem = siteIconUploadHandler.onUploadedItem val uiModel: LiveData = merge( _currentAccountAvatarUrl, selectedSiteRepository.selectedSiteChange, - selectedSiteRepository.showSiteIconProgressBar.distinct() - ) { currentAvatarUrl, site, showSiteIconProgressBar -> + selectedSiteRepository.showSiteIconProgressBar.distinct(), + domainRegistrationHandler.isDomainCreditAvailable.distinct() + ) { currentAvatarUrl, site, showSiteIconProgressBar, isDomainCreditAvailable -> val items = if (site != null) { val siteItems = mutableListOf() siteItems.add( @@ -143,6 +154,10 @@ class MySiteViewModel site.isSelfHostedAdmin || site.hasCapabilityEditPages ) ) + if (isDomainCreditAvailable == true) { + analyticsTrackerWrapper.track(DOMAIN_CREDIT_PROMPT_SHOWN) + siteItems.add(DomainRegistrationBlock(ListItemInteraction.create(site, this::domainRegistrationClick))) + } siteItems.addAll(siteItemsBuilder.buildSiteItems(site, this::onItemClick)) siteItems } else { @@ -249,6 +264,11 @@ class MySiteViewModel _onNavigation.value = Event(OpenMedia(site)) } + private fun domainRegistrationClick(site: SiteModel) { + analyticsTrackerWrapper.track(DOMAIN_CREDIT_REDEMPTION_TAPPED, site) + _onNavigation.value = Event(OpenDomainRegistration(site)) + } + fun refresh() { selectedSiteRepository.updateSiteSettingsIfNecessary() _currentAccountAvatarUrl.value = accountStore.account?.avatarUrl.orEmpty() @@ -330,6 +350,11 @@ class MySiteViewModel selectedSiteRepository.getSelectedSite()?.let { site -> _onNavigation.value = Event(OpenStats(site)) } } + fun handleSuccessfulDomainRegistrationResult(email: String?) { + analyticsTrackerWrapper.track(DOMAIN_CREDIT_REDEMPTION_SUCCESS) + _onSnackbarMessage.postValue(Event(SnackbarMessageHolder(getEmailValidationMessage(email)))) + } + private fun startSiteIconUpload(filePath: String) { if (TextUtils.isEmpty(filePath)) { _onSnackbarMessage.postValue(Event(SnackbarMessageHolder(UiStringRes(R.string.error_locating_image)))) @@ -376,9 +401,17 @@ class MySiteViewModel override fun onCleared() { siteIconUploadHandler.clear() + siteStoriesHandler.clear() + domainRegistrationHandler.clear() super.onCleared() } + fun handleStoriesPhotoPickerResult(data: Intent) { + selectedSiteRepository.getSelectedSite()?.let { + siteStoriesHandler.handleStoriesResult(it, data, STORY_FROM_MY_SITE) + } + } + data class UiModel( val accountAvatarUrl: String, val items: List @@ -393,31 +426,6 @@ class MySiteViewModel val isInputEnabled: Boolean ) - sealed class NavigationAction { - object OpenMeScreen : NavigationAction() - data class OpenSite(val site: SiteModel) : NavigationAction() - data class OpenSitePicker(val site: SiteModel) : NavigationAction() - data class OpenMediaPicker(val site: SiteModel) : NavigationAction() - data class OpenCropActivity(val imageUri: UriWrapper) : NavigationAction() - data class OpenActivityLog(val site: SiteModel) : NavigationAction() - data class OpenScan(val site: SiteModel) : NavigationAction() - data class OpenPlan(val site: SiteModel) : NavigationAction() - data class OpenPosts(val site: SiteModel) : NavigationAction() - data class OpenPages(val site: SiteModel) : NavigationAction() - data class OpenAdmin(val site: SiteModel) : NavigationAction() - data class OpenPeople(val site: SiteModel) : NavigationAction() - data class OpenSharing(val site: SiteModel) : NavigationAction() - data class OpenSiteSettings(val site: SiteModel) : NavigationAction() - data class OpenThemes(val site: SiteModel) : NavigationAction() - data class OpenPlugins(val site: SiteModel) : NavigationAction() - data class OpenMedia(val site: SiteModel) : NavigationAction() - data class OpenComments(val site: SiteModel) : NavigationAction() - object StartWPComLoginForJetpackStats : NavigationAction() - data class OpenStats(val site: SiteModel) : NavigationAction() - data class ConnectJetpackForStats(val site: SiteModel) : NavigationAction() - data class OpenJetpackSettings(val site: SiteModel) : NavigationAction() - } - companion object { const val TAG_ADD_SITE_ICON_DIALOG = "TAG_ADD_SITE_ICON_DIALOG" const val TAG_CHANGE_SITE_ICON_DIALOG = "TAG_CHANGE_SITE_ICON_DIALOG" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt new file mode 100644 index 000000000000..fdcc0e7543ac --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt @@ -0,0 +1,49 @@ +package org.wordpress.android.ui.mysite + +import com.wordpress.stories.compose.frame.StorySaveEvents.StorySaveResult +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.PagePostCreationSourcesDetail +import org.wordpress.android.util.UriWrapper + +sealed class SiteNavigationAction { + object OpenMeScreen : SiteNavigationAction() + data class OpenSite(val site: SiteModel) : SiteNavigationAction() + data class OpenSitePicker(val site: SiteModel) : SiteNavigationAction() + data class OpenMediaPicker(val site: SiteModel) : SiteNavigationAction() + data class OpenCropActivity(val imageUri: UriWrapper) : SiteNavigationAction() + data class OpenActivityLog(val site: SiteModel) : SiteNavigationAction() + data class OpenScan(val site: SiteModel) : SiteNavigationAction() + data class OpenPlan(val site: SiteModel) : SiteNavigationAction() + data class OpenPosts(val site: SiteModel) : SiteNavigationAction() + data class OpenPages(val site: SiteModel) : SiteNavigationAction() + data class OpenAdmin(val site: SiteModel) : SiteNavigationAction() + data class OpenPeople(val site: SiteModel) : SiteNavigationAction() + data class OpenSharing(val site: SiteModel) : SiteNavigationAction() + data class OpenSiteSettings(val site: SiteModel) : SiteNavigationAction() + data class OpenThemes(val site: SiteModel) : SiteNavigationAction() + data class OpenPlugins(val site: SiteModel) : SiteNavigationAction() + data class OpenMedia(val site: SiteModel) : SiteNavigationAction() + data class OpenComments(val site: SiteModel) : SiteNavigationAction() + object StartWPComLoginForJetpackStats : SiteNavigationAction() + data class OpenStats(val site: SiteModel) : SiteNavigationAction() + data class ConnectJetpackForStats(val site: SiteModel) : SiteNavigationAction() + data class OpenJetpackSettings(val site: SiteModel) : SiteNavigationAction() + data class OpenStories(val site: SiteModel, val event: StorySaveResult) : SiteNavigationAction() + data class AddNewStory( + val site: SiteModel, + val source: PagePostCreationSourcesDetail + ) : SiteNavigationAction() + + data class AddNewStoryWithMediaIds( + val site: SiteModel, + val source: PagePostCreationSourcesDetail, + val mediaIds: List + ) : SiteNavigationAction() + + data class AddNewStoryWithMediaUris( + val site: SiteModel, + val source: PagePostCreationSourcesDetail, + val mediaUris: List + ) : SiteNavigationAction() + data class OpenDomainRegistration(val site: SiteModel) : SiteNavigationAction() +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteStoriesHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteStoriesHandler.kt new file mode 100644 index 000000000000..5e085754ea2c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteStoriesHandler.kt @@ -0,0 +1,104 @@ +package org.wordpress.android.ui.mysite + +import android.content.Intent +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.wordpress.stories.compose.frame.FrameSaveNotifier +import com.wordpress.stories.compose.frame.StorySaveEvents +import com.wordpress.stories.compose.frame.StorySaveEvents.StorySaveProcessStart +import com.wordpress.stories.compose.frame.StorySaveEvents.StorySaveResult +import com.wordpress.stories.compose.story.StoryRepository +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker.Stat.STORY_SAVE_ERROR_SNACKBAR_MANAGE_TAPPED +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.PagePostCreationSourcesDetail +import org.wordpress.android.ui.mysite.SiteNavigationAction.OpenStories +import org.wordpress.android.ui.pages.SnackbarMessageHolder +import org.wordpress.android.ui.stories.StoriesMediaPickerResultHandler +import org.wordpress.android.ui.stories.StoriesTrackerHelper +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.ui.utils.UiString.UiStringText +import org.wordpress.android.util.EventBusWrapper +import org.wordpress.android.util.merge +import org.wordpress.android.viewmodel.ContextProvider +import org.wordpress.android.viewmodel.Event +import org.wordpress.android.viewmodel.ResourceProvider +import javax.inject.Inject + +class SiteStoriesHandler +@Inject constructor( + private val eventBusWrapper: EventBusWrapper, + private val resourceProvider: ResourceProvider, + private val storiesTrackerHelper: StoriesTrackerHelper, + private val contextProvider: ContextProvider, + private val selectedSiteRepository: SelectedSiteRepository, + private val storiesMediaPickerResultHandler: StoriesMediaPickerResultHandler +) { + private val _onSnackbar = MutableLiveData>() + val onSnackbar = _onSnackbar as LiveData> + private val _onNavigation = MutableLiveData>() + val onNavigation = merge(_onNavigation, storiesMediaPickerResultHandler.onNavigation) + + init { + eventBusWrapper.register(this) + } + + fun clear() { + eventBusWrapper.unregister(this) + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onEventMainThread(event: StorySaveResult) { + eventBusWrapper.removeStickyEvent(event) + if (!event.isSuccess()) { + // note: no tracking added here as we'll perform tracking in StoryMediaSaveUploadBridge + val errorText = String.format( + resourceProvider.getString(R.string.story_saving_snackbar_finished_with_error), + StoryRepository.getStoryAtIndex(event.storyIndex).title + ) + val snackbarMessage = FrameSaveNotifier.buildSnackbarErrorMessage( + contextProvider.getContext(), + StorySaveEvents.allErrorsInResult(event.frameSaveResult).size, + errorText + ) + + _onSnackbar.postValue( + Event( + SnackbarMessageHolder( + UiStringText(snackbarMessage), + UiStringRes(R.string.story_saving_failed_quick_action_manage) + ) { + val selectedSite = selectedSiteRepository.getSelectedSite() + ?: return@SnackbarMessageHolder + _onNavigation.postValue(Event(OpenStories(selectedSite, event))) + // TODO WPSTORIES add TRACKS: the putExtra described here below for NOTIFICATION_TYPE + // is meant to be used for tracking purposes. Use it! + // TODO add NotificationType.MEDIA_SAVE_ERROR param later when integrating with WPAndroid + // val notificationType = NotificationType.MEDIA_SAVE_ERROR + // notificationIntent.putExtra(ARG_NOTIFICATION_TYPE, notificationType) + storiesTrackerHelper.trackStorySaveResultEvent( + event, + STORY_SAVE_ERROR_SNACKBAR_MANAGE_TAPPED + ) + } + ) + ) + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onStorySaveStart(event: StorySaveProcessStart) { + eventBusWrapper.removeStickyEvent(event) + val snackbarMessage = String.format( + resourceProvider.getString(R.string.story_saving_snackbar_started), + StoryRepository.getStoryAtIndex(event.storyIndex).title + ) + _onSnackbar.postValue(Event(SnackbarMessageHolder(UiStringText(snackbarMessage)))) + } + + fun handleStoriesResult(siteModel: SiteModel, data: Intent, source: PagePostCreationSourcesDetail) { + storiesMediaPickerResultHandler.handleMediaPickerResultForStories(data, siteModel, source) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerActivity.java index 70b3c4eb9646..bdccb881bb0c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerActivity.java @@ -325,12 +325,7 @@ private void doMediaIdsSelected(ArrayList mediaIds, @NonNull PhotoPickerMe if (mediaIds != null && mediaIds.size() > 0) { if (mBrowserType == MediaBrowserType.WP_STORIES_MEDIA_PICKER) { // TODO WPSTORIES add TRACKS (see how it's tracked below? maybe do along the same lines) - Intent data = new Intent() - .putExtra(MediaBrowserActivity.RESULT_IDS, ListUtils.toLongArray(mediaIds)) - .putExtra(ARG_BROWSER_TYPE, mBrowserType) - .putExtra(MediaPickerConstants.EXTRA_MEDIA_SOURCE, source.name()); - setResult(RESULT_OK, data); - finish(); + getPickerFragment().mediaIdsSelectedFromWPMediaPicker(mediaIds); } else { // if user chose a featured image, track image picked event if (mBrowserType == MediaBrowserType.FEATURED_IMAGE_PICKER) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.kt index 1d1e70773da9..6a1290b66ef1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerFragment.kt @@ -473,6 +473,10 @@ class PhotoPickerFragment : Fragment() { viewModel.urisSelectedFromSystemPicker(uris.map { UriWrapper(it) }) } + fun mediaIdsSelectedFromWPMediaPicker(mediaIds: List) { + viewModel.mediaIdsSelectedFromWPMediaPicker(mediaIds) + } + companion object { private const val KEY_LAST_TAPPED_ICON = "last_tapped_icon" private const val KEY_SELECTED_POSITIONS = "selected_positions" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModel.kt index 548ca7d91d30..8617ba0a0044 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModel.kt @@ -1,6 +1,7 @@ package org.wordpress.android.ui.photopicker import android.Manifest.permission +import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineDispatcher @@ -41,6 +42,7 @@ import org.wordpress.android.ui.photopicker.PhotoPickerViewModel.BottomBarUiMode import org.wordpress.android.ui.photopicker.PhotoPickerViewModel.BottomBarUiModel.BottomBar.NONE import org.wordpress.android.ui.photopicker.PhotoPickerViewModel.PopupMenuUiModel.PopupMenuItem import org.wordpress.android.ui.posts.editor.media.CopyMediaToAppStorageUseCase +import org.wordpress.android.ui.posts.editor.media.GetMediaModelUseCase import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.ui.utils.UiString.UiStringText @@ -75,7 +77,8 @@ class PhotoPickerViewModel @Inject constructor( private val permissionsHandler: PermissionsHandler, private val tenorFeatureConfig: TenorFeatureConfig, private val resourceProvider: ResourceProvider, - private val copyMediaToAppStorageUseCase: CopyMediaToAppStorageUseCase + private val copyMediaToAppStorageUseCase: CopyMediaToAppStorageUseCase, + private val getMediaModelUseCase: GetMediaModelUseCase ) : ScopedViewModel(mainDispatcher) { private val _navigateToPreview = MutableLiveData>() private val _onInsert = MutableLiveData>>() @@ -478,6 +481,18 @@ class PhotoPickerViewModel @Inject constructor( } fun urisSelectedFromSystemPicker(uris: List) { + copySelectedUrisLocally(uris) + } + + fun mediaIdsSelectedFromWPMediaPicker(mediaIds: List) { + launch { + val mediaModels = getMediaModelUseCase + .loadMediaByRemoteId(requireNotNull(site), mediaIds) + copySelectedUrisLocally(mediaModels.map { UriWrapper(Uri.parse(it.url)) }) + } + } + + fun copySelectedUrisLocally(uris: List) { launch { _showProgressDialog.value = ProgressDialogUiModel.Visible(R.string.uploading_title) { _showProgressDialog.postValue(ProgressDialogUiModel.Hidden) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java index efe1d3381d88..877f93f2b8ae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.java @@ -144,6 +144,7 @@ import org.wordpress.android.ui.posts.PostEditorAnalyticsSession.Editor; import org.wordpress.android.ui.posts.PostEditorAnalyticsSession.Outcome; import org.wordpress.android.ui.posts.RemotePreviewLogicHelper.PreviewLogicOperationResult; +import org.wordpress.android.ui.posts.RemotePreviewLogicHelper.RemotePreviewType; import org.wordpress.android.ui.posts.editor.EditorActionsProvider; import org.wordpress.android.ui.posts.editor.EditorPhotoPicker; import org.wordpress.android.ui.posts.editor.EditorPhotoPickerListener; @@ -1321,12 +1322,34 @@ private boolean handleBackPressed() { } else if (mEditorPhotoPicker.isPhotoPickerShowing()) { mEditorPhotoPicker.hidePhotoPicker(); } else { - savePostAndOptionallyFinish(true, false); + performWhenNoStoriesBeingSaved(new DoWhenNoStoriesBeingSavedCallback() { + @Override public void doWhenNoStoriesBeingSaved() { + savePostAndOptionallyFinish(true, false); + } + }); } return true; } + interface DoWhenNoStoriesBeingSavedCallback { + void doWhenNoStoriesBeingSaved(); + } + + private void performWhenNoStoriesBeingSaved(DoWhenNoStoriesBeingSavedCallback callback) { + if (mWPStoriesFeatureConfig.isEnabled()) { + if (mStoriesEventListener.getStoriesSavingInProgress().isEmpty()) { + callback.doWhenNoStoriesBeingSaved(); + } else { + // Oops! A story is still being saved, let's wait + ToastUtils.showToast(EditPostActivity.this, + getString(R.string.toast_edit_story_update_in_progress_title)); + } + } else { + callback.doWhenNoStoriesBeingSaved(); + } + } + private RemotePreviewLogicHelper.RemotePreviewHelperFunctions getEditPostActivityStrategyFunctions() { return new RemotePreviewLogicHelper.RemotePreviewHelperFunctions() { @Override @@ -1498,7 +1521,7 @@ private boolean performSecondaryAction() { case PUBLISH_NOW: mAnalyticsTrackerWrapper.track(Stat.EDITOR_POST_PUBLISH_TAPPED); mPublishPostImmediatelyUseCase.updatePostToPublishImmediately(mEditPostRepository, mIsNewPost); - showPrepublishingNudgeBottomSheet(); + checkNoStorySaveOperationInProgressAndShowPrepublishingNudgeBottomSheet(); return true; case NONE: throw new IllegalStateException("Switch in `secondaryAction` shouldn't go through the NONE case"); @@ -1610,12 +1633,12 @@ private void performPrimaryAction() { switch (getPrimaryAction()) { case PUBLISH_NOW: mAnalyticsTrackerWrapper.track(Stat.EDITOR_POST_PUBLISH_TAPPED); - showPrepublishingNudgeBottomSheet(); + checkNoStorySaveOperationInProgressAndShowPrepublishingNudgeBottomSheet(); return; case UPDATE: case SCHEDULE: case SUBMIT_FOR_REVIEW: - showPrepublishingNudgeBottomSheet(); + checkNoStorySaveOperationInProgressAndShowPrepublishingNudgeBottomSheet(); return; case SAVE: uploadPost(false); @@ -2013,6 +2036,14 @@ private void setupPrepublishingBottomSheetRunnable() { }; } + private void checkNoStorySaveOperationInProgressAndShowPrepublishingNudgeBottomSheet() { + performWhenNoStoriesBeingSaved(new DoWhenNoStoriesBeingSavedCallback() { + @Override public void doWhenNoStoriesBeingSaved() { + showPrepublishingNudgeBottomSheet(); + } + }); + } + private void showPrepublishingNudgeBottomSheet() { mViewPager.setCurrentItem(PAGE_CONTENT); ActivityUtils.hideKeyboard(this); @@ -2305,7 +2336,7 @@ private GutenbergPropsBuilder getGutenbergPropsBuilder() { boolean unsupportedBlockEditorSwitch = !mIsJetpackSsoEnabled && "gutenberg".equals(mSite.getWebEditor()); return new GutenbergPropsBuilder( - mWPStoriesFeatureConfig.isEnabled(), + mWPStoriesFeatureConfig.isEnabled() && SiteUtils.supportsStoriesFeature(mSite), enableMentions, isUnsupportedBlockEditorEnabled, unsupportedBlockEditorSwitch, @@ -3377,7 +3408,10 @@ public void onPostChanged(OnPostChanged event) { AppLog.e(T.POSTS, "REMOTE_AUTO_SAVE_POST failed: " + event.error.type + " - " + event.error.message); } mEditPostRepository.loadPostByLocalPostId(mEditPostRepository.getId()); - mEditPostRepository.replace(postModel -> handleRemoteAutoSave(event.isError(), postModel)); + if (isRemotePreviewingFromEditor()) { + handleRemotePreviewUploadResult(event.isError(), + RemotePreviewType.REMOTE_PREVIEW_WITH_REMOTE_AUTO_SAVE); + } } } @@ -3403,7 +3437,7 @@ private boolean isRemoteAutoSaveError() { } @Nullable - private PostModel handleRemoteAutoSave(boolean isError, PostModel post) { + private void handleRemotePreviewUploadResult(boolean isError, RemotePreviewLogicHelper.RemotePreviewType param) { // We are in the process of remote previewing a post from the editor if (!isError && isUploadingPostForPreview()) { // We were uploading post for preview and we got no error: @@ -3412,19 +3446,16 @@ private PostModel handleRemoteAutoSave(boolean isError, PostModel post) { ActivityLauncher.previewPostOrPageForResult( EditPostActivity.this, mSite, - post, - mPostLoadingState == PostLoadingState.UPLOADING_FOR_PREVIEW - ? RemotePreviewLogicHelper.RemotePreviewType.REMOTE_PREVIEW - : RemotePreviewLogicHelper.RemotePreviewType.REMOTE_PREVIEW_WITH_REMOTE_AUTO_SAVE + mEditPostRepository.getPost(), + param ); - updatePostLoadingAndDialogState(PostLoadingState.PREVIEWING, post); + updatePostLoadingAndDialogState(PostLoadingState.PREVIEWING, mEditPostRepository.getPost()); } else if (isError || isRemoteAutoSaveError()) { // We got an error from the uploading or from the remote auto save of a post: show snackbar error updatePostLoadingAndDialogState(PostLoadingState.NONE); mUploadUtilsWrapper.showSnackbarError(findViewById(R.id.editor_activity), getString(R.string.remote_preview_operation_error)); } - return post; } @SuppressWarnings("unused") @@ -3444,7 +3475,8 @@ public void onPostUploaded(OnPostUploaded event) { }); } } else { - mEditPostRepository.set(() -> handleRemoteAutoSave(event.isError(), post)); + mEditPostRepository.set(() -> post); + handleRemotePreviewUploadResult(event.isError(), RemotePreviewType.REMOTE_PREVIEW); } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt index ae29191808cf..d8e18537455b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt @@ -34,6 +34,7 @@ import org.wordpress.android.util.ToastUtils.Duration import org.wordpress.android.viewmodel.helpers.ToastMessageHolder import org.wordpress.android.widgets.PostListButtonType import org.wordpress.android.widgets.PostListButtonType.BUTTON_CANCEL_PENDING_AUTO_UPLOAD +import org.wordpress.android.widgets.PostListButtonType.BUTTON_COPY import org.wordpress.android.widgets.PostListButtonType.BUTTON_DELETE import org.wordpress.android.widgets.PostListButtonType.BUTTON_DELETE_PERMANENTLY import org.wordpress.android.widgets.PostListButtonType.BUTTON_EDIT @@ -67,7 +68,8 @@ class PostActionHandler( private val checkNetworkConnection: () -> Boolean, private val showSnackbar: (SnackbarMessageHolder) -> Unit, private val showToast: (ToastMessageHolder) -> Unit, - private val triggerPreviewStateUpdate: (PostListRemotePreviewState, PostInfoType) -> Unit + private val triggerPreviewStateUpdate: (PostListRemotePreviewState, PostInfoType) -> Unit, + private val copyPost: (SiteModel, PostModel, Boolean) -> Unit ) { private val criticalPostActionTracker = CriticalPostActionTracker(onStateChanged = { invalidateList.invoke() @@ -112,6 +114,7 @@ class PostActionHandler( else -> trashPost(post) } } + BUTTON_COPY -> copyPost(site, post, true) BUTTON_DELETE, BUTTON_DELETE_PERMANENTLY -> { postListDialogHelper.showDeletePostConfirmationDialog(post) } @@ -168,6 +171,13 @@ class PostActionHandler( triggerPostUploadAction.invoke(PublishPost(dispatcher, site, post)) } + fun resolveConflictsAndEditPost(localPostId: Int) { + val post = postStore.getPostByLocalPostId(localPostId) + if (post != null) { + performChecksAndEdit(site, post) + } + } + fun moveTrashedPostToDraft(localPostId: Int) { val post = postStore.getPostByLocalPostId(localPostId) if (post != null) { @@ -198,7 +208,9 @@ class PostActionHandler( showSnackbar.invoke(snackBarHolder) } - private fun editPostButtonAction(site: SiteModel, post: PostModel) { + private fun editPostButtonAction(site: SiteModel, post: PostModel) = performChecksAndEdit(site, post) + + private fun performChecksAndEdit(site: SiteModel, post: PostModel) { // first of all, check whether this post is in Conflicted state with a more recent remote version if (doesPostHaveUnhandledConflict.invoke(post)) { postListDialogHelper.showConflictedPostResolutionDialog(post) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListActionTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListActionTracker.kt index 3002502577a9..32b7c13b95da 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListActionTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListActionTracker.kt @@ -6,6 +6,7 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.util.analytics.AnalyticsUtils import org.wordpress.android.widgets.PostListButtonType import org.wordpress.android.widgets.PostListButtonType.BUTTON_CANCEL_PENDING_AUTO_UPLOAD +import org.wordpress.android.widgets.PostListButtonType.BUTTON_COPY import org.wordpress.android.widgets.PostListButtonType.BUTTON_DELETE import org.wordpress.android.widgets.PostListButtonType.BUTTON_DELETE_PERMANENTLY import org.wordpress.android.widgets.PostListButtonType.BUTTON_EDIT @@ -39,6 +40,7 @@ fun trackPostListAction(site: SiteModel, buttonType: PostListButtonType, postDat BUTTON_PREVIEW -> "preview" BUTTON_STATS -> "stats" BUTTON_TRASH -> "trash" + BUTTON_COPY -> "copy" BUTTON_DELETE, BUTTON_DELETE_PERMANENTLY -> "delete" BUTTON_PUBLISH -> "publish" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt index 5a9136a3fee2..c9a6f86982cc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListDialogHelper.kt @@ -18,6 +18,7 @@ private const val CONFIRM_TRASH_POST_WITH_UNSAVED_CHANGES_DIALOG_TAG = private const val CONFIRM_ON_CONFLICT_LOAD_REMOTE_POST_DIALOG_TAG = "CONFIRM_ON_CONFLICT_LOAD_REMOTE_POST_DIALOG_TAG" private const val CONFIRM_ON_AUTOSAVE_REVISION_DIALOG_TAG = "CONFIRM_ON_AUTOSAVE_REVISION_DIALOG_TAG" private const val CONFIRM_SYNC_SCHEDULED_POST_DIALOG_TAG = "CONFIRM_SYNC_SCHEDULED_POST_DIALOG_TAG" +private const val COPY_CONFLICT_DIALOG_TAG = "COPY_CONFLICT_DIALOG_TAG" private const val POST_TYPE = "post_type" /** @@ -37,6 +38,7 @@ class PostListDialogHelper( private var localPostIdForConflictResolutionDialog: Int? = null private var localPostIdForAutosaveRevisionResolutionDialog: Int? = null private var localPostIdForScheduledPostSyncDialog: Int? = null + private var localPostIdForCopyConflictDialog: Int? = null fun showMoveTrashedPostToDraftDialog(post: PostModel) { val dialogHolder = DialogHolder( @@ -137,6 +139,18 @@ class PostListDialogHelper( showDialog.invoke(dialogHolder) } + fun showCopyConflictDialog(post: PostModel) { + val dialogHolder = DialogHolder( + tag = COPY_CONFLICT_DIALOG_TAG, + title = UiStringRes(R.string.dialog_confirm_copy_local_title), + message = UiStringRes(R.string.dialog_confirm_copy_local_message), + positiveButton = UiStringRes(R.string.dialog_confirm_copy_local_edit_button), + negativeButton = UiStringRes(R.string.dialog_confirm_copy_local_copy_button) + ) + localPostIdForCopyConflictDialog = post.id + showDialog.invoke(dialogHolder) + } + fun onPositiveClickedForBasicDialog( instanceTag: String, trashPostWithLocalChanges: (Int) -> Unit, @@ -145,7 +159,8 @@ class PostListDialogHelper( publishPost: (Int) -> Unit, updateConflictedPostWithRemoteVersion: (Int) -> Unit, editRestoredAutoSavePost: (Int) -> Unit, - moveTrashedPostToDraft: (Int) -> Unit + moveTrashedPostToDraft: (Int) -> Unit, + resolveConflictsAndEditPost: (Int) -> Unit ) { when (instanceTag) { CONFIRM_DELETE_POST_DIALOG_TAG -> localPostIdForDeleteDialog?.let { @@ -181,6 +196,10 @@ class PostListDialogHelper( UNPUBLISHED_REVISION_DIALOG_LOAD_UNPUBLISHED_VERSION_CLICKED, mapOf(POST_TYPE to "post")) } + COPY_CONFLICT_DIALOG_TAG -> localPostIdForCopyConflictDialog?.let { + localPostIdForCopyConflictDialog = null + resolveConflictsAndEditPost(it) + } else -> throw IllegalArgumentException("Dialog's positive button click is not handled: $instanceTag") } } @@ -188,7 +207,8 @@ class PostListDialogHelper( fun onNegativeClickedForBasicDialog( instanceTag: String, updateConflictedPostWithLocalVersion: (Int) -> Unit, - editLocalPost: (Int) -> Unit + editLocalPost: (Int) -> Unit, + copyLocalPost: (Int) -> Unit ) { when (instanceTag) { CONFIRM_DELETE_POST_DIALOG_TAG -> localPostIdForDeleteDialog = null @@ -207,6 +227,10 @@ class PostListDialogHelper( ) } CONFIRM_RESTORE_TRASHED_POST_DIALOG_TAG -> localPostIdForMoveTrashedPostToDraftDialog = null + COPY_CONFLICT_DIALOG_TAG -> localPostIdForCopyConflictDialog?.let { + localPostIdForCopyConflictDialog = null + copyLocalPost(it) + } else -> throw IllegalArgumentException("Dialog's negative button click is not handled: $instanceTag") } } @@ -214,7 +238,8 @@ class PostListDialogHelper( fun onDismissByOutsideTouchForBasicDialog( instanceTag: String, updateConflictedPostWithLocalVersion: (Int) -> Unit, - editLocalPost: (Int) -> Unit + editLocalPost: (Int) -> Unit, + copyLocalPost: (Int) -> Unit ) { // Cancel and outside touch dismiss works the same way for all, except for conflict and autosave revision // dialogs, for which tapping outside and actively tapping the "edit local" have different meanings @@ -223,7 +248,8 @@ class PostListDialogHelper( onNegativeClickedForBasicDialog( instanceTag = instanceTag, updateConflictedPostWithLocalVersion = updateConflictedPostWithLocalVersion, - editLocalPost = editLocalPost + editLocalPost = editLocalPost, + copyLocalPost = copyLocalPost ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt index 547c39d97e4b..d8ec5fe96116 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt @@ -203,8 +203,28 @@ class PostListMainViewModel @Inject constructor( checkNetworkConnection = this::checkNetworkConnection, showSnackbar = { _snackBarMessage.postValue(it) }, showToast = { _toastMessage.postValue(it) }, - triggerPreviewStateUpdate = this::updatePreviewAndDialogState + triggerPreviewStateUpdate = this::updatePreviewAndDialogState, + copyPost = this::copyPost + ) + } + + fun copyPost(site: SiteModel, postToCopy: PostModel, performChecks: Boolean = false) { + if (performChecks && (postConflictResolver.doesPostHaveUnhandledConflict(postToCopy) || + postConflictResolver.hasUnhandledAutoSave(postToCopy))) { + postListDialogHelper.showCopyConflictDialog(postToCopy) + return + } + val post = postStore.instantiatePostModel( + site, + false, + postToCopy.title, + postToCopy.content, + PostStatus.DRAFT.toString(), + postToCopy.categoryIdList, + postToCopy.postFormat, + true ) + _postListAction.postValue(PostListAction.EditPost(site, post, loadAutoSaveRevision = false)) } /** @@ -437,6 +457,15 @@ class PostListMainViewModel @Inject constructor( } } + private fun copyLocalPost(localPostId: Int) { + val post = postStore.getPostByLocalPostId(localPostId) + if (post != null) { + copyPost(site, post) + } else { + _snackBarMessage.value = SnackbarMessageHolder(UiStringRes(R.string.error_post_does_not_exist)) + } + } + // BasicFragmentDialog Events fun onPositiveClickedForBasicDialog(instanceTag: String) { @@ -448,7 +477,8 @@ class PostListMainViewModel @Inject constructor( publishPost = postActionHandler::publishPost, updateConflictedPostWithRemoteVersion = postConflictResolver::updateConflictedPostWithRemoteVersion, editRestoredAutoSavePost = this::editRestoredAutoSavePost, - moveTrashedPostToDraft = postActionHandler::moveTrashedPostToDraft + moveTrashedPostToDraft = postActionHandler::moveTrashedPostToDraft, + resolveConflictsAndEditPost = postActionHandler::resolveConflictsAndEditPost ) } @@ -456,7 +486,8 @@ class PostListMainViewModel @Inject constructor( postListDialogHelper.onNegativeClickedForBasicDialog( instanceTag = instanceTag, updateConflictedPostWithLocalVersion = postConflictResolver::updateConflictedPostWithLocalVersion, - editLocalPost = this::editLocalPost + editLocalPost = this::editLocalPost, + copyLocalPost = this::copyLocalPost ) } @@ -464,7 +495,8 @@ class PostListMainViewModel @Inject constructor( postListDialogHelper.onDismissByOutsideTouchForBasicDialog( instanceTag = instanceTag, updateConflictedPostWithLocalVersion = postConflictResolver::updateConflictedPostWithLocalVersion, - editLocalPost = this::editLocalPost + editLocalPost = this::editLocalPost, + copyLocalPost = this::copyLocalPost ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StoriesEventListener.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StoriesEventListener.kt index b8559f5a8bf0..d718edcabdca 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StoriesEventListener.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/StoriesEventListener.kt @@ -13,7 +13,9 @@ import com.wordpress.stories.compose.frame.StorySaveEvents.FrameSaveCompleted import com.wordpress.stories.compose.frame.StorySaveEvents.FrameSaveFailed import com.wordpress.stories.compose.frame.StorySaveEvents.FrameSaveProgress import com.wordpress.stories.compose.frame.StorySaveEvents.FrameSaveStart +import com.wordpress.stories.compose.frame.StorySaveEvents.StorySaveProcessStart import com.wordpress.stories.compose.frame.StorySaveEvents.StorySaveResult +import com.wordpress.stories.compose.story.StoryIndex import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.wordpress.android.R @@ -58,6 +60,8 @@ class StoriesEventListener @Inject constructor( private lateinit var site: SiteModel private lateinit var editPostRepository: EditPostRepository private var storySaveMediaListener: StorySaveMediaListener? = null + var storiesSavingInProgress = HashSet() + private set @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) private fun onCreate() { @@ -168,6 +172,7 @@ class StoriesEventListener @Inject constructor( @Subscribe(threadMode = ThreadMode.MAIN) fun onStorySaveProcessFinished(event: StorySaveResult) { + storiesSavingInProgress.remove(event.storyIndex) if (!lifecycle.currentState.isAtLeast(CREATED)) { return } @@ -179,6 +184,11 @@ class StoriesEventListener @Inject constructor( } } + @Subscribe(threadMode = ThreadMode.MAIN) + fun onStorySaveStart(event: StorySaveProcessStart) { + storiesSavingInProgress.add(event.storyIndex) + } + // Editor load / cancel events fun onRequestMediaFilesEditorLoad( activity: Activity, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java index 392c19c4e85e..44425bec409c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java @@ -48,10 +48,10 @@ public GalleryBlockProcessor(String localId, MediaFile mediaFile, String siteUrl Element parent = targetImg.parent(); if (parent != null && parent.is("a") && mLinkTo != null) { switch (mLinkTo) { - case "media": + case "file": parent.attr("href", mRemoteUrl); break; - case "attachment": + case "post": parent.attr("href", mAttachmentPageUrl); break; default: diff --git a/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeWebViewFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeWebViewFragment.java index 4abb649881cd..2a284e76a5e4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeWebViewFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeWebViewFragment.java @@ -6,7 +6,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -26,6 +25,7 @@ import org.wordpress.android.ui.WPWebViewActivity; import org.wordpress.android.ui.publicize.PublicizeConstants.ConnectAction; import org.wordpress.android.util.WebViewUtils; +import org.wordpress.android.util.helpers.WebChromeClientWithVideoPoster; import javax.inject.Inject; @@ -192,9 +192,9 @@ public void onPageFinished(WebView view, String url) { } } - private class PublicizeWebChromeClient extends WebChromeClient { + private class PublicizeWebChromeClient extends WebChromeClientWithVideoPoster { PublicizeWebChromeClient() { - super(); + super(mWebView, R.drawable.media_movieclip); } @Override diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderVideoViewerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderVideoViewerActivity.java index 979c045357ac..49437736da92 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderVideoViewerActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderVideoViewerActivity.java @@ -3,7 +3,6 @@ import android.graphics.Color; import android.os.Bundle; import android.view.View; -import android.webkit.WebChromeClient; import android.webkit.WebView; import android.widget.ProgressBar; @@ -12,6 +11,7 @@ import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.ui.LocaleAwareActivity; +import org.wordpress.android.util.helpers.WebChromeClientWithVideoPoster; /** * Full screen landscape video player for the reader @@ -33,7 +33,7 @@ public void onCreate(Bundle savedInstanceState) { mWebView.getSettings().setJavaScriptEnabled(true); mWebView.getSettings().setUserAgentString(WordPress.getUserAgent()); - mWebView.setWebChromeClient(new WebChromeClient() { + mWebView.setWebChromeClient(new WebChromeClientWithVideoPoster(mWebView, R.drawable.media_movieclip) { public void onProgressChanged(WebView view, int progress) { if (progress == 100) { mProgress.setVisibility(View.GONE); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/ReaderCommentsFollowUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/ReaderCommentsFollowUseCase.kt index 96218fc8d5f6..9b204c0070bc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/ReaderCommentsFollowUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/usecases/ReaderCommentsFollowUseCase.kt @@ -13,7 +13,6 @@ import org.wordpress.android.ui.reader.usecases.ReaderCommentsFollowUseCase.Anal import org.wordpress.android.ui.reader.usecases.ReaderCommentsFollowUseCase.AnalyticsFollowCommentsGenericError.NO_NETWORK import org.wordpress.android.ui.reader.usecases.ReaderCommentsFollowUseCase.FollowCommentsState.UserNotAuthenticated import org.wordpress.android.ui.reader.utils.PostSubscribersApiCallsProvider -import org.wordpress.android.ui.reader.utils.PostSubscribersApiCallsProvider.PostSubscribersCallResult import org.wordpress.android.ui.reader.utils.PostSubscribersApiCallsProvider.PostSubscribersCallResult.Failure import org.wordpress.android.ui.reader.utils.PostSubscribersApiCallsProvider.PostSubscribersCallResult.Success import org.wordpress.android.ui.utils.UiString @@ -22,7 +21,6 @@ import org.wordpress.android.ui.utils.UiString.UiStringText import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper import javax.inject.Inject -import kotlin.coroutines.suspendCoroutine class ReaderCommentsFollowUseCase @Inject constructor( private val networkUtilsWrapper: NetworkUtilsWrapper, @@ -44,16 +42,12 @@ class ReaderCommentsFollowUseCase @Inject constructor( if (!networkUtilsWrapper.isNetworkAvailable()) { emit(FollowCommentsState.Failure(blogId, postId, UiStringRes(R.string.error_network_connection))) } else { - val canFollowComments: Boolean = suspendCoroutine { continuation -> - postSubscribersApiCallsProvider.getCanFollowComments(blogId, continuation) - } + val canFollowComments = postSubscribersApiCallsProvider.getCanFollowComments(blogId) if (!canFollowComments) { emit(FollowCommentsState.FollowCommentsNotAllowed) } else { - val status: PostSubscribersCallResult = suspendCoroutine { continuation -> - postSubscribersApiCallsProvider.getMySubscriptionToPost(blogId, postId, continuation) - } + val status = postSubscribersApiCallsProvider.getMySubscriptionToPost(blogId, postId) when (status) { is Success -> { @@ -90,12 +84,10 @@ class ReaderCommentsFollowUseCase @Inject constructor( emit(FollowCommentsState.Failure(blogId, postId, UiStringRes(R.string.error_network_connection))) properties.addFollowActionResult(ERROR, NO_NETWORK.errorMessage) } else { - val status: PostSubscribersCallResult = suspendCoroutine { continuation -> - if (subscribe) { - postSubscribersApiCallsProvider.subscribeMeToPost(blogId, postId, continuation) - } else { - postSubscribersApiCallsProvider.unsubscribeMeFromPost(blogId, postId, continuation) - } + val status = if (subscribe) { + postSubscribersApiCallsProvider.subscribeMeToPost(blogId, postId) + } else { + postSubscribersApiCallsProvider.unsubscribeMeFromPost(blogId, postId) } when (status) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/PostSubscribersApiCallsProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/PostSubscribersApiCallsProvider.kt index f9ad6610a630..0bfc83a8f6ba 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/PostSubscribersApiCallsProvider.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/utils/PostSubscribersApiCallsProvider.kt @@ -13,13 +13,13 @@ import org.wordpress.android.util.AppLog.T import org.wordpress.android.util.VolleyUtils import org.wordpress.android.viewmodel.ContextProvider import javax.inject.Inject -import kotlin.coroutines.Continuation import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class PostSubscribersApiCallsProvider @Inject constructor( private val contextProvider: ContextProvider ) { - fun getCanFollowComments(blogId: Long, cont: Continuation) { + suspend fun getCanFollowComments(blogId: Long): Boolean = suspendCoroutine { cont -> val endPointPath = "/sites/$blogId/" val listener = Listener { jsonObject -> @@ -45,7 +45,10 @@ class PostSubscribersApiCallsProvider @Inject constructor( ) } - fun getMySubscriptionToPost(blogId: Long, postId: Long, cont: Continuation) { + suspend fun getMySubscriptionToPost( + blogId: Long, + postId: Long + ): PostSubscribersCallResult = suspendCoroutine { cont -> val endPointPath = "/sites/$blogId/posts/$postId/subscribers/mine" val listener = Listener { jsonObject -> @@ -68,7 +71,7 @@ class PostSubscribersApiCallsProvider @Inject constructor( ) } - fun subscribeMeToPost(blogId: Long, postId: Long, cont: Continuation) { + suspend fun subscribeMeToPost(blogId: Long, postId: Long): PostSubscribersCallResult = suspendCoroutine { cont -> val endPointPath = "/sites/$blogId/posts/$postId/subscribers/new" val listener = Listener { jsonObject -> @@ -91,7 +94,10 @@ class PostSubscribersApiCallsProvider @Inject constructor( ) } - fun unsubscribeMeFromPost(blogId: Long, postId: Long, cont: Continuation) { + suspend fun unsubscribeMeFromPost( + blogId: Long, + postId: Long + ): PostSubscribersCallResult = suspendCoroutine { cont -> val endPointPath = "/sites/$blogId/posts/$postId/subscribers/mine/delete" val listener = Listener { jsonObject -> diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderWebView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderWebView.java index ed775561bcbb..5e23eebeae62 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderWebView.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderWebView.java @@ -11,17 +11,18 @@ import android.view.View; import android.view.ViewGroup; import android.webkit.CookieManager; -import android.webkit.WebChromeClient; import android.webkit.WebResourceResponse; import android.webkit.WebView; import android.webkit.WebViewClient; +import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.fluxc.store.AccountStore; import org.wordpress.android.ui.WPWebView; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.UrlUtils; import org.wordpress.android.util.WPUrlUtils; +import org.wordpress.android.util.helpers.WebChromeClientWithVideoPoster; import java.io.IOException; import java.net.HttpURLConnection; @@ -260,6 +261,8 @@ private static class ReaderWebViewClient extends WebViewClient { mReaderWebView = readerWebView; } + + @Override public void onPageFinished(WebView view, String url) { if (mReaderWebView.hasPageFinishedListener()) { @@ -312,12 +315,13 @@ public WebResourceResponse shouldInterceptRequest(WebView view, String url) { } } - private static class ReaderWebChromeClient extends WebChromeClient { + private static class ReaderWebChromeClient extends WebChromeClientWithVideoPoster { private final ReaderWebView mReaderWebView; private View mCustomView; private CustomViewCallback mCustomViewCallback; ReaderWebChromeClient(ReaderWebView readerWebView) { + super(readerWebView, R.drawable.media_movieclip); if (readerWebView == null) { throw new IllegalArgumentException("ReaderWebChromeClient requires readerWebView"); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt index 38d2bf9ab227..bd63a7e3b544 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsFragment.kt @@ -128,7 +128,7 @@ class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { private fun updateTitleVisibility(visible: Boolean) { val actionBar = (requireActivity() as? AppCompatActivity)?.supportActionBar - actionBar?.setDisplayShowTitleEnabled(displayUtils.isLandscape() || visible) // Always visible in landscape + actionBar?.setDisplayShowTitleEnabled(displayUtils.isLandscapeBySize() || visible) } private fun getSegmentIdFromArguments(): Long? = arguments?.getLong(EXTRA_SEGMENT_ID) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt index 0b243b05ab32..c0561babc406 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationTracker.kt @@ -1,6 +1,7 @@ package org.wordpress.android.ui.sitecreation.misc import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.ui.sitecreation.theme.defaultTemplateSlug import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import java.util.Locale import javax.inject.Inject @@ -45,16 +46,37 @@ class SiteCreationTracker @Inject constructor(val tracker: AnalyticsTrackerWrapp ) } - fun trackPreviewLoading() { - tracker.track(AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SUCCESS_LOADING) + fun trackPreviewLoading(template: String?) { + if (template == null || template == defaultTemplateSlug) { + tracker.track(AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SUCCESS_LOADING) + } else { + tracker.track( + AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SUCCESS_LOADING, + mapOf("template" to template) + ) + } } - fun trackPreviewWebviewShown() { - tracker.track(AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SUCCESS_PREVIEW_VIEWED) + fun trackPreviewWebviewShown(template: String?) { + if (template == null || template == defaultTemplateSlug) { + tracker.track(AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SUCCESS_PREVIEW_VIEWED) + } else { + tracker.track( + AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SUCCESS_PREVIEW_VIEWED, + mapOf("template" to template) + ) + } } - fun trackPreviewWebviewFullyLoaded() { - tracker.track(AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SUCCESS_PREVIEW_LOADED) + fun trackPreviewWebviewFullyLoaded(template: String?) { + if (template == null || template == defaultTemplateSlug) { + tracker.track(AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SUCCESS_PREVIEW_LOADED) + } else { + tracker.track( + AnalyticsTracker.Stat.ENHANCED_SITE_CREATION_SUCCESS_PREVIEW_LOADED, + mapOf("template" to template) + ) + } } fun trackPreviewOkButtonTapped() { @@ -65,8 +87,12 @@ class SiteCreationTracker @Inject constructor(val tracker: AnalyticsTrackerWrapp * This stat is part of a funnel that provides critical information. Before * making ANY modification to this stat please refer to: p4qSXL-35X-p2 */ - fun trackSiteCreated() { - tracker.track(AnalyticsTracker.Stat.SITE_CREATED) + fun trackSiteCreated(template: String?) { + if (template == null || template == defaultTemplateSlug) { + tracker.track(AnalyticsTracker.Stat.SITE_CREATED) + } else { + tracker.track(AnalyticsTracker.Stat.SITE_CREATED, mapOf("template" to template)) + } } fun trackFlowExited() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt index 8a74e13928e6..a6f758b91cd9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModel.kt @@ -256,7 +256,7 @@ class SitePreviewViewModel @Inject constructor( } private fun startPreLoadingWebView(skipDelay: Boolean = false) { - tracker.trackPreviewLoading() + tracker.trackPreviewLoading(siteCreationState.siteDesign) launch { if (!skipDelay) { /** @@ -271,7 +271,7 @@ class SitePreviewViewModel @Inject constructor( */ withContext(mainDispatcher) { if (uiState.value !is SitePreviewContentUiState) { - tracker.trackPreviewWebviewShown() + tracker.trackPreviewWebviewShown(siteCreationState.siteDesign) updateUiState(SitePreviewLoadingShimmerState(createSitePreviewData())) } } @@ -288,10 +288,7 @@ class SitePreviewViewModel @Inject constructor( fun onUrlLoaded() { if (!webviewFullyLoadedTracked) { webviewFullyLoadedTracked = true - tracker.trackPreviewWebviewFullyLoaded() - } - if (uiState.value is SitePreviewFullscreenProgressUiState) { - tracker.trackPreviewWebviewShown() + tracker.trackPreviewWebviewFullyLoaded(siteCreationState.siteDesign) } /** * Update the ui state if the loading or error screen is being shown. diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceManager.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceManager.kt index 26558e6b453c..9b013d6f224b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceManager.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceManager.kt @@ -84,7 +84,7 @@ class SiteCreationServiceManager @Inject constructor( updateServiceState(SUCCESS, newSiteRemoteId) // This stat is part of a funnel that provides critical information. Before // making ANY modification to this stat please refer to: p4qSXL-35X-p2 - tracker.trackSiteCreated() + tracker.trackSiteCreated(siteData.siteDesign) } FAILURE -> { val currentState = serviceListener.getCurrentState() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerFragment.kt index 2a73eaab334b..36bb5d23585d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/HomePagePickerFragment.kt @@ -114,7 +114,7 @@ class HomePagePickerFragment : Fragment() { } private fun setupUi() { - title?.setVisible(isPhoneLandscape()) + title?.visibility = if (isPhoneLandscape()) View.VISIBLE else View.INVISIBLE header?.setText(R.string.hpp_title) description?.setText(R.string.hpp_subtitle) } @@ -148,5 +148,5 @@ class HomePagePickerFragment : Fragment() { } } - private fun isPhoneLandscape() = displayUtils.isLandscape() && !displayUtils.isTablet() + private fun isPhoneLandscape() = displayUtils.isLandscapeBySize() && !displayUtils.isTablet() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/ThumbDimensionProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/ThumbDimensionProvider.kt index 54693f3ba6e4..5d572c351272 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/ThumbDimensionProvider.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/theme/ThumbDimensionProvider.kt @@ -20,7 +20,7 @@ class ThumbDimensionProvider @Inject constructor( get() = min(getPixels(dimen.hpp_maximum_container_width), displayUtilsWrapper.getDisplayPixelWidth()) private val isTabletOrLandscape: Boolean - get() = displayUtilsWrapper.isTablet() || displayUtilsWrapper.isLandscape() + get() = displayUtilsWrapper.isTablet() || displayUtilsWrapper.isLandscapeBySize() /** * We calculate the columns by dividing the container width (without the margins) by the card width. The horizontal diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt index fc7fba04378f..ea21786fbe90 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/usecases/OverviewUseCase.kt @@ -3,6 +3,8 @@ package org.wordpress.android.ui.stats.refresh.lists.sections.granular.usecases import kotlinx.coroutines.CoroutineDispatcher import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.Stat.STATS_OVERVIEW_ERROR +import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.stats.LimitMode import org.wordpress.android.fluxc.model.stats.time.VisitsAndViewsModel import org.wordpress.android.fluxc.network.utils.StatsGranularity @@ -23,10 +25,13 @@ import org.wordpress.android.ui.stats.refresh.utils.toStatsSection import org.wordpress.android.ui.stats.refresh.utils.trackGranular import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T +import org.wordpress.android.util.LocaleManagerWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.viewmodel.ResourceProvider +import java.util.Calendar import javax.inject.Inject import javax.inject.Named +import kotlin.math.ceil const val OVERVIEW_ITEMS_TO_LOAD = 15 @@ -42,6 +47,7 @@ constructor( @Named(BG_THREAD) private val backgroundDispatcher: CoroutineDispatcher, private val analyticsTracker: AnalyticsTrackerWrapper, private val statsWidgetUpdaters: StatsWidgetUpdaters, + private val localeManagerWrapper: LocaleManagerWrapper, private val resourceProvider: ResourceProvider ) : BaseStatsUseCase( OVERVIEW, @@ -68,6 +74,7 @@ constructor( LimitMode.All ) if (cachedData != null) { + logIfIncorrectData(cachedData, statsGranularity, statsSiteProvider.siteModel, false) selectedDateProvider.onDateLoadingSucceeded(statsGranularity) } return cachedData @@ -89,6 +96,7 @@ constructor( State.Error(error.message ?: error.type.name) } model != null && model.dates.isNotEmpty() -> { + logIfIncorrectData(model, statsGranularity, statsSiteProvider.siteModel, true) selectedDateProvider.onDateLoadingSucceeded(statsGranularity) State.Data(model) } @@ -99,6 +107,38 @@ constructor( } } + /** + * Track the incorrect data shown for some users + * see https://github.com/wordpress-mobile/WordPress-Android/issues/11412 + */ + private fun logIfIncorrectData( + model: VisitsAndViewsModel, + granularity: StatsGranularity, + site: SiteModel, + fetched: Boolean + ) { + model.dates.lastOrNull()?.let { lastDayData -> + val yesterday = localeManagerWrapper.getCurrentCalendar() + yesterday.add(Calendar.DAY_OF_YEAR, -1) + val lastDayDate = statsDateFormatter.parseStatsDate(granularity, lastDayData.period) + if (lastDayDate.before(yesterday.time)) { + val currentCalendar = localeManagerWrapper.getCurrentCalendar() + val lastItemAge = ceil((currentCalendar.timeInMillis - lastDayDate.time) / 86400000.0) + analyticsTracker.track( + STATS_OVERVIEW_ERROR, + mapOf( + "stats_last_date" to statsDateFormatter.printStatsDate(lastDayDate), + "stats_current_date" to statsDateFormatter.printStatsDate(currentCalendar.time), + "stats_age_in_days" to lastItemAge.toInt(), + "is_jetpack_connected" to site.isJetpackConnected, + "is_atomic" to site.isWPComAtomic, + "action_source" to if (fetched) "remote" else "cached" + ) + ) + } + } + } + override fun buildUiModel( domainModel: VisitsAndViewsModel, uiState: UiState @@ -195,6 +235,7 @@ constructor( private val visitsAndViewsStore: VisitsAndViewsStore, private val analyticsTracker: AnalyticsTrackerWrapper, private val statsWidgetUpdaters: StatsWidgetUpdaters, + private val localeManagerWrapper: LocaleManagerWrapper, private val resourceProvider: ResourceProvider ) : GranularUseCaseFactory { override fun build(granularity: StatsGranularity, useCaseMode: UseCaseMode) = @@ -209,6 +250,7 @@ constructor( backgroundDispatcher, analyticsTracker, statsWidgetUpdaters, + localeManagerWrapper, resourceProvider ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stories/SaveStoryGutenbergBlockUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/stories/SaveStoryGutenbergBlockUseCase.kt index 8dae05f35665..65c3ed0fce73 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stories/SaveStoryGutenbergBlockUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stories/SaveStoryGutenbergBlockUseCase.kt @@ -101,9 +101,10 @@ class SaveStoryGutenbergBlockUseCase @Inject constructor( while (storyBlockStartIndex > -1 && storyBlockStartIndex < content.length) { storyBlockStartIndex = content.indexOf(HEADING_START, storyBlockStartIndex) if (storyBlockStartIndex > -1) { + val storyBlockEndIndex = content.indexOf(HEADING_END, storyBlockStartIndex) val jsonString: String = content.substring( storyBlockStartIndex + HEADING_START.length, - content.indexOf(HEADING_END)) + storyBlockEndIndex) content = listener.doWithMediaFilesJson(content, jsonString) storyBlockStartIndex += HEADING_START.length } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stories/StoriesMediaPickerResultHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/stories/StoriesMediaPickerResultHandler.kt index 0e3fd3cd563f..854bb695850f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stories/StoriesMediaPickerResultHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stories/StoriesMediaPickerResultHandler.kt @@ -2,43 +2,89 @@ package org.wordpress.android.ui.stories import android.app.Activity import android.content.Intent +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.PagePostCreationSourcesDetail import org.wordpress.android.ui.media.MediaBrowserActivity import org.wordpress.android.ui.media.MediaBrowserType +import org.wordpress.android.ui.mysite.SiteNavigationAction +import org.wordpress.android.ui.mysite.SiteNavigationAction.AddNewStory +import org.wordpress.android.ui.mysite.SiteNavigationAction.AddNewStoryWithMediaIds +import org.wordpress.android.ui.mysite.SiteNavigationAction.AddNewStoryWithMediaUris import org.wordpress.android.ui.photopicker.MediaPickerConstants import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T.UTILS +import org.wordpress.android.viewmodel.Event import javax.inject.Inject class StoriesMediaPickerResultHandler @Inject constructor() { - /* return true if MediaPickerResult was handled */ + private val _onNavigation = MutableLiveData>() + val onNavigation = _onNavigation as LiveData> + + @Deprecated("Use rather the other handle method and the live data navigation.") fun handleMediaPickerResultForStories( data: Intent, activity: Activity?, selectedSite: SiteModel?, source: PagePostCreationSourcesDetail ): Boolean { - if (data.getBooleanExtra(MediaPickerConstants.EXTRA_LAUNCH_WPSTORIES_CAMERA_REQUESTED, false)) { - ActivityLauncher.addNewStoryForResult( + if (selectedSite == null) { + return false + } + when (val navigationAction = buildNavigationAction(data, selectedSite, source)) { + is AddNewStory -> ActivityLauncher.addNewStoryForResult( + activity, + navigationAction.site, + navigationAction.source + ) + is AddNewStoryWithMediaIds -> ActivityLauncher.addNewStoryWithMediaIdsForResult( + activity, + navigationAction.site, + navigationAction.source, + navigationAction.mediaIds.toLongArray() + ) + is AddNewStoryWithMediaUris -> ActivityLauncher.addNewStoryWithMediaUrisForResult( activity, - selectedSite, - source + navigationAction.site, + navigationAction.source, + navigationAction.mediaUris.toTypedArray() ) - return true + else -> { + return false + } + } + + return true + } + + fun handleMediaPickerResultForStories( + data: Intent, + selectedSite: SiteModel, + source: PagePostCreationSourcesDetail + ): Boolean { + val navigationAction = buildNavigationAction(data, selectedSite, source) + return if (navigationAction != null) { + _onNavigation.postValue(Event(navigationAction)) + true + } else { + false + } + } + + private fun buildNavigationAction( + data: Intent, + selectedSite: SiteModel, + source: PagePostCreationSourcesDetail + ): SiteNavigationAction? { + if (data.getBooleanExtra(MediaPickerConstants.EXTRA_LAUNCH_WPSTORIES_CAMERA_REQUESTED, false)) { + return AddNewStory(selectedSite, source) } else if (isWPStoriesMediaBrowserTypeResult(data)) { if (data.hasExtra(MediaBrowserActivity.RESULT_IDS)) { - ActivityLauncher.addNewStoryWithMediaIdsForResult( - activity, - selectedSite, - source, - data.getLongArrayExtra( - MediaBrowserActivity.RESULT_IDS - ) - ) - return true + val mediaIds = data.getLongArrayExtra(MediaBrowserActivity.RESULT_IDS)?.asList() ?: listOf() + return AddNewStoryWithMediaIds(selectedSite, source, mediaIds) } else { val mediaUriStringsArray = data.getStringArrayExtra( MediaPickerConstants.EXTRA_MEDIA_URIS @@ -48,18 +94,17 @@ class StoriesMediaPickerResultHandler UTILS, "Can't resolve picked or captured image" ) - return false + return null } - ActivityLauncher.addNewStoryWithMediaUrisForResult( - activity, - selectedSite, - source, - mediaUriStringsArray - ) - return true + val mediaUris = mediaUriStringsArray.asList() + return AddNewStoryWithMediaUris( + selectedSite, + source, + mediaUris = mediaUris + ) } } - return false + return null } private fun isWPStoriesMediaBrowserTypeResult(data: Intent): Boolean { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stories/intro/StoriesIntroViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stories/intro/StoriesIntroViewModel.kt index de217a9e9816..a2d64f8343a3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stories/intro/StoriesIntroViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stories/intro/StoriesIntroViewModel.kt @@ -56,8 +56,8 @@ class StoriesIntroViewModel @Inject constructor( } companion object { - private const val STORY_URL_1 = "https://wpstories.wordpress.com/2020/10/12/patagonia-2/" - private const val STORY_URL_2 = "https://wpstories.wordpress.com/2020/10/12/hiking-in-the-southwest/" + private const val STORY_URL_1 = "https://wpstories.wordpress.com/2020/12/02/story-demo-01/" + private const val STORY_URL_2 = "https://wpstories.wordpress.com/2020/12/02/story-demo-02/" private const val STORY_FULLSCREEN_URL_PARAMS = "?wp-story-load-in-fullscreen=true&wp-story-play-on-load=true" } } diff --git a/WordPress/src/main/java/org/wordpress/android/util/DisplayUtilsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/DisplayUtilsWrapper.kt index 9566a367501e..092628764a0d 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/DisplayUtilsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/DisplayUtilsWrapper.kt @@ -8,6 +8,8 @@ import javax.inject.Inject class DisplayUtilsWrapper @Inject constructor(private val contextProvider: ContextProvider) { fun getDisplayPixelWidth() = DisplayUtils.getDisplayPixelWidth() + fun isLandscapeBySize() = getDisplayPixelWidth() > DisplayUtils.getDisplayPixelHeight(contextProvider.getContext()) + fun isLandscape() = DisplayUtils.isLandscape(contextProvider.getContext()) fun isTablet() = DisplayUtils.isTablet(contextProvider.getContext()) || diff --git a/WordPress/src/main/java/org/wordpress/android/util/DomainRegistrationUtils.kt b/WordPress/src/main/java/org/wordpress/android/util/DomainRegistrationUtils.kt index 587fa4c0615f..36e6943d1fb1 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/DomainRegistrationUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/DomainRegistrationUtils.kt @@ -4,6 +4,9 @@ import android.content.Context import android.text.TextUtils import android.view.Gravity import org.wordpress.android.R +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.ToastUtils.Duration fun requestEmailValidation(context: Context, email: String?) { @@ -22,3 +25,9 @@ fun requestEmailValidation(context: Context, email: String?) { context.resources.getDimensionPixelOffset(R.dimen.smart_toast_offset_y) ) } + +fun getEmailValidationMessage(email: String?) = if (email.isNullOrEmpty()) { + UiStringRes(R.string.my_site_verify_your_email_without_email) +} else { + UiStringResWithParams(R.string.my_site_verify_your_email, listOf(UiStringText(email))) +} diff --git a/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt index 7d1a5b3e8ee7..489f4d159016 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt @@ -19,4 +19,6 @@ class SiteUtilsWrapper @Inject constructor() { SiteUtils.getAccessibilityInfoFromSite(site) fun isAccessedViaWPComRest(site: SiteModel): Boolean = SiteUtils.isAccessedViaWPComRest(site) + fun onFreePlan(site: SiteModel): Boolean = SiteUtils.onFreePlan(site) + fun hasCustomDomain(site: SiteModel): Boolean = SiteUtils.hasCustomDomain(site) } diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt index 7b963fe44dee..7d39b59e6d85 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt @@ -15,17 +15,23 @@ import org.wordpress.android.fluxc.model.activity.RewindStatusModel.Rewind.Statu import org.wordpress.android.fluxc.store.ActivityLogStore import org.wordpress.android.fluxc.store.ActivityLogStore.OnActivityLogFetched import org.wordpress.android.modules.UI_THREAD -import org.wordpress.android.ui.jetpack.rewind.RewindStatusService -import org.wordpress.android.ui.jetpack.rewind.RewindStatusService.RewindProgress +import org.wordpress.android.ui.activitylog.ActivityLogNavigationEvents +import org.wordpress.android.ui.activitylog.ActivityLogNavigationEvents.ShowBackupDownload +import org.wordpress.android.ui.activitylog.ActivityLogNavigationEvents.ShowRestore import org.wordpress.android.ui.activitylog.list.ActivityLogListItem -import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.Event import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.Footer import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.Header import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.Loading +import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.SecondaryAction.DOWNLOAD_BACKUP +import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.SecondaryAction.RESTORE +import org.wordpress.android.ui.jetpack.rewind.RewindStatusService +import org.wordpress.android.ui.jetpack.rewind.RewindStatusService.RewindProgress import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.util.AppLog +import org.wordpress.android.util.BackupFeatureConfig import org.wordpress.android.util.config.ActivityLogFiltersFeatureConfig +import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ResourceProvider import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent @@ -43,6 +49,7 @@ class ActivityLogViewModel @Inject constructor( private val rewindStatusService: RewindStatusService, private val resourceProvider: ResourceProvider, private val activityLogFiltersFeatureConfig: ActivityLogFiltersFeatureConfig, + private val backupFeatureConfig: BackupFeatureConfig, @param:Named(UI_THREAD) private val uiDispatcher: CoroutineDispatcher ) : ScopedViewModel(uiDispatcher) { enum class ActivityLogListStatus { @@ -91,6 +98,11 @@ class ActivityLogViewModel @Inject constructor( val showSnackbarMessage: LiveData get() = _showSnackbarMessage + private val _navigationEvents = + MutableLiveData>() + val navigationEvents: LiveData> + get() = _navigationEvents + private val isLoadingInProgress: Boolean get() = eventListStatus.value == LOADING_MORE || eventListStatus.value == ActivityLogListStatus.FETCHING @@ -171,17 +183,36 @@ class ActivityLogViewModel @Inject constructor( } fun onItemClicked(item: ActivityLogListItem) { - if (item is Event) { + if (item is ActivityLogListItem.Event) { _showItemDetail.value = item } } + // todo: annmarie - Remove once the feature exclusively uses the more menu fun onActionButtonClicked(item: ActivityLogListItem) { - if (item is Event) { + if (item is ActivityLogListItem.Event) { _showRewindDialog.value = item } } + fun onSecondaryActionClicked( + secondaryAction: ActivityLogListItem.SecondaryAction, + item: ActivityLogListItem + ): Boolean { + if (item is ActivityLogListItem.Event) { + val navigationEvent = when (secondaryAction) { + RESTORE -> { + ShowRestore(item) + } + DOWNLOAD_BACKUP -> { + ShowBackupDownload(item) + } + } + _navigationEvents.value = org.wordpress.android.viewmodel.Event(navigationEvent) + } + return true + } + fun dateRangePickerClicked() { _showDateRangePicker.value = ShowDateRangePicker(initialSelection = currentDateRangeFilter) } @@ -235,8 +266,8 @@ class ActivityLogViewModel @Inject constructor( moveToTop = eventListStatus.value != LOADING_MORE } eventList.forEach { model -> - val currentItem = Event(model, disableActions) - val lastItem = items.lastOrNull() as? Event + val currentItem = ActivityLogListItem.Event(model, disableActions, backupFeatureConfig.isEnabled()) + val lastItem = items.lastOrNull() as? ActivityLogListItem.Event if (lastItem == null || lastItem.formattedDate != currentItem.formattedDate) { items.add(Header(currentItem.formattedDate)) } @@ -265,7 +296,9 @@ class ActivityLogViewModel @Inject constructor( private fun getRewindProgressItem(activityLogModel: ActivityLogModel?): ActivityLogListItem.Progress { return activityLogModel?.let { - val rewoundEvent = ActivityLogListItem.Event(it) + val rewoundEvent = ActivityLogListItem.Event( + model = it, + backupFeatureEnabled = backupFeatureConfig.isEnabled()) ActivityLogListItem.Progress(resourceProvider.getString(R.string.activity_log_currently_restoring_title), resourceProvider.getString(R.string.activity_log_currently_restoring_message, rewoundEvent.formattedDate, rewoundEvent.formattedTime)) @@ -275,7 +308,7 @@ class ActivityLogViewModel @Inject constructor( private fun requestEventsUpdate(isLoadingMore: Boolean) { if (canRequestEventsUpdate(isLoadingMore)) { - val newStatus = if (isLoadingMore) ActivityLogListStatus.LOADING_MORE else ActivityLogListStatus.FETCHING + val newStatus = if (isLoadingMore) LOADING_MORE else ActivityLogListStatus.FETCHING _eventListStatus.value = newStatus val payload = ActivityLogStore.FetchActivityLogPayload(site, isLoadingMore) launch { @@ -295,7 +328,7 @@ class ActivityLogViewModel @Inject constructor( private fun showRewindStartedMessage() { rewindStatusService.rewindingActivity?.let { - val event = Event(it) + val event = ActivityLogListItem. Event(model = it, backupFeatureEnabled = backupFeatureConfig.isEnabled()) _showSnackbarMessage.value = resourceProvider.getString( R.string.activity_log_rewind_started_snackbar_message, event.formattedDate, @@ -307,7 +340,7 @@ class ActivityLogViewModel @Inject constructor( private fun showRewindFinishedMessage() { val item = rewindStatusService.rewindingActivity if (item != null) { - val event = Event(item) + val event = ActivityLogListItem.Event(model = item, backupFeatureEnabled = backupFeatureConfig.isEnabled()) _showSnackbarMessage.value = resourceProvider.getString(R.string.activity_log_rewind_finished_snackbar_message, event.formattedDate, diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt index 16ee92234bc3..16496be41a02 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt @@ -44,6 +44,7 @@ import org.wordpress.android.viewmodel.posts.PostListItemType.PostListItemUiStat import org.wordpress.android.viewmodel.uistate.ProgressBarUiState import org.wordpress.android.widgets.PostListButtonType import org.wordpress.android.widgets.PostListButtonType.BUTTON_CANCEL_PENDING_AUTO_UPLOAD +import org.wordpress.android.widgets.PostListButtonType.BUTTON_COPY import org.wordpress.android.widgets.PostListButtonType.BUTTON_DELETE import org.wordpress.android.widgets.PostListButtonType.BUTTON_DELETE_PERMANENTLY import org.wordpress.android.widgets.PostListButtonType.BUTTON_EDIT @@ -351,6 +352,7 @@ class PostListItemUiStateHelper @Inject constructor( postStatus == PUBLISHED && !isLocalDraft && !isLocallyChanged + val canShowCopy = postStatus == PUBLISHED || postStatus == DRAFT val canShowViewButton = !canRetryUpload && postStatus != PostStatus.TRASHED val canShowPublishButton = canRetryUpload || canPublishPost val buttonTypes = ArrayList() @@ -400,6 +402,10 @@ class PostListItemUiStateHelper @Inject constructor( buttonTypes.add(BUTTON_STATS) } + if (canShowCopy) { + buttonTypes.add(BUTTON_COPY) + } + return buttonTypes } diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/PostListButtonType.kt b/WordPress/src/main/java/org/wordpress/android/widgets/PostListButtonType.kt index a058f64bfdbc..a8291b9a07ab 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/PostListButtonType.kt +++ b/WordPress/src/main/java/org/wordpress/android/widgets/PostListButtonType.kt @@ -36,7 +36,8 @@ enum class PostListButtonType constructor( R.drawable.ic_undo_white_24dp, R.attr.wpColorWarningDark ), - BUTTON_SHOW_MOVE_TRASHED_POST_TO_DRAFT_DIALOG(15, 0, 0, 0); + BUTTON_SHOW_MOVE_TRASHED_POST_TO_DRAFT_DIALOG(15, 0, 0, 0), + BUTTON_COPY(16, R.string.button_copy, R.drawable.ic_copy_white_24dp, R.attr.colorOnSurface); companion object { fun fromInt(value: Int): PostListButtonType? = values().firstOrNull { it.value == value } diff --git a/WordPress/src/main/res/drawable-hdpi/stories_intro_cover_1.png b/WordPress/src/main/res/drawable-hdpi/stories_intro_cover_1.png index fb0f763f0735..9a8f74d1815a 100644 Binary files a/WordPress/src/main/res/drawable-hdpi/stories_intro_cover_1.png and b/WordPress/src/main/res/drawable-hdpi/stories_intro_cover_1.png differ diff --git a/WordPress/src/main/res/drawable-hdpi/stories_intro_cover_2.png b/WordPress/src/main/res/drawable-hdpi/stories_intro_cover_2.png index c53bc1adb94a..a6c71d049118 100644 Binary files a/WordPress/src/main/res/drawable-hdpi/stories_intro_cover_2.png and b/WordPress/src/main/res/drawable-hdpi/stories_intro_cover_2.png differ diff --git a/WordPress/src/main/res/drawable-xhdpi/stories_intro_cover_1.png b/WordPress/src/main/res/drawable-xhdpi/stories_intro_cover_1.png index 869d01a24c4b..12260bf4b1a7 100644 Binary files a/WordPress/src/main/res/drawable-xhdpi/stories_intro_cover_1.png and b/WordPress/src/main/res/drawable-xhdpi/stories_intro_cover_1.png differ diff --git a/WordPress/src/main/res/drawable-xhdpi/stories_intro_cover_2.png b/WordPress/src/main/res/drawable-xhdpi/stories_intro_cover_2.png index bc84edec4be2..c76abd7dc35d 100644 Binary files a/WordPress/src/main/res/drawable-xhdpi/stories_intro_cover_2.png and b/WordPress/src/main/res/drawable-xhdpi/stories_intro_cover_2.png differ diff --git a/WordPress/src/main/res/drawable/ic_get_app_24dp.xml b/WordPress/src/main/res/drawable/ic_get_app_24dp.xml new file mode 100644 index 000000000000..d4be3b26dbad --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_get_app_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/WordPress/src/main/res/layout/activity_log_list_event_item.xml b/WordPress/src/main/res/layout/activity_log_list_event_item.xml index b602d73f59d4..d96ef71e7bef 100644 --- a/WordPress/src/main/res/layout/activity_log_list_event_item.xml +++ b/WordPress/src/main/res/layout/activity_log_list_event_item.xml @@ -31,7 +31,7 @@ android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="@string/activity_log_button" android:padding="@dimen/activity_log_icon_margin" - android:src="@drawable/ic_history_white_24dp" + tools:src="@drawable/ic_ellipsis_vertical_white_24dp" app:tint="?attr/colorPrimary" /> + + + + + + diff --git a/WordPress/src/main/res/layout/activity_log_type_filter_fragment.xml b/WordPress/src/main/res/layout/activity_log_type_filter_fragment.xml index e64199516a06..b0158ae75d87 100644 --- a/WordPress/src/main/res/layout/activity_log_type_filter_fragment.xml +++ b/WordPress/src/main/res/layout/activity_log_type_filter_fragment.xml @@ -11,6 +11,8 @@ android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" + android:layout_marginEnd="@dimen/margin_extra_large" + android:layout_marginStart="@dimen/margin_extra_large" android:layout_marginTop="@dimen/toolbar_height" /> + + @@ -35,9 +42,9 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" - app:aevImage="@drawable/img_illustration_cloud_off_152dp" app:aevButton="@string/retry" + app:aevImage="@drawable/img_illustration_cloud_off_152dp" app:aevTitle="@string/error" - tools:visibility="visible"/> + tools:visibility="visible" /> diff --git a/WordPress/src/main/res/layout/activity_log_type_filter_header.xml b/WordPress/src/main/res/layout/activity_log_type_filter_header.xml index 50cae8341aad..f87e544e29d8 100644 --- a/WordPress/src/main/res/layout/activity_log_type_filter_header.xml +++ b/WordPress/src/main/res/layout/activity_log_type_filter_header.xml @@ -8,6 +8,9 @@ android:id="@+id/header_title" android:layout_width="0dp" android:layout_height="wrap_content" + android:fontFamily="sans-serif-medium" + android:textColor="?attr/colorOnSurface" + android:textSize="@dimen/text_sz_large" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/WordPress/src/main/res/layout/activity_log_type_filter_item.xml b/WordPress/src/main/res/layout/activity_log_type_filter_item.xml index f4496a25be12..fe22f8b1d07b 100644 --- a/WordPress/src/main/res/layout/activity_log_type_filter_item.xml +++ b/WordPress/src/main/res/layout/activity_log_type_filter_item.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/container" android:layout_width="match_parent" - android:layout_height="?attr/listPreferredItemHeight" + android:layout_height="?attr/listPreferredItemHeightSmall" android:background="?attr/selectableItemBackground" android:focusable="true"> @@ -11,6 +11,7 @@ android:id="@+id/checkbox" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/margin_extra_extra_medium_large" android:clickable="false" android:focusable="false" app:layout_constraintBottom_toBottomOf="parent" @@ -23,6 +24,8 @@ android:id="@+id/activity_type" android:layout_width="0dp" android:layout_height="wrap_content" + android:textSize="@dimen/text_sz_large" + android:textColor="?attr/colorOnSurface" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/checkbox" diff --git a/WordPress/src/main/res/layout/backup_details_choose_items.xml b/WordPress/src/main/res/layout/backup_details_list_item.xml similarity index 65% rename from WordPress/src/main/res/layout/backup_details_choose_items.xml rename to WordPress/src/main/res/layout/backup_details_list_item.xml index c5d154d40a83..c5c2e0d9fcd6 100644 --- a/WordPress/src/main/res/layout/backup_details_choose_items.xml +++ b/WordPress/src/main/res/layout/backup_details_list_item.xml @@ -4,26 +4,35 @@ android:id="@+id/item_container" android:background="?attr/selectableItemBackground" android:layout_width="match_parent" - android:layout_height="?attr/listPreferredItemHeight"> + android:layout_height="?attr/listPreferredItemHeight" + android:focusable="true"> + + diff --git a/WordPress/src/main/res/layout/backup_download_details_fragment.xml b/WordPress/src/main/res/layout/backup_download_details_fragment.xml index 77a2a9cd9588..949e9da4a3cd 100644 --- a/WordPress/src/main/res/layout/backup_download_details_fragment.xml +++ b/WordPress/src/main/res/layout/backup_download_details_fragment.xml @@ -54,56 +54,17 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/backup_details_action_button" + app:layout_constraintBottom_toTopOf="@+id/recycler_view" tools:text="Items included in this download" android:text="@string/backup_download_details_choose_items_header"/> - - - - - - - - - - - - - + app:layout_constraintBottom_toBottomOf="parent"/> diff --git a/WordPress/src/main/res/layout/domain_registration_block.xml b/WordPress/src/main/res/layout/domain_registration_block.xml new file mode 100644 index 000000000000..6b511301bfc4 --- /dev/null +++ b/WordPress/src/main/res/layout/domain_registration_block.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/home_page_picker_titlebar.xml b/WordPress/src/main/res/layout/home_page_picker_titlebar.xml index 1c7cb36355d4..18504a629f34 100644 --- a/WordPress/src/main/res/layout/home_page_picker_titlebar.xml +++ b/WordPress/src/main/res/layout/home_page_picker_titlebar.xml @@ -1,5 +1,5 @@ - @@ -16,7 +16,10 @@