diff --git a/.buildkite/beta-builds.yml b/.buildkite/beta-builds.yml index c2d717af9541..556fb4dfb1bc 100644 --- a/.buildkite/beta-builds.yml +++ b/.buildkite/beta-builds.yml @@ -5,7 +5,7 @@ common_params: # Common plugin settings to use with the `plugins` key. - &common_plugins - - automattic/bash-cache#2.11.0 + - automattic/a8c-ci-toolkit#2.15.0 steps: ################# diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 67388fb4884e..1aa998530d8b 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -2,7 +2,7 @@ common_params: # Common plugin settings to use with the `plugins` key. - &common_plugins - - automattic/bash-cache#2.11.0 + - automattic/a8c-ci-toolkit#2.14.0 steps: ################# diff --git a/.buildkite/release-builds.yml b/.buildkite/release-builds.yml index 41984ffc0f79..a19960ace524 100644 --- a/.buildkite/release-builds.yml +++ b/.buildkite/release-builds.yml @@ -5,7 +5,7 @@ common_params: # Common plugin settings to use with the `plugins` key. - &common_plugins - - automattic/bash-cache#2.11.0 + - automattic/a8c-ci-toolkit#2.14.0 steps: ################# diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index b5a960813166..432b907ff552 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -7,4 +7,4 @@ ### Steps to reproduce the behavior -##### Tested on [device], Android [version], WPAndroid [version] +##### Tested on [device], Android [version], JPAndroid / WPAndroid [version] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 14e196c030fd..ccc229398313 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -16,3 +16,15 @@ PR submission checklist: - [ ] I have completed the Regression Notes. - [ ] I have considered adding accessibility improvements for my changes. - [ ] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary. + +UI Changes testing checklist: + +- [ ] Portrait and landscape orientations. +- [ ] Light and dark modes. +- [ ] Fonts: Larger, smaller and bold text. +- [ ] High contrast. +- [ ] Talkback. +- [ ] Languages with large words or with letters/accents not frequently used in English. +- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout) +- [ ] Large and small screen sizes. (Tablet and smaller phones) +- [ ] Multi-tasking: Split screen and Pop-up view. (Android 10 or higher) diff --git a/.gitignore b/.gitignore index cea2ed803d70..07c8006dfc35 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,9 @@ WordPress/src/main/res/values/com_crashlytics_export_strings.xml # Silver Searcher ignore file .agignore +# ripgrep ignore file +.rgignore + # Monkey runner settings *.pyc diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index d77a9299aae7..25fd80da4522 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,8 +1,25 @@ *** PLEASE FOLLOW THIS FORMAT: [] [] -22.0 +22.3 +----- + + +22.2 ----- +* [*] Adds runtime notifications permission [https://github.com/wordpress-mobile/WordPress-Android/pull/18239] +* [**] Adds media permissions support for Android 13 [https://github.com/wordpress-mobile/WordPress-Android/pull/18183] +* [**] [Jetpack-only] Adds a dashboard card for purchasing domains. [https://github.com/wordpress-mobile/WordPress-Android/pull/18240] +* [**] [Jetpack-only] Blogging Prompts: adds the ability to view other users' responses to a prompt. [https://github.com/wordpress-mobile/WordPress-Android/pull/18265] +* [*] [internal] [Jetpack-only] Redesigned the migration success card. [https://github.com/wordpress-mobile/WordPress-Android/pull/18271] +22.1 +----- +* [**] [WordPress-only] Warns user about sites with only individual plugins not supporting core app features and offers the option to switch to the Jetpack app. [https://github.com/wordpress-mobile/WordPress-Android/pull/18199] +* [*] Block editor: Avoid empty Gallery block error [https://github.com/WordPress/gutenberg/pull/49557] + +22.0 +----- +* [*] Block editor: Allow new block transforms for most blocks. [https://github.com/WordPress/gutenberg/pull/48792] 21.9 ----- diff --git a/WordPress/build.gradle b/WordPress/build.gradle index ed04eb58ad3a..ce3b3cddda45 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -125,12 +125,17 @@ android { buildConfigField "boolean", "JETPACK_FEATURE_REMOVAL_PHASE_THREE", "false" buildConfigField "boolean", "JETPACK_FEATURE_REMOVAL_PHASE_FOUR", "false" buildConfigField "boolean", "JETPACK_FEATURE_REMOVAL_NEW_USERS", "false" + buildConfigField "boolean", "JETPACK_FEATURE_REMOVAL_STATIC_POSTERS", "false" buildConfigField "boolean", "JETPACK_FEATURE_REMOVAL_SELF_HOSTED_USERS", "false" buildConfigField "boolean", "PREVENT_DUPLICATE_NOTIFS_REMOTE_FIELD", "false" buildConfigField "boolean", "OPEN_WEB_LINKS_WITH_JETPACK_FLOW", "false" buildConfigField "boolean", "ENABLE_WORDPRESS_SUPPORT_FORUM", "false" buildConfigField "boolean", "JETPACK_INSTALL_FULL_PLUGIN", "false" buildConfigField "boolean", "ENABLE_BLAZE_FEATURE", "false" + buildConfigField "boolean", "WP_INDIVIDUAL_PLUGIN_OVERLAY", "false" + buildConfigField "boolean", "DASHBOARD_CARD_PAGES", "false" + buildConfigField "boolean", "DASHBOARD_CARD_ACTIVITY_LOG", "false" + buildConfigField "boolean", "DASHBOARD_CARD_DOMAIN", "false" // Override these constants in jetpack product flavor to enable/ disable features buildConfigField "boolean", "ENABLE_SITE_CREATION", "true" @@ -258,9 +263,11 @@ android { } lintOptions{ + warningsAsErrors = true checkDependencies = true checkGeneratedSources = true - lintConfig file("${project.rootDir}/config/lint.xml") + lintConfig file("${project.rootDir}/config/lint/lint.xml") + baseline file("${project.rootDir}/config/lint/baseline.xml") } packagingOptions { @@ -438,7 +445,8 @@ dependencies { implementation "androidx.compose.material:material:$androidxComposeVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$androidxComposeLifecycleVersion" implementation "io.coil-kt:coil-compose:$coilComposeVersion" - implementation ('com.github.indexos.media-for-mobile:android:43a9026f0973a2f0a74fa813132f6a16f7499c3a') + implementation "com.github.indexos.media-for-mobile:domain:$indexosMediaForMobileVersion" + implementation "com.github.indexos.media-for-mobile:android:$indexosMediaForMobileVersion" implementation "com.zendesk:support:$zendeskVersion" implementation (name:'tenor-android-core-jetified', ext:'aar') // Jetified Tenor Gif library implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion" @@ -448,7 +456,6 @@ dependencies { implementation ("com.google.android.exoplayer:exoplayer:$googleExoPlayerVersion") { exclude group: 'com.android.support', module: 'support-annotations' } - compileOnly "org.glassfish:javax.annotation:$glassfishJavaxAnnotationVersion" implementation "com.google.dagger:dagger-android-support:$gradle.ext.daggerVersion" kapt "com.google.dagger:dagger-android-processor:$gradle.ext.daggerVersion" implementation "com.google.dagger:hilt-android:$gradle.ext.daggerVersion" diff --git a/WordPress/jetpack_metadata/PlayStoreStrings.po b/WordPress/jetpack_metadata/PlayStoreStrings.po index fb95c53e92be..229c4573686a 100644 --- a/WordPress/jetpack_metadata/PlayStoreStrings.po +++ b/WordPress/jetpack_metadata/PlayStoreStrings.po @@ -10,17 +10,19 @@ msgstr "" "X-Generator: VsCode\n" "Project-Id-Version: Jetpack - Apps - Android - Release Notes\n" -msgctxt "release_note_219" +msgctxt "release_note_222" msgid "" -"21.9:\n" -"Hot news—Jetpack now supports Blaze, so you can reach new readers by promoting a post or page from within the app.\n" -"We also added a post-migration FAQ card to the Help screen, which you’ll see after switching from the WordPress app over to Jetpack.\n" +"22.2:\n" +"- Added warning message for when push notifications are turned off\n" +"- Updated media access permissions to align with Android 13 update\n" +"- Added card to dashboard for purchasing custom domains\n" +"- Added other users’ responses to blogging prompts\n" msgstr "" -msgctxt "release_note_218" +msgctxt "release_note_221" msgid "" -"21.8:\n" -"We’ve added a little extra support for sites with individual plugins. These plugins don’t support all app features yet, but you can always install the full Jetpack plugin instead. Ready to launch?\n" +"22.1:\n" +"No updates this week. In the meantime, we're just over here cooling our jets.\n" msgstr "" #. translators: Release notes for this version to be displayed in the Play Store. Limit to 500 characters including spaces and commas! diff --git a/WordPress/jetpack_metadata/release_notes.txt b/WordPress/jetpack_metadata/release_notes.txt index 09a4b20a2a11..5079ceaa7470 100644 --- a/WordPress/jetpack_metadata/release_notes.txt +++ b/WordPress/jetpack_metadata/release_notes.txt @@ -1,2 +1,4 @@ -Hot news—Jetpack now supports Blaze, so you can reach new readers by promoting a post or page from within the app. -We also added a post-migration FAQ card to the Help screen, which you’ll see after switching from the WordPress app over to Jetpack. +- Added warning message for when push notifications are turned off +- Updated media access permissions to align with Android 13 update +- Added card to dashboard for purchasing custom domains +- Added other users’ responses to blogging prompts diff --git a/WordPress/jetpack_metadata/release_notes_short.txt b/WordPress/jetpack_metadata/release_notes_short.txt index 09a4b20a2a11..e69de29bb2d1 100644 --- a/WordPress/jetpack_metadata/release_notes_short.txt +++ b/WordPress/jetpack_metadata/release_notes_short.txt @@ -1,2 +0,0 @@ -Hot news—Jetpack now supports Blaze, so you can reach new readers by promoting a post or page from within the app. -We also added a post-migration FAQ card to the Help screen, which you’ll see after switching from the WordPress app over to Jetpack. diff --git a/WordPress/metadata/PlayStoreStrings.po b/WordPress/metadata/PlayStoreStrings.po index 84e4ac0f9c59..cdf8bf5e2e82 100644 --- a/WordPress/metadata/PlayStoreStrings.po +++ b/WordPress/metadata/PlayStoreStrings.po @@ -10,19 +10,17 @@ msgstr "" "X-Generator: VsCode\n" "Project-Id-Version: Release Notes & Play Store Descriptions\n" -msgctxt "release_note_219" +msgctxt "release_note_222" msgid "" -"21.9:\n" -"Good words are read\n" -"The WordPress logo is blue\n" -"There are no new updates\n" -"Sorry to disappoint you\n" +"22.2:\n" +"- We added a yellow warning box telling you when notifications and blogging reminders are no longer being sent to your device as push notifications.\n" +"- We updated media access permissions for photos, videos, and audio to align with the Android 13 update.\n" msgstr "" -msgctxt "release_note_218" +msgctxt "release_note_221" msgid "" -"21.8:\n" -"Say hello to our shiny new in-app landing screen! It almost makes you want to look at it all day and not publish anything new. Almost.\n" +"22.1:\n" +"When you log in to a self-hosted site that connects to Jetpack through individual plugins, you’ll see a pop-up stating that this type of connection doesn’t support the app’s core features yet. You can get around that problem by switching over to the Jetpack app. Up, up, and away.\n" msgstr "" #. translators: Release notes for this version to be displayed in the Play Store. Limit to 500 characters including spaces and commas! diff --git a/WordPress/metadata/release_notes.txt b/WordPress/metadata/release_notes.txt index f9f4153122e9..67aedfc6d9b8 100644 --- a/WordPress/metadata/release_notes.txt +++ b/WordPress/metadata/release_notes.txt @@ -1,4 +1,2 @@ -Good words are read -The WordPress logo is blue -There are no new updates -Sorry to disappoint you +- We added a yellow warning box telling you when notifications and blogging reminders are no longer being sent to your device as push notifications. +- We updated media access permissions for photos, videos, and audio to align with the Android 13 update. diff --git a/WordPress/metadata/release_notes_short.txt b/WordPress/metadata/release_notes_short.txt index 83b79f7ade86..e69de29bb2d1 100644 --- a/WordPress/metadata/release_notes_short.txt +++ b/WordPress/metadata/release_notes_short.txt @@ -1,3 +0,0 @@ -It is a truth universally acknowledged, that a content creator in possession of a good app, must be in want of an update. - -…and we’re still working on it. diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/BlockEditorTests.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/BlockEditorTests.kt index 06509ce796fd..37277701b4e3 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/BlockEditorTests.kt +++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/BlockEditorTests.kt @@ -1,11 +1,8 @@ package org.wordpress.android.e2e -import android.Manifest.permission -import androidx.test.rule.GrantPermissionRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Ignore -import org.junit.Rule import org.junit.Test import org.wordpress.android.e2e.pages.BlockEditorPage import org.wordpress.android.e2e.pages.MySitesPage @@ -14,9 +11,6 @@ import java.time.Instant @HiltAndroidTest class BlockEditorTests : BaseTest() { - @JvmField @Rule - var mRuntimeImageAccessRule = GrantPermissionRule.grant(permission.WRITE_EXTERNAL_STORAGE) - @Before fun setUp() { logoutIfNecessary() @@ -48,8 +42,8 @@ class BlockEditorTests : BaseTest() { .verifyPostPublished() } - @Ignore @Test + @Ignore("This test is temporarily disabled as being flaky.") fun e2ePublishFullPost() { val title = "publishFullPost" MySitesPage() diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/ReaderTests.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/ReaderTests.kt index e353e82ae306..cedcf01b9563 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/ReaderTests.kt +++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/ReaderTests.kt @@ -1,19 +1,13 @@ package org.wordpress.android.e2e -import android.Manifest.permission -import androidx.test.rule.GrantPermissionRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before -import org.junit.Rule import org.junit.Test import org.wordpress.android.e2e.pages.ReaderPage import org.wordpress.android.support.BaseTest @HiltAndroidTest class ReaderTests : BaseTest() { - @JvmField @Rule - var mRuntimeImageAccessRule = GrantPermissionRule.grant(permission.WRITE_EXTERNAL_STORAGE) - @Before fun setUp() { logoutIfNecessary() @@ -21,18 +15,16 @@ class ReaderTests : BaseTest() { ReaderPage().go() } - var mCoachingPostTitle = "Let's check out the coaching team!" - var mCompetitionPostTitle = "Let's focus on the competition." @Test fun e2eNavigateThroughPosts() { ReaderPage() .tapFollowingTab() - .openPost(mCoachingPostTitle) - .verifyPostDisplayed(mCoachingPostTitle) + .openBlogOrPost(TITLE_COACHING_POST) + .verifyPostDisplayed(TITLE_COACHING_POST) .slideToPreviousPost() - .verifyPostDisplayed(mCompetitionPostTitle) + .verifyPostDisplayed(TITLE_COMPETITION_POST) .slideToNextPost() - .verifyPostDisplayed(mCoachingPostTitle) + .verifyPostDisplayed(TITLE_COACHING_POST) .goBackToReader() } @@ -40,11 +32,29 @@ class ReaderTests : BaseTest() { fun e2eLikePost() { ReaderPage() .tapFollowingTab() - .openPost(mCoachingPostTitle) + .openBlogOrPost(TITLE_COACHING_POST) .likePost() .verifyPostLiked() .unlikePost() .verifyPostNotLiked() .goBackToReader() } + + @Test + fun e2eBookmarkPost() { + ReaderPage() + .tapFollowingTab() + .openBlogOrPost(TITLE_LONGREADS_BLOG) + .bookmarkPost() + .verifyPostBookmarked() + .removeBookmarkPost() + .verifyPostNotBookmarked() + .goBackToReader() + } + + companion object { + private const val TITLE_LONGREADS_BLOG = "Longreads" + private const val TITLE_COACHING_POST = "Let's check out the coaching team!" + private const val TITLE_COMPETITION_POST = "Let's focus on the competition." + } } diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/StatsTests.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/StatsTests.kt index 96fdeaff0c02..380862080c0c 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/StatsTests.kt +++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/StatsTests.kt @@ -4,8 +4,10 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.matcher.ViewMatchers import dagger.hilt.android.testing.HiltAndroidTest import org.junit.After +import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test +import org.wordpress.android.BuildConfig import org.wordpress.android.R import org.wordpress.android.e2e.pages.MySitesPage import org.wordpress.android.support.BaseTest @@ -33,6 +35,10 @@ class StatsTests : BaseTest() { @Test fun e2eAllDayStatsLoad() { + // We're not running this test on JP. + // See https://github.com/wordpress-mobile/WordPress-Android/issues/18065 + assumeTrue(!BuildConfig.IS_JETPACK_APP) + val todayVisits = StatsVisitsData("97", "28", "14", "11") val postsList: List = StatsMocksReader().readDayTopPostsToList() val referrersList: List = StatsMocksReader().readDayTopReferrersToList() diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/MySitesPage.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/MySitesPage.kt index 968ba14c192b..11af1dd9588e 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/MySitesPage.kt +++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/MySitesPage.kt @@ -154,7 +154,11 @@ class MySitesPage { fun goToStats(): StatsPage { goToMenuTab() - clickQuickActionOrSiteMenuItem(R.id.quick_action_stats_button, R.string.stats) + val statsButton = Espresso.onView(Matchers.allOf( + ViewMatchers.withText(R.string.stats), + ViewMatchers.withId(R.id.my_site_item_primary_text) + )) + WPSupportUtils.clickOn(statsButton) WPSupportUtils.idleFor(4000) WPSupportUtils.dismissJetpackAdIfPresent() WPSupportUtils.waitForElementToBeDisplayedWithoutFailure(R.id.tabLayout) diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/ReaderPage.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/ReaderPage.kt index 15fc175dafd4..3756d7e54e97 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/ReaderPage.kt +++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/ReaderPage.kt @@ -16,7 +16,7 @@ class ReaderPage { return this } - fun openPost(postTitle: String?): ReaderViewPage { + fun openBlogOrPost(postTitle: String): ReaderViewPage { val post = Espresso.onView(ViewMatchers.withChild(ViewMatchers.withText(postTitle))) WPSupportUtils.scrollIntoView(R.id.reader_recycler_view, post, 1f) WPSupportUtils.clickOn(postTitle) diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/ReaderViewPage.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/ReaderViewPage.kt index aed5f4c374a8..97d863fc79c4 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/ReaderViewPage.kt +++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/ReaderViewPage.kt @@ -3,58 +3,88 @@ package org.wordpress.android.e2e.pages import android.view.KeyEvent import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiSelector import junit.framework.TestCase import org.wordpress.android.support.WPSupportUtils class ReaderViewPage { - var mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - var mLikerContainerId = "org.wordpress.android.prealpha:id/liker_faces_container" - var mRelatedPostsId = "org.wordpress.android.prealpha:id/container_related_posts" - var mFooterId = "org.wordpress.android.prealpha:id/layout_post_detail_footer" - var mLikerContainer = mDevice.findObject(UiSelector().resourceId(mLikerContainerId)) - var mRelatedPostsContainer = mDevice.findObject(UiSelector().resourceId(mRelatedPostsId)) - var mSwipeForMore = mDevice.findObject(UiSelector().textContains("Swipe for more")) - var mFooter = mDevice.findObject(UiSelector().resourceId(mFooterId)) + private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + private val likerContainer = device.findObject(UiSelector().resourceId(buildResourceId("liker_faces_container"))) + private val relatedPostsContainer = device.findObject( + UiSelector().resourceId(buildResourceId("container_related_posts")) + ) + private val swipeForMore = device.findObject(UiSelector().textContains("Swipe for more")) + private val recyclerView = UiScrollable(UiSelector().resourceId(buildResourceId("recycler_view"))) + private val savePostsForLater = device.findObject(UiSelector().text("Save Posts for Later")) + private val okButton = device.findObject(UiSelector().text("OK")) + private val bookmarkButtonSelector = UiSelector().resourceId(buildResourceId("bookmark")) + + private fun buildResourceId(id: String): String { + val packageName = InstrumentationRegistry.getInstrumentation().targetContext.packageName + return "$packageName:id/$id" + } + fun waitUntilLoaded(): ReaderViewPage { - mRelatedPostsContainer.waitForExists(WPSupportUtils.DEFAULT_TIMEOUT.toLong()) + relatedPostsContainer.waitForExists(WPSupportUtils.DEFAULT_TIMEOUT.toLong()) return this } fun likePost(): ReaderViewPage { tapLikeButton() - mLikerContainer.waitForExists(WPSupportUtils.DEFAULT_TIMEOUT.toLong()) + likerContainer.waitForExists(WPSupportUtils.DEFAULT_TIMEOUT.toLong()) return this } fun unlikePost(): ReaderViewPage { tapLikeButton() - mLikerContainer.waitUntilGone(WPSupportUtils.DEFAULT_TIMEOUT.toLong()) + likerContainer.waitUntilGone(WPSupportUtils.DEFAULT_TIMEOUT.toLong()) return this } private fun tapLikeButton() { - mSwipeForMore.waitUntilGone(WPSupportUtils.DEFAULT_TIMEOUT.toLong()) + swipeForMore.waitUntilGone(WPSupportUtils.DEFAULT_TIMEOUT.toLong()) // Even though it was working locally in simulator, tapping the footer buttons, // like 'mLikeButton.click()', was not working in CI. // The current workaround is to use arrows navigation. // Bring focus to the footer. First button is selected. - mDevice.pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN) + device.pressKeyCode(KeyEvent.KEYCODE_DPAD_DOWN) // Navigate to Like button. - mDevice.pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT) - mDevice.pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT) - mDevice.pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT) + device.pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT) + device.pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT) + device.pressKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT) // Click the Like button. - mDevice.pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER) + device.pressKeyCode(KeyEvent.KEYCODE_DPAD_CENTER) // Navigate back to the first footer button. - mDevice.pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT) - mDevice.pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT) - mDevice.pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT) + device.pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT) + device.pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT) + device.pressKeyCode(KeyEvent.KEYCODE_DPAD_LEFT) + } + + fun bookmarkPost(): ReaderViewPage { + tapBookmarkButton() + // Dismiss save posts locally dialog. + if (savePostsForLater.exists()) { + okButton.clickAndWaitForNewWindow() + } + return this + } + + fun removeBookmarkPost(): ReaderViewPage { + tapBookmarkButton() + return this + } + + private fun tapBookmarkButton() { + // Scroll to the bookmark button. + recyclerView.scrollIntoView(bookmarkButtonSelector) + // Tap the bookmark button. + device.findObject(bookmarkButtonSelector).clickAndWaitForNewWindow() } fun goBackToReader(): ReaderPage { - mDevice.pressBack() + device.pressBack() return ReaderPage() } @@ -77,7 +107,7 @@ class ReaderViewPage { } fun verifyPostLiked(): ReaderViewPage { - val isLiked = mDevice + val isLiked = device .findObject(UiSelector().textContains("You like this.")) .waitForExists(WPSupportUtils.DEFAULT_TIMEOUT.toLong()) TestCase.assertTrue("Liker was not displayed.", isLiked) @@ -85,8 +115,21 @@ class ReaderViewPage { } fun verifyPostNotLiked(): ReaderViewPage { - val likerDisplayed = mLikerContainer.exists() + val likerDisplayed = likerContainer.exists() TestCase.assertFalse("Liker faces container was displayed.", likerDisplayed) return this } + + fun verifyPostBookmarked(): ReaderViewPage { + val isBookmarked = device.findObject(UiSelector().text("Post saved")) + .waitForExists(WPSupportUtils.DEFAULT_TIMEOUT.toLong()) + TestCase.assertTrue("Snackbar was not displayed.", isBookmarked) + return this + } + + fun verifyPostNotBookmarked(): ReaderViewPage { + val isBookmarked = device.findObject(bookmarkButtonSelector).isSelected + TestCase.assertFalse("The bookmark button is selected", isBookmarked) + return this + } } diff --git a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/StatsPage.kt b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/StatsPage.kt index cc39fae61d96..0f515bfdc26a 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/StatsPage.kt +++ b/WordPress/src/androidTest/java/org/wordpress/android/e2e/pages/StatsPage.kt @@ -5,7 +5,6 @@ import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers import org.hamcrest.Matchers -import org.wordpress.android.BuildConfig import org.wordpress.android.R import org.wordpress.android.support.WPSupportUtils import org.wordpress.android.util.StatsKeyValueData @@ -67,41 +66,37 @@ class StatsPage { } fun assertVisits(visitsData: StatsVisitsData): StatsPage { - // Skip this check for JP because of the bug with Stats card load. - // See https://github.com/wordpress-mobile/WordPress-Android/issues/18065 - if (!BuildConfig.IS_JETPACK_APP) { - val cardStructure = Espresso.onView( - Matchers.allOf( - ViewMatchers.isDescendantOfA(visibleCoordinatorLayout), - ViewMatchers.withId(R.id.stats_block_list), - ViewMatchers.hasDescendant( - Matchers.allOf( - ViewMatchers.withText("Views"), - ViewMatchers.hasSibling(ViewMatchers.withText(visitsData.views)) - ) - ), - ViewMatchers.hasDescendant( - Matchers.allOf( - ViewMatchers.withText("Visitors"), - ViewMatchers.hasSibling(ViewMatchers.withText(visitsData.visitors)) - ) - ), - ViewMatchers.hasDescendant( - Matchers.allOf( - ViewMatchers.withText("Likes"), - ViewMatchers.hasSibling(ViewMatchers.withText(visitsData.likes)) - ) - ), - ViewMatchers.hasDescendant( - Matchers.allOf( - ViewMatchers.withText("Comments"), - ViewMatchers.hasSibling(ViewMatchers.withText(visitsData.comments)) - ) + val cardStructure = Espresso.onView( + Matchers.allOf( + ViewMatchers.isDescendantOfA(visibleCoordinatorLayout), + ViewMatchers.withId(R.id.stats_block_list), + ViewMatchers.hasDescendant( + Matchers.allOf( + ViewMatchers.withText("Views"), + ViewMatchers.hasSibling(ViewMatchers.withText(visitsData.views)) + ) + ), + ViewMatchers.hasDescendant( + Matchers.allOf( + ViewMatchers.withText("Visitors"), + ViewMatchers.hasSibling(ViewMatchers.withText(visitsData.visitors)) + ) + ), + ViewMatchers.hasDescendant( + Matchers.allOf( + ViewMatchers.withText("Likes"), + ViewMatchers.hasSibling(ViewMatchers.withText(visitsData.likes)) + ) + ), + ViewMatchers.hasDescendant( + Matchers.allOf( + ViewMatchers.withText("Comments"), + ViewMatchers.hasSibling(ViewMatchers.withText(visitsData.comments)) ) ) ) - cardStructure.check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed())) - } + ) + cardStructure.check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed())) return this } diff --git a/WordPress/src/androidTest/java/org/wordpress/android/support/WPSupportUtils.java b/WordPress/src/androidTest/java/org/wordpress/android/support/WPSupportUtils.java index c403130c4117..b0e3d51d923d 100644 --- a/WordPress/src/androidTest/java/org/wordpress/android/support/WPSupportUtils.java +++ b/WordPress/src/androidTest/java/org/wordpress/android/support/WPSupportUtils.java @@ -37,6 +37,7 @@ import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.hamcrest.TypeSafeMatcher; +import org.wordpress.android.BuildConfig; import org.wordpress.android.R; import org.wordpress.android.util.image.ImageType; @@ -818,6 +819,10 @@ public static void scrollIntoView(Integer scrollableContainerID, ViewInteraction } public static void dismissJetpackAdIfPresent() { + if (BuildConfig.IS_JETPACK_APP) { + return; + } + String jetpackAdText = "Stats, Reader, Notifications, and other features are powered by Jetpack."; ViewInteraction jetpackBanner = onView(withText(jetpackAdText)); diff --git a/WordPress/src/debug/AndroidManifest.xml b/WordPress/src/debug/AndroidManifest.xml index 3f5486d99084..b85e8966d005 100644 --- a/WordPress/src/debug/AndroidManifest.xml +++ b/WordPress/src/debug/AndroidManifest.xml @@ -4,11 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" package="org.wordpress.android"> - - - - - + + + + + diff --git a/WordPress/src/jetpack/AndroidManifest.xml b/WordPress/src/jetpack/AndroidManifest.xml index 992305182b40..c99333b66deb 100644 --- a/WordPress/src/jetpack/AndroidManifest.xml +++ b/WordPress/src/jetpack/AndroidManifest.xml @@ -6,7 +6,7 @@ - + @@ -28,24 +28,13 @@ - - - - - - + + + + + + + diff --git a/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/components/RepeatingColumn.kt b/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/components/RepeatingColumn.kt index 5f54dde9d07e..6ceeefb1453f 100644 --- a/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/components/RepeatingColumn.kt +++ b/WordPress/src/jetpack/java/org/wordpress/android/ui/accounts/login/components/RepeatingColumn.kt @@ -18,8 +18,8 @@ import androidx.compose.ui.unit.Constraints @Composable fun RepeatingColumn( position: Float, - repeat: Int = 3, modifier: Modifier = Modifier, + repeat: Int = 3, content: @Composable () -> Unit ) { Layout( diff --git a/WordPress/src/jetpack/res/drawable/img_quick_start_tour_illustration.xml b/WordPress/src/jetpack/res/drawable/img_quick_start_tour_illustration.xml index c0e13c6266cd..a34e076c6167 100644 --- a/WordPress/src/jetpack/res/drawable/img_quick_start_tour_illustration.xml +++ b/WordPress/src/jetpack/res/drawable/img_quick_start_tour_illustration.xml @@ -1,5 +1,6 @@ - - + android:strokeColor="#00000000" + tools:ignore="VectorPath"/> - + + + + + + + @@ -319,7 +329,7 @@ @@ -773,7 +783,7 @@ @@ -1064,6 +1074,13 @@ android:label="@string/blaze_activity_title" android:screenOrientation="portrait" android:theme="@style/WordPress.NoActionBar"/> + + + diff --git a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt index aaa8d0031726..900d19f88aa5 100644 --- a/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt +++ b/WordPress/src/main/java/org/wordpress/android/AppInitializer.kt @@ -604,6 +604,8 @@ class AppInitializer @Inject constructor( @Suppress("unused", "UNUSED_PARAMETER") @Subscribe(threadMode = ThreadMode.MAIN) fun onAuthenticationChanged(event: OnAuthenticationChanged) { + appConfig.refresh(appScope) + if (accountStore.hasAccessToken()) { // Make sure the Push Notification token is sent to our servers after a successful login GCMRegistrationIntentService.enqueueWork( diff --git a/WordPress/src/main/java/org/wordpress/android/models/JetpackPoweredScreen.kt b/WordPress/src/main/java/org/wordpress/android/models/JetpackPoweredScreen.kt index e3d3ac846bb9..4ba178317359 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/JetpackPoweredScreen.kt +++ b/WordPress/src/main/java/org/wordpress/android/models/JetpackPoweredScreen.kt @@ -1,5 +1,8 @@ package org.wordpress.android.models +import android.os.Parcelable +import androidx.annotation.AnimRes +import kotlinx.parcelize.Parcelize import org.wordpress.android.R import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringRes @@ -12,6 +15,29 @@ sealed interface JetpackPoweredScreen { val isPlural: Boolean } + @Parcelize + enum class WithStaticPoster( + val screen: WithDynamicText, + @AnimRes val animResLtr: Int, + @AnimRes val animResRtl: Int, + ) : Parcelable { + STATS( + screen = WithDynamicText.STATS, + animResLtr = R.raw.jp_stats_left, + animResRtl = R.raw.jp_stats_rtl, + ), + READER( + screen = WithDynamicText.READER, + animResLtr = R.raw.jp_reader_left, + animResRtl = R.raw.jp_reader_rtl, + ), + NOTIFICATIONS( + screen = WithDynamicText.NOTIFICATIONS, + animResLtr = R.raw.jp_notifications_left, + animResRtl = R.raw.jp_notifications_rtl, + ), + } + enum class WithStaticText( override val trackingName: String, ) : JetpackPoweredScreen { @@ -26,7 +52,7 @@ sealed interface JetpackPoweredScreen { override val trackingName: String, override val featureName: UiString, override val isPlural: Boolean, - ): JetpackPoweredScreenWithDynamicText { + ) : JetpackPoweredScreenWithDynamicText { ACTIVITY_LOG( trackingName = "activity_log", featureName = UiStringRes(R.string.activity_log), diff --git a/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java b/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java index a41446992b4a..56cca449f174 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java +++ b/WordPress/src/main/java/org/wordpress/android/models/NotificationsSettings.java @@ -22,7 +22,7 @@ public class NotificationsSettings { private JSONObject mWPComSettings; private LongSparseArray mBlogSettings; - // The main notification settings channels (displayed at root of NoticationsSettingsFragment) + // The main notification settings channels (displayed at root of NotificationsSettingsFragment) public enum Channel { OTHER, BLOGS, diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java index a5c8d3f49c06..198b63338e8c 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java @@ -3,15 +3,11 @@ import com.automattic.android.tracks.crashlogging.CrashLogging; import org.wordpress.android.ui.AddQuickPressShortcutActivity; -import org.wordpress.android.ui.CommentFullScreenDialogFragment; import org.wordpress.android.ui.JetpackConnectionResultActivity; -import org.wordpress.android.ui.JetpackRemoteInstallFragment; import org.wordpress.android.ui.ShareIntentReceiverActivity; import org.wordpress.android.ui.ShareIntentReceiverFragment; import org.wordpress.android.ui.WPWebViewActivity; import org.wordpress.android.ui.about.UnifiedAboutActivity; -import org.wordpress.android.ui.accounts.PostSignupInterstitialActivity; -import org.wordpress.android.ui.accounts.SignupEpilogueActivity; import org.wordpress.android.ui.accounts.signup.SignupEpilogueFragment; import org.wordpress.android.ui.activitylog.detail.ActivityLogDetailFragment; import org.wordpress.android.ui.activitylog.list.ActivityLogListFragment; @@ -20,7 +16,6 @@ import org.wordpress.android.ui.bloggingreminders.BloggingReminderBottomSheetFragment; import org.wordpress.android.ui.bloggingreminders.BloggingReminderTimePicker; import org.wordpress.android.ui.comments.CommentDetailFragment; -import org.wordpress.android.ui.comments.CommentsDetailActivity; import org.wordpress.android.ui.comments.EditCommentActivity; import org.wordpress.android.ui.comments.unified.EditCancelDialogFragment; import org.wordpress.android.ui.comments.unified.UnifiedCommentDetailsFragment; @@ -30,7 +25,6 @@ import org.wordpress.android.ui.comments.unified.UnifiedCommentsDetailsActivity; import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditFragment; import org.wordpress.android.ui.debug.cookies.DebugCookiesFragment; -import org.wordpress.android.ui.domains.DomainRegistrationActivity; import org.wordpress.android.ui.domains.DomainRegistrationDetailsFragment; import org.wordpress.android.ui.domains.DomainRegistrationResultFragment; import org.wordpress.android.ui.domains.DomainSuggestionsFragment; @@ -50,7 +44,6 @@ import org.wordpress.android.ui.main.AddContentAdapter; import org.wordpress.android.ui.main.MainBottomSheetFragment; import org.wordpress.android.ui.main.MeFragment; -import org.wordpress.android.ui.main.SitePickerActivity; import org.wordpress.android.ui.main.SitePickerAdapter; import org.wordpress.android.ui.main.WPMainActivity; import org.wordpress.android.ui.media.MediaBrowserActivity; @@ -141,9 +134,7 @@ import org.wordpress.android.ui.quickstart.QuickStartFullScreenDialogFragment; import org.wordpress.android.ui.reader.CommentNotificationsBottomSheetFragment; import org.wordpress.android.ui.reader.ReaderBlogFragment; -import org.wordpress.android.ui.reader.ReaderCommentListActivity; import org.wordpress.android.ui.reader.ReaderPostDetailFragment; -import org.wordpress.android.ui.reader.ReaderPostListActivity; import org.wordpress.android.ui.reader.ReaderPostListFragment; import org.wordpress.android.ui.reader.ReaderPostPagerActivity; import org.wordpress.android.ui.reader.ReaderSearchActivity; @@ -170,7 +161,6 @@ import org.wordpress.android.ui.reader.views.ReaderWebView; import org.wordpress.android.ui.sitecreation.theme.DesignPreviewFragment; import org.wordpress.android.ui.stats.StatsConnectJetpackActivity; -import org.wordpress.android.ui.stats.refresh.lists.StatsListFragment; import org.wordpress.android.ui.stats.refresh.lists.widget.alltime.AllTimeWidgetBlockListProviderFactory; import org.wordpress.android.ui.stats.refresh.lists.widget.alltime.AllTimeWidgetListProvider; import org.wordpress.android.ui.stats.refresh.lists.widget.alltime.StatsAllTimeWidget; @@ -210,12 +200,8 @@ public interface AppComponent { void inject(PostUploadHandler object); - void inject(SignupEpilogueActivity object); - void inject(SignupEpilogueFragment object); - void inject(PostSignupInterstitialActivity object); - void inject(JetpackConnectionResultActivity object); void inject(StatsConnectJetpackActivity object); @@ -228,12 +214,8 @@ public interface AppComponent { void inject(CommentDetailFragment object); - void inject(CommentFullScreenDialogFragment object); - void inject(EditCommentActivity object); - void inject(CommentsDetailActivity object); - void inject(MeFragment object); void inject(MyProfileActivity object); @@ -242,8 +224,6 @@ public interface AppComponent { void inject(AccountSettingsFragment object); - void inject(SitePickerActivity object); - void inject(SitePickerAdapter object); void inject(SiteSettingsFragment object); @@ -324,8 +304,6 @@ public interface AppComponent { void inject(NotificationsDetailListFragment object); - void inject(ReaderCommentListActivity object); - void inject(ReaderSubsActivity object); void inject(ReaderUpdateLogic object); @@ -360,8 +338,6 @@ public interface AppComponent { void inject(ReaderPostPagerActivity object); - void inject(ReaderPostListActivity object); - void inject(ReaderBlogFragment object); void inject(ReaderBlogAdapter object); @@ -418,8 +394,6 @@ public interface AppComponent { void inject(PublicizeServiceAdapter object); - void inject(JetpackRemoteInstallFragment jetpackRemoteInstallFragment); - void inject(PlansListAdapter object); void inject(PlanDetailsFragment object); @@ -448,10 +422,6 @@ public interface AppComponent { void inject(TodayWidgetBlockListProviderFactory object); - void inject(StatsListFragment object); - - void inject(DomainRegistrationActivity object); - void inject(EditPostPublishSettingsFragment object); void inject(PostDatePickerDialogFragment object); diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java index c80723422816..ee49d99e3f93 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ApplicationModule.java @@ -12,31 +12,13 @@ import com.tenor.android.core.network.IApiClient; import org.wordpress.android.BuildConfig; -import org.wordpress.android.ui.CommentFullScreenDialogFragment; -import org.wordpress.android.ui.accounts.signup.SettingsUsernameChangerFragment; -import org.wordpress.android.ui.accounts.signup.UsernameChangerFullScreenDialogFragment; -import org.wordpress.android.ui.debug.DebugSettingsFragment; -import org.wordpress.android.ui.domains.DomainRegistrationDetailsFragment.CountryPickerDialogFragment; -import org.wordpress.android.ui.domains.DomainRegistrationDetailsFragment.StatePickerDialogFragment; import org.wordpress.android.ui.jetpack.backup.download.BackupDownloadStep; import org.wordpress.android.ui.jetpack.backup.download.BackupDownloadStepsProvider; import org.wordpress.android.ui.jetpack.restore.RestoreStep; import org.wordpress.android.ui.jetpack.restore.RestoreStepsProvider; import org.wordpress.android.ui.mediapicker.loader.TenorGifClient; -import org.wordpress.android.ui.posts.BasicDialog; -import org.wordpress.android.ui.reader.ReaderPostWebViewCachingFragment; -import org.wordpress.android.ui.reader.subfilter.SubfilterPageFragment; import org.wordpress.android.ui.sitecreation.SiteCreationStep; import org.wordpress.android.ui.sitecreation.SiteCreationStepsProvider; -import org.wordpress.android.ui.stats.refresh.StatsViewAllFragment; -import org.wordpress.android.ui.stats.refresh.lists.StatsListFragment; -import org.wordpress.android.ui.stats.refresh.lists.detail.StatsDetailFragment; -import org.wordpress.android.ui.stats.refresh.lists.sections.insights.management.InsightsManagementFragment; -import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetColorSelectionDialogFragment; -import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetConfigureFragment; -import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetDataTypeSelectionDialogFragment; -import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetSiteSelectionDialogFragment; -import org.wordpress.android.ui.stats.refresh.lists.widget.minified.StatsMinifiedWidgetConfigureFragment; import org.wordpress.android.util.wizard.WizardManager; import org.wordpress.android.viewmodel.helpers.ConnectionStatus; import org.wordpress.android.viewmodel.helpers.ConnectionStatusLiveData; @@ -45,7 +27,6 @@ import dagger.Module; import dagger.Provides; import dagger.android.AndroidInjectionModule; -import dagger.android.ContributesAndroidInjector; import dagger.hilt.InstallIn; import dagger.hilt.android.qualifiers.ApplicationContext; import dagger.hilt.components.SingletonComponent; @@ -57,60 +38,6 @@ public abstract class ApplicationModule { @Binds abstract Context bindContext(Application application); - @ContributesAndroidInjector - abstract StatsListFragment contributeStatListFragment(); - - @ContributesAndroidInjector - abstract StatsViewAllFragment contributeStatsViewAllFragment(); - - @ContributesAndroidInjector - abstract InsightsManagementFragment contributeInsightsManagementFragment(); - - @ContributesAndroidInjector - abstract StatsDetailFragment contributeStatsDetailFragment(); - - @ContributesAndroidInjector - abstract CountryPickerDialogFragment contributeCountryPickerDialogFragment(); - - @ContributesAndroidInjector - abstract StatePickerDialogFragment contributeCStatePickerDialogFragment(); - - @ContributesAndroidInjector - abstract StatsWidgetConfigureFragment contributeStatsViewsWidgetConfigureFragment(); - - @ContributesAndroidInjector - abstract StatsWidgetSiteSelectionDialogFragment contributeSiteSelectionDialogFragment(); - - @ContributesAndroidInjector - abstract StatsWidgetColorSelectionDialogFragment contributeViewModeSelectionDialogFragment(); - - @ContributesAndroidInjector - abstract StatsMinifiedWidgetConfigureFragment contributeStatsMinifiedWidgetConfigureFragment(); - - @ContributesAndroidInjector - abstract StatsWidgetDataTypeSelectionDialogFragment contributeDataTypeSelectionDialogFragment(); - - @ContributesAndroidInjector - abstract CommentFullScreenDialogFragment contributecommentFullScreenDialogFragment(); - - @ContributesAndroidInjector - abstract UsernameChangerFullScreenDialogFragment contributeUsernameChangerFullScreenDialogFragment(); - - @ContributesAndroidInjector - abstract SettingsUsernameChangerFragment contributeSettingsUsernameChangerFragment(); - - @ContributesAndroidInjector - abstract ReaderPostWebViewCachingFragment contributeReaderPostWebViewCachingFragment(); - - @ContributesAndroidInjector - abstract SubfilterPageFragment contributeSubfilterPageFragment(); - - @ContributesAndroidInjector - abstract DebugSettingsFragment contributeDebugSettingsFragment(); - - @ContributesAndroidInjector - abstract BasicDialog contributeBasicDialog(); - @Provides public static SharedPreferences provideSharedPrefs(@ApplicationContext Context context) { return PreferenceManager.getDefaultSharedPreferences(context); diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java index e0f80248ddfa..c4a3a4893e8f 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java @@ -3,7 +3,6 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; -import org.wordpress.android.ui.JetpackRemoteInstallViewModel; import org.wordpress.android.ui.accounts.LoginEpilogueViewModel; import org.wordpress.android.ui.accounts.LoginViewModel; import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel; @@ -148,11 +147,6 @@ abstract class ViewModelModule { @ViewModelKey(SubfilterPageViewModel.class) abstract ViewModel subfilterPageViewModel(SubfilterPageViewModel viewModel); - @Binds - @IntoMap - @ViewModelKey(JetpackRemoteInstallViewModel.class) - abstract ViewModel jetpackRemoteInstallViewModel(JetpackRemoteInstallViewModel viewModel); - @Binds @IntoMap @ViewModelKey(QuickStartViewModel.class) 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 f77b6237990e..a224b08206c3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -69,6 +69,8 @@ import org.wordpress.android.ui.jetpack.scan.ScanActivity; import org.wordpress.android.ui.jetpack.scan.details.ThreatDetailsActivity; import org.wordpress.android.ui.jetpack.scan.history.ScanHistoryActivity; +import org.wordpress.android.ui.jetpackoverlay.JetpackStaticPosterActivity; +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallActivity; import org.wordpress.android.ui.main.MeActivity; import org.wordpress.android.ui.main.SitePickerActivity; import org.wordpress.android.ui.main.SitePickerAdapter.SitePickerMode; @@ -145,6 +147,7 @@ import static org.wordpress.android.analytics.AnalyticsTracker.Stat.STATS_ACCESS_ERROR; import static org.wordpress.android.editor.gutenberg.GutenbergEditorFragment.ARG_STORY_BLOCK_ID; import static org.wordpress.android.imageeditor.preview.PreviewImageFragment.ARG_EDIT_IMAGE_DATA; +import static org.wordpress.android.login.LoginMode.JETPACK_LOGIN_ONLY; import static org.wordpress.android.login.LoginMode.WPCOM_LOGIN_ONLY; import static org.wordpress.android.push.NotificationsProcessingService.ARG_NOTIFICATION_TYPE; import static org.wordpress.android.ui.WPWebViewActivity.ENCODING_UTF8; @@ -162,6 +165,7 @@ import static org.wordpress.android.ui.stories.StoryComposerActivity.KEY_POST_LOCAL_ID; import static org.wordpress.android.viewmodel.activitylog.ActivityLogDetailViewModelKt.ACTIVITY_LOG_ARE_BUTTONS_VISIBLE_KEY; import static org.wordpress.android.viewmodel.activitylog.ActivityLogDetailViewModelKt.ACTIVITY_LOG_ID_KEY; +import static org.wordpress.android.viewmodel.activitylog.ActivityLogDetailViewModelKt.ACTIVITY_LOG_IS_DASHBOARD_CARD_ENTRY_KEY; import static org.wordpress.android.viewmodel.activitylog.ActivityLogDetailViewModelKt.ACTIVITY_LOG_IS_RESTORE_HIDDEN_KEY; import static org.wordpress.android.viewmodel.activitylog.ActivityLogViewModelKt.ACTIVITY_LOG_REWINDABLE_ONLY_KEY; @@ -289,7 +293,7 @@ public static void showStockMediaPickerForResult(Activity activity, public static void startJetpackInstall(Context context, JetpackConnectionSource source, SiteModel site) { Intent intent = new Intent(context, JetpackRemoteInstallActivity.class); intent.putExtra(WordPress.SITE, site); - intent.putExtra(JetpackRemoteInstallFragment.TRACKING_SOURCE_KEY, source); + intent.putExtra(JetpackRemoteInstallActivity.TRACKING_SOURCE_KEY, source); context.startActivity(intent); } @@ -818,6 +822,26 @@ public static void viewActivityLogDetailForResult( activity.startActivityForResult(intent, RequestCodes.ACTIVITY_LOG_DETAIL); } + public static void viewActivityLogDetailFromDashboardCard( + Activity activity, + SiteModel site, + String activityId, + Boolean isRewindable + ) { + Map properties = new HashMap<>(); + properties.put(ACTIVITY_LOG_ACTIVITY_ID_KEY, activityId); + properties.put(SOURCE_TRACK_EVENT_PROPERTY_KEY, ACTIVITY_LOG_TRACK_EVENT_PROPERTY_VALUE); + AnalyticsUtils.trackWithSiteDetails(AnalyticsTracker.Stat.ACTIVITY_LOG_DETAIL_OPENED, site, properties); + + Intent intent = new Intent(activity, ActivityLogDetailActivity.class); + intent.putExtra(WordPress.SITE, site); + intent.putExtra(ACTIVITY_LOG_ID_KEY, activityId); + intent.putExtra(ACTIVITY_LOG_IS_DASHBOARD_CARD_ENTRY_KEY, true); + intent.putExtra(ACTIVITY_LOG_ARE_BUTTONS_VISIBLE_KEY, isRewindable); + intent.putExtra(SOURCE_TRACK_EVENT_PROPERTY_KEY, ACTIVITY_LOG_TRACK_EVENT_PROPERTY_VALUE); + activity.startActivity(intent); + } + public static void viewScan(Activity activity, SiteModel site) { if (site == null) { ToastUtils.showToast(activity, R.string.blog_not_found, ToastUtils.Duration.SHORT); @@ -1469,6 +1493,7 @@ public static void showSignInForResultJetpackOnly(Activity activity) { Intent intent = new Intent(activity, LoginActivity.class); intent.setFlags( Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + JETPACK_LOGIN_ONLY.putInto(intent); activity.startActivityForResult(intent, RequestCodes.ADD_ACCOUNT); } @@ -1849,4 +1874,9 @@ public static void openPromoteWithBlaze(@NonNull Context context, intent.putExtra(ARG_BLAZE_FLOW_SOURCE, source); context.startActivity(intent); } + + public static void showJetpackStaticPoster(@NonNull Context context) { + Intent intent = new Intent(context, JetpackStaticPosterActivity.class); + context.startActivity(intent); + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java index da9489a8c9af..b6e3cd7ad806 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/AddQuickPressShortcutActivity.java @@ -71,7 +71,7 @@ public void onCreate(Bundle savedInstanceState) { @Override public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/CollapseFullScreenDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/CollapseFullScreenDialogFragment.java index 808cacf81de9..7e694dfa9634 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/CollapseFullScreenDialogFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/CollapseFullScreenDialogFragment.java @@ -14,6 +14,8 @@ import android.view.ViewGroup; import android.view.Window; +import androidx.activity.ComponentDialog; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -146,12 +148,14 @@ public void setConfirmEnabled(boolean enabled) { public Dialog onCreateDialog(Bundle savedInstanceState) { initBuilderArguments(); - Dialog dialog = new Dialog(requireContext(), getTheme()) { + ComponentDialog dialog = (ComponentDialog) super.onCreateDialog(savedInstanceState); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { @Override - public void onBackPressed() { + public void handleOnBackPressed() { onCollapseClicked(); } }; + dialog.getOnBackPressedDispatcher().addCallback(this, callback); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); return dialog; @@ -297,7 +301,7 @@ private void initToolbar(View view) { } } - public void onBackPressed() { + public void collapse() { if (isAdded()) { onCollapseClicked(); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/CommentFullScreenDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/CommentFullScreenDialogFragment.kt index 69a366df3b40..d960bf09b9f4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/CommentFullScreenDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/CommentFullScreenDialogFragment.kt @@ -13,7 +13,7 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.TextView import androidx.fragment.app.Fragment -import dagger.android.support.AndroidSupportInjection +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -29,6 +29,7 @@ import org.wordpress.android.viewmodel.observeEvent import org.wordpress.android.widgets.SuggestionAutoCompleteText import javax.inject.Inject +@AndroidEntryPoint class CommentFullScreenDialogFragment : Fragment(), CollapseFullScreenDialogContent { @Inject lateinit var viewModel: CommentFullScreenDialogViewModel @@ -126,11 +127,6 @@ class CommentFullScreenDialogFragment : Fragment(), CollapseFullScreenDialogCont dialogController = controller } - override fun onAttach(context: Context) { - super.onAttach(context) - AndroidSupportInjection.inject(this) - } - companion object { const val RESULT_REPLY = "RESULT_REPLY" const val RESULT_SELECTION_START = "RESULT_SELECTION_START" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java index b07b8084ac39..42f7004390a8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/FullScreenDialogFragment.java @@ -14,6 +14,8 @@ import android.view.ViewGroup; import android.view.Window; +import androidx.activity.ComponentDialog; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.ColorRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -166,12 +168,14 @@ public void dismiss() { public Dialog onCreateDialog(Bundle savedInstanceState) { initBuilderArguments(); - Dialog dialog = new Dialog(getActivity(), getTheme()) { + ComponentDialog dialog = (ComponentDialog) super.onCreateDialog(savedInstanceState); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { @Override - public void onBackPressed() { + public void handleOnBackPressed() { onDismissClicked(); } }; + dialog.getOnBackPressedDispatcher().addCallback(this, callback); dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); return dialog; @@ -329,12 +333,6 @@ private void initToolbar(View view) { } } - public void onBackPressed() { - if (isAdded()) { - onDismissClicked(); - } - } - protected void onConfirmClicked() { boolean isConsumed = ((FullScreenDialogContent) mFragment).onConfirmClicked(mController); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/FullscreenBottomSheetDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/FullscreenBottomSheetDialogFragment.kt index 60f5d0d95a43..93580ce380d6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/FullscreenBottomSheetDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/FullscreenBottomSheetDialogFragment.kt @@ -2,11 +2,10 @@ package org.wordpress.android.ui import android.content.DialogInterface import android.os.Bundle -import android.view.View import android.view.WindowManager -import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.wordpress.android.util.extensions.fillScreen /** * Customises [BottomSheetDialogFragment] for fullscreen @@ -20,24 +19,7 @@ abstract class FullscreenBottomSheetDialogFragment : BottomSheetDialogFragment() } override fun onCreateDialog(savedInstanceState: Bundle?) = BottomSheetDialog(requireContext(), getTheme()).apply { - fillTheScreen(this) + this.fillScreen(isDraggable = true) window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) } - - private fun fillTheScreen(dialog: BottomSheetDialog) { - dialog.setOnShowListener { - dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet)?.let { - val behaviour = BottomSheetBehavior.from(it) - setupFullHeight(it) - behaviour.skipCollapsed = true - behaviour.state = BottomSheetBehavior.STATE_EXPANDED - } - } - } - - private fun setupFullHeight(bottomSheet: View) { - val layoutParams = bottomSheet.layoutParams - layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT - bottomSheet.layoutParams = layoutParams - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionResultActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionResultActivity.java index 69159dc25f3f..93dc0b76cc6f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionResultActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionResultActivity.java @@ -5,6 +5,7 @@ import android.os.Bundle; import android.text.TextUtils; +import androidx.activity.OnBackPressedCallback; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; @@ -22,6 +23,7 @@ import org.wordpress.android.util.SiteUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import javax.inject.Inject; @@ -54,6 +56,15 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.stats_loading_activity); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + finishAndGoBackToSource(); + } + }; + + getOnBackPressedDispatcher().addCallback(this, callback); Toolbar toolbar = findViewById(R.id.toolbar_main); setSupportActionBar(toolbar); @@ -109,12 +120,6 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } } - @Override - public void onBackPressed() { - super.onBackPressed(); - finishAndGoBackToSource(); - } - private void trackResult() { if (!TextUtils.isEmpty(mReason)) { if (mReason.equals(ALREADY_CONNECTED)) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionUtils.java index b8fa46db4d97..b6c435982d9c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionUtils.java @@ -7,13 +7,13 @@ /** * Wraps utility methods for Jetpack */ -class JetpackConnectionUtils { +public class JetpackConnectionUtils { /** * Adds source as a parameter to the tracked Stat * @param stat to be tracked * @param source of tracking */ - static void trackWithSource(AnalyticsTracker.Stat stat, JetpackConnectionSource source) { + public static void trackWithSource(AnalyticsTracker.Stat stat, JetpackConnectionSource source) { HashMap sourceMap = new HashMap<>(); sourceMap.put("source", source.toString()); AnalyticsTracker.track(stat, sourceMap); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionWebViewActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionWebViewActivity.java index 921e49b36574..6f9f1759c31f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionWebViewActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/JetpackConnectionWebViewActivity.java @@ -44,7 +44,12 @@ public static void startJetpackConnectionFlow(Context context, JetpackConnection } } - static void startManualFlow(Context context, JetpackConnectionSource source, SiteModel site, boolean authorized) { + public static void startManualFlow( + Context context, + JetpackConnectionSource source, + SiteModel site, + boolean authorized + ) { String url = "https://wordpress.com/jetpack/connect?" + "url=" + site.getUrl() + "&mobile_redirect=" + JETPACK_CONNECTION_DEEPLINK diff --git a/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallActivity.kt deleted file mode 100644 index 273062232db3..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallActivity.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.wordpress.android.ui - -import android.os.Bundle -import android.view.MenuItem -import org.wordpress.android.R -import org.wordpress.android.analytics.AnalyticsTracker.Stat.INSTALL_JETPACK_CANCELLED -import org.wordpress.android.databinding.JetpackRemoteInstallActivityBinding -import org.wordpress.android.ui.JetpackConnectionUtils.trackWithSource -import org.wordpress.android.ui.JetpackRemoteInstallFragment.Companion.TRACKING_SOURCE_KEY - -class JetpackRemoteInstallActivity : LocaleAwareActivity() { - public override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - with(JetpackRemoteInstallActivityBinding.inflate(layoutInflater)) { - setContentView(root) - setSupportActionBar(toolbarLayout.toolbarMain) - } - - supportActionBar?.let { - it.setHomeButtonEnabled(true) - it.setDisplayHomeAsUpEnabled(true) - it.setTitle(R.string.jetpack) - } - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - return super.onOptionsItemSelected(item) - } - - override fun onBackPressed() { - trackWithSource( - INSTALL_JETPACK_CANCELLED, - intent.getSerializableExtra(TRACKING_SOURCE_KEY) as JetpackConnectionSource - ) - super.onBackPressed() - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallFragment.kt deleted file mode 100644 index e12f6b5a29fd..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallFragment.kt +++ /dev/null @@ -1,158 +0,0 @@ -package org.wordpress.android.ui - -import android.app.Activity -import android.content.Intent -import android.content.res.ColorStateList -import android.os.Bundle -import android.view.View -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider -import org.wordpress.android.R -import org.wordpress.android.WordPress -import org.wordpress.android.databinding.JetpackRemoteInstallFragmentBinding -import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.login.LoginMode -import org.wordpress.android.ui.JetpackRemoteInstallViewModel.JetpackResultActionData -import org.wordpress.android.ui.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.CONNECT -import org.wordpress.android.ui.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.LOGIN -import org.wordpress.android.ui.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.MANUAL_INSTALL -import org.wordpress.android.ui.RequestCodes.JETPACK_LOGIN -import org.wordpress.android.ui.accounts.LoginActivity -import org.wordpress.android.util.AppLog -import javax.inject.Inject - -class JetpackRemoteInstallFragment : Fragment(R.layout.jetpack_remote_install_fragment) { - @Inject - lateinit var viewModelFactory: ViewModelProvider.Factory - private lateinit var viewModel: JetpackRemoteInstallViewModel - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - with(JetpackRemoteInstallFragmentBinding.bind(view)) { - initDagger() - initViewModel(savedInstanceState) - } - } - - private fun initDagger() { - (requireActivity().application as WordPress).component().inject(this) - } - - private fun JetpackRemoteInstallFragmentBinding.initViewModel(savedInstanceState: Bundle?) { - requireActivity().let { activity -> - val intent = activity.intent - val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel - val source = intent.getSerializableExtra(TRACKING_SOURCE_KEY) as JetpackConnectionSource - val retrievedState = savedInstanceState?.getSerializable(VIEW_STATE) as? JetpackRemoteInstallViewState.Type - viewModel = ViewModelProvider( - this@JetpackRemoteInstallFragment, viewModelFactory - ).get(JetpackRemoteInstallViewModel::class.java) - viewModel.start(site, retrievedState) - - initLiveViewStateObserver() - - viewModel.liveActionOnResult.observe(viewLifecycleOwner, Observer { result -> - if (result != null) { - when (result.action) { - MANUAL_INSTALL -> onManualInstallResultAction(activity, source, result) - LOGIN -> onLoginResultAction(activity, source) - CONNECT -> onConnectResultAction(activity, source, result) - } - } - }) - } - } - - private fun onManualInstallResultAction( - activity: FragmentActivity, - source: JetpackConnectionSource, - result: JetpackResultActionData - ) { - JetpackConnectionWebViewActivity.startManualFlow( - activity, - source, - result.site, - result.loggedIn - ) - activity.finish() - } - - @Suppress("DEPRECATION") - private fun onLoginResultAction( - activity: FragmentActivity, - source: JetpackConnectionSource - ) { - val loginIntent = Intent(activity, LoginActivity::class.java) - LoginMode.JETPACK_STATS.putInto(loginIntent) - loginIntent.putExtra(LoginActivity.ARG_JETPACK_CONNECT_SOURCE, source) - startActivityForResult(loginIntent, JETPACK_LOGIN) - } - - private fun onConnectResultAction( - activity: FragmentActivity, - source: JetpackConnectionSource, - result: JetpackResultActionData - ) { - JetpackConnectionWebViewActivity.startJetpackConnectionFlow( - activity, - source, - result.site, - result.loggedIn - ) - activity.finish() - } - - private fun JetpackRemoteInstallFragmentBinding.initLiveViewStateObserver() { - viewModel.liveViewState.observe(viewLifecycleOwner, Observer { viewState -> - if (viewState != null) { - if (viewState is JetpackRemoteInstallViewState.Error) { - AppLog.e(AppLog.T.JETPACK_REMOTE_INSTALL, "An error occurred while installing Jetpack") - } - jetpackInstallIcon.setImageResource(viewState.icon) - if (viewState.iconTint != null) { - jetpackInstallIcon.imageTintList = ColorStateList.valueOf( - ContextCompat.getColor( - jetpackInstallIcon.context, viewState.iconTint - ) - ) - } else { - jetpackInstallIcon.imageTintList = null - } - jetpackInstallTitle.setText(viewState.titleResource) - jetpackInstallMessage.setText(viewState.messageResource) - if (viewState.buttonResource != null) { - jetpackInstallButton.visibility = View.VISIBLE - jetpackInstallButton.setText(viewState.buttonResource) - } else { - jetpackInstallButton.visibility = View.GONE - } - jetpackInstallButton.setOnClickListener { viewState.onClick() } - jetpackInstallProgress.visibility = if (viewState.progressBarVisible) View.VISIBLE else View.GONE - } - }) - } - - @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == JETPACK_LOGIN && resultCode == Activity.RESULT_OK) { - val site = requireActivity().intent!!.getSerializableExtra(WordPress.SITE) as SiteModel - viewModel.onLogin(site.id) - } - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - viewModel.liveViewState.value?.type?.let { - outState.putSerializable(VIEW_STATE, it) - } - } - - companion object { - const val TRACKING_SOURCE_KEY = "tracking_source_key" - private const val VIEW_STATE = "view_state_key" - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallViewModel.kt deleted file mode 100644 index 2af7a3f6dd80..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallViewModel.kt +++ /dev/null @@ -1,190 +0,0 @@ -package org.wordpress.android.ui - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import org.wordpress.android.analytics.AnalyticsTracker -import org.wordpress.android.analytics.AnalyticsTracker.Stat.INSTALL_JETPACK_REMOTE_RESTART -import org.wordpress.android.analytics.AnalyticsTracker.Stat.INSTALL_JETPACK_REMOTE_START -import org.wordpress.android.fluxc.Dispatcher -import org.wordpress.android.fluxc.generated.JetpackActionBuilder -import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.store.AccountStore -import org.wordpress.android.fluxc.store.JetpackStore -import org.wordpress.android.fluxc.store.JetpackStore.OnJetpackInstalled -import org.wordpress.android.fluxc.store.SiteStore -import org.wordpress.android.ui.JetpackRemoteInstallViewModel.JetpackResultActionData.Action -import org.wordpress.android.ui.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.CONNECT -import org.wordpress.android.ui.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.LOGIN -import org.wordpress.android.ui.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.MANUAL_INSTALL -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Error -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Installed -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Start -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Type -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Type.ERROR -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Type.INSTALLED -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Type.INSTALLING -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Type.START -import org.wordpress.android.viewmodel.SingleLiveEvent -import javax.inject.Inject - -private const val INVALID_CREDENTIALS = "INVALID_CREDENTIALS" -private const val FORBIDDEN = "FORBIDDEN" -private const val INSTALL_FAILURE = "INSTALL_FAILURE" -private const val INSTALL_RESPONSE_ERROR = "INSTALL_RESPONSE_ERROR" -private const val LOGIN_FAILURE = "LOGIN_FAILURE" -private const val SITE_IS_JETPACK = "SITE_IS_JETPACK" -private const val ACTIVATION_ON_INSTALL_FAILURE = "ACTIVATION_ON_INSTALL_FAILURE" -private const val ACTIVATION_RESPONSE_ERROR = "ACTIVATION_RESPONSE_ERROR" -private const val ACTIVATION_FAILURE = "ACTIVATION_FAILURE" -private val BLOCKING_FAILURES = listOf( - FORBIDDEN, - INSTALL_FAILURE, - INSTALL_RESPONSE_ERROR, - LOGIN_FAILURE, - INVALID_CREDENTIALS, - ACTIVATION_ON_INSTALL_FAILURE, - ACTIVATION_RESPONSE_ERROR, - ACTIVATION_FAILURE -) -private const val CONTEXT = "JetpackRemoteInstall" -private const val EMPTY_TYPE = "EMPTY_TYPE" -private const val EMPTY_MESSAGE = "EMPTY_MESSAGE" - -class JetpackRemoteInstallViewModel -@Inject constructor( - private val dispatcher: Dispatcher, - private val accountStore: AccountStore, - private val siteStore: SiteStore, - /** - * JetpackStore needs to be injected here as otherwise FluxC doesn't accept emitted events. - */ - @Suppress("unused") private val jetpackStore: JetpackStore -) : ViewModel() { - private val mutableViewState = MutableLiveData() - val liveViewState: LiveData = mutableViewState - private val mutableActionOnResult = SingleLiveEvent() - val liveActionOnResult: LiveData = mutableActionOnResult - private var siteModel: SiteModel? = null - - init { - dispatcher.register(this) - } - - fun start(site: SiteModel, type: Type?) { - siteModel = site - // Init state only if it's empty - if (mutableViewState.value == null) { - mutableViewState.value = type.toState(site) - } - } - - override fun onCleared() { - super.onCleared() - dispatcher.unregister(this) - } - - fun onLogin(siteId: Int) { - connect(siteId) - } - - private fun Type?.toState(site: SiteModel): JetpackRemoteInstallViewState { - if (this == null) { - return Start { start(site) } - } - return when (this) { - START -> Start { start(site) } - INSTALLING -> { - startRemoteInstall(site) - JetpackRemoteInstallViewState.Installing - } - INSTALLED -> Installed { connect(site.id) } - ERROR -> Error { restart(site) } - } - } - - private fun start(site: SiteModel) { - AnalyticsTracker.track(INSTALL_JETPACK_REMOTE_START) - startRemoteInstall(site) - } - - private fun restart(site: SiteModel) { - AnalyticsTracker.track(INSTALL_JETPACK_REMOTE_RESTART) - startRemoteInstall(site) - } - - private fun connect(siteId: Int) { - val hasAccessToken = accountStore.hasAccessToken() - val action = if (hasAccessToken) { - AnalyticsTracker.track(AnalyticsTracker.Stat.INSTALL_JETPACK_REMOTE_CONNECT) - CONNECT - } else { - AnalyticsTracker.track(AnalyticsTracker.Stat.INSTALL_JETPACK_REMOTE_LOGIN) - LOGIN - } - triggerResultAction(siteId, action, hasAccessToken) - } - - private fun startRemoteInstall(site: SiteModel) { - mutableViewState.postValue(JetpackRemoteInstallViewState.Installing) - dispatcher.dispatch(JetpackActionBuilder.newInstallJetpackAction(site)) - } - - private fun triggerResultAction( - siteId: Int, - action: Action, - hasAccessToken: Boolean = accountStore.hasAccessToken() - ) { - mutableActionOnResult.postValue( - JetpackResultActionData( - siteStore.getSiteByLocalId(siteId)!!, - hasAccessToken, - action - ) - ) - } - - // Network Callbacks - @Suppress("unused") - @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onEventsUpdated(event: OnJetpackInstalled) { - val site = siteModel ?: return - if (event.isError) { - AnalyticsTracker.track( - AnalyticsTracker.Stat.INSTALL_JETPACK_REMOTE_FAILED, - CONTEXT, - event.error?.apiError ?: EMPTY_TYPE, - event.error?.message ?: EMPTY_MESSAGE - ) - when { - event.error?.apiError == SITE_IS_JETPACK -> { - AnalyticsTracker.track(AnalyticsTracker.Stat.INSTALL_JETPACK_REMOTE_COMPLETED) - mutableViewState.postValue(Installed { connect(site.id) }) - } - BLOCKING_FAILURES.contains(event.error?.apiError) -> { - AnalyticsTracker.track(AnalyticsTracker.Stat.INSTALL_JETPACK_REMOTE_START_MANUAL_FLOW) - triggerResultAction(site.id, MANUAL_INSTALL) - } - else -> mutableViewState.postValue(Error { - restart(site) - }) - } - return - } - if (event.success) { - AnalyticsTracker.track(AnalyticsTracker.Stat.INSTALL_JETPACK_REMOTE_COMPLETED) - mutableViewState.postValue(Installed { connect(site.id) }) - } else { - AnalyticsTracker.track(AnalyticsTracker.Stat.INSTALL_JETPACK_REMOTE_FAILED) - mutableViewState.postValue(Error { restart(site) }) - } - } - - data class JetpackResultActionData(val site: SiteModel, val loggedIn: Boolean, val action: Action) { - enum class Action { - LOGIN, MANUAL_INSTALL, CONNECT - } - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallViewState.kt b/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallViewState.kt deleted file mode 100644 index 095758d1daf4..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/JetpackRemoteInstallViewState.kt +++ /dev/null @@ -1,63 +0,0 @@ -package org.wordpress.android.ui - -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import org.wordpress.android.R -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Type.ERROR -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Type.INSTALLED -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Type.INSTALLING -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Type.START - -sealed class JetpackRemoteInstallViewState( - val type: Type, - @StringRes val titleResource: Int, - @StringRes val messageResource: Int, - @DrawableRes val icon: Int, - @ColorRes val iconTint: Int? = null, - @StringRes val buttonResource: Int? = null, - open val onClick: () -> Unit = {}, - val progressBarVisible: Boolean = false -) { - data class Start(override val onClick: () -> Unit) : JetpackRemoteInstallViewState( - START, - R.string.install_jetpack, - R.string.install_jetpack_message, - icon = R.drawable.ic_plans_white_24dp, - iconTint = R.color.jetpack_green, - buttonResource = R.string.install_jetpack_continue, - onClick = onClick - ) - - object Installing : JetpackRemoteInstallViewState( - INSTALLING, - R.string.installing_jetpack, - R.string.installing_jetpack_message, - icon = R.drawable.ic_plans_white_24dp, - iconTint = R.color.jetpack_green, - progressBarVisible = true - ) - - data class Installed(override val onClick: () -> Unit) : JetpackRemoteInstallViewState( - INSTALLED, - R.string.jetpack_installed, - R.string.jetpack_installed_message, - icon = R.drawable.ic_plans_white_24dp, - buttonResource = R.string.install_jetpack_continue, - iconTint = R.color.jetpack_green, - onClick = onClick - ) - - data class Error(override val onClick: () -> Unit) : JetpackRemoteInstallViewState( - ERROR, - R.string.jetpack_installation_problem, - R.string.jetpack_installation_problem_message, - icon = R.drawable.ic_warning, - buttonResource = R.string.install_jetpack_retry, - onClick = onClick - ) - - enum class Type { - START, INSTALLING, INSTALLED, ERROR - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ShortcutsNavigator.java b/WordPress/src/main/java/org/wordpress/android/ui/ShortcutsNavigator.java index 08e2945ef1b8..2ff58cd2fa96 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ShortcutsNavigator.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ShortcutsNavigator.java @@ -32,8 +32,12 @@ public void showTargetScreen(String action, Activity activity, SiteModel current case OPEN_STATS: AnalyticsTracker.track(AnalyticsTracker.Stat.SHORTCUT_STATS_CLICKED); if (!mJetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()) { - ActivityLauncher.viewBlogStats(activity, currentSite); - } + if (mJetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { + ActivityLauncher.showJetpackStaticPoster(activity); + } else { + ActivityLauncher.viewBlogStats(activity, currentSite); + } + } break; case CREATE_NEW_POST: AnalyticsTracker.track(AnalyticsTracker.Stat.SHORTCUT_NEW_POST_CLICKED); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/TextInputDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/TextInputDialogFragment.java index f10a16a997e4..30cf4f387d03 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/TextInputDialogFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/TextInputDialogFragment.java @@ -54,7 +54,7 @@ public static TextInputDialogFragment newInstance(String title, @Override @SuppressWarnings("deprecation") public Dialog onCreateDialog(Bundle savedInstanceState) { - LayoutInflater layoutInflater = LayoutInflater.from(getActivity()); + LayoutInflater layoutInflater = getActivity().getLayoutInflater(); //noinspection InflateParams View promptView = layoutInflater.inflate(R.layout.text_input_dialog, null); AlertDialog.Builder alertDialogBuilder = new MaterialAlertDialogBuilder(getActivity()); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/WPTooltipView.kt b/WordPress/src/main/java/org/wordpress/android/ui/WPTooltipView.kt index dcaf4e163818..093b7315d7cd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/WPTooltipView.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/WPTooltipView.kt @@ -137,7 +137,7 @@ class WPTooltipView @JvmOverloads constructor( .alpha(0f) .setDuration(HIDE_ANIMATION_DURATION) .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { super.onAnimationEnd(animation) visibility = View.GONE } 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 1dc437490e16..f12310eba6ea 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/WPWebViewActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/WPWebViewActivity.java @@ -22,6 +22,7 @@ import android.widget.RelativeLayout.LayoutParams; import android.widget.TextView; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; @@ -60,6 +61,7 @@ import org.wordpress.android.util.UrlUtils; import org.wordpress.android.util.WPUrlUtils; import org.wordpress.android.util.WPWebViewClient; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import org.wordpress.android.viewmodel.wpwebview.WPWebViewViewModel; import org.wordpress.android.viewmodel.wpwebview.WPWebViewViewModel.NavBarUiState; import org.wordpress.android.viewmodel.wpwebview.WPWebViewViewModel.PreviewModeSelectorStatus; @@ -161,6 +163,21 @@ public class WPWebViewActivity extends WebViewActivity implements ErrorManagedWe public void onCreate(Bundle savedInstanceState) { ((WordPress) getApplication()).component().inject(this); super.onCreate(savedInstanceState); + + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (mWebView.canGoBack()) { + mWebView.goBack(); + refreshBackForwardNavButtons(); + } else { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + mViewModel.track(Stat.WEBVIEW_DISMISSED); + setResultIfNeeded(); + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); } @Override @@ -916,18 +933,6 @@ private void setResultIfNeededAndFinish() { finish(); } - @Override - public void onBackPressed() { - if (mWebView.canGoBack()) { - mWebView.goBack(); - refreshBackForwardNavButtons(); - } else { - super.onBackPressed(); - mViewModel.track(Stat.WEBVIEW_DISMISSED); - setResultIfNeeded(); - } - } - @Override public void onStart() { super.onStart(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java index 43ccf8c2f184..cb67fdb3f6f0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/WebViewActivity.java @@ -6,11 +6,13 @@ import android.view.Window; import android.webkit.WebView; +import androidx.activity.OnBackPressedCallback; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import org.wordpress.android.R; import org.wordpress.android.WordPress; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import java.util.Map; @@ -39,6 +41,19 @@ public void onCreate(Bundle savedInstanceState) { configureView(); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (mWebView != null && mWebView.canGoBack()) { + mWebView.goBack(); + } else { + cancel(); + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + mWebView = (WebView) findViewById(R.id.webView); mWebView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); // Setting this user agent makes Calypso sites hide any WordPress UIs (e.g. Masterbar, banners, etc.). @@ -145,16 +160,6 @@ public void loadUrl(String url, Map additionalHttpHeaders) { mWebView.loadUrl(url, additionalHttpHeaders); } - @Override - public void onBackPressed() { - if (mWebView != null && mWebView.canGoBack()) { - mWebView.goBack(); - } else { - cancel(); - super.onBackPressed(); - } - } - @Override public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == android.R.id.home) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/HelpActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/HelpActivity.kt index 0e4e66f13810..1550cb5839eb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/HelpActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/HelpActivity.kt @@ -148,7 +148,7 @@ class HelpActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) @@ -362,7 +362,8 @@ class HelpActivity : LocaleAwareActivity() { SCAN_SCREEN_HELP("origin:scan-screen-help"), JETPACK_MIGRATION_HELP("origin:jetpack-migration-help"), JETPACK_INSTALL_FULL_PLUGIN_ONBOARDING("origin:jp-install-full-plugin-overlay"), - JETPACK_INSTALL_FULL_PLUGIN_ERROR("origin:jp-install-full-plugin-error"); + JETPACK_INSTALL_FULL_PLUGIN_ERROR("origin:jp-install-full-plugin-error"), + JETPACK_REMOTE_INSTALL_PLUGIN_ERROR("origin:jp-remote-install-plugin-error"); override fun toString(): String { return stringValue diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java index 2446676c933b..be6f9584d790 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java @@ -161,6 +161,7 @@ protected void onCreate(Bundle savedInstanceState) { switch (getLoginMode()) { case FULL: + case JETPACK_LOGIN_ONLY: mUnifiedLoginTracker.setSource(Source.DEFAULT); mIsSignupFromLoginEnabled = mBuildConfigWrapper.isSignupEnabled(); loginFromPrologue(); @@ -295,7 +296,7 @@ private LoginEmailFragment getLoginEmailFragment() { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } @@ -320,6 +321,7 @@ private void loggedInAndFinish(ArrayList oldSitesIds, boolean doLoginUp AppPrefs.setIsJetpackMigrationInProgress(false); switch (getLoginMode()) { case FULL: + case JETPACK_LOGIN_ONLY: case WPCOM_LOGIN_ONLY: if (!mSiteStore.hasSite() && AppPrefs.shouldShowPostSignupInterstitial() && !doLoginUpdate) { ActivityLauncher.showPostSignupInterstitial(this); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueActivity.java index 0271557de23d..8e31fe1a8e60 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueActivity.java @@ -16,11 +16,13 @@ import org.wordpress.android.ui.accounts.LoginNavigationEvents.CloseWithResultOk; import org.wordpress.android.ui.accounts.LoginNavigationEvents.CreateNewSite; import org.wordpress.android.ui.accounts.LoginNavigationEvents.SelectSite; +import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowJetpackIndividualPluginOverlay; import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowNoJetpackSites; import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowPostSignupInterstitialScreen; import org.wordpress.android.ui.accounts.login.LoginEpilogueFragment; import org.wordpress.android.ui.accounts.login.LoginEpilogueListener; import org.wordpress.android.ui.accounts.login.jetpack.LoginNoSitesFragment; +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginFragment; import org.wordpress.android.ui.main.SitePickerActivity; import org.wordpress.android.ui.mysite.SelectedSiteRepository; import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource; @@ -80,6 +82,8 @@ private void initObservers() { closeWithResultOk(); } else if (loginEvent instanceof ShowNoJetpackSites) { showNoJetpackSites(); + } else if (loginEvent instanceof ShowJetpackIndividualPluginOverlay) { + showJetpackIndividualPluginOverlay(); } }); } @@ -150,6 +154,10 @@ private void showFragment(Fragment fragment, String tag, boolean applySlideAnima fragmentTransaction.commit(); } + private void showJetpackIndividualPluginOverlay() { + WPJetpackIndividualPluginFragment.show(getSupportFragmentManager()); + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueViewModel.kt index 565ef74fd283..8d376dfa35be 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginEpilogueViewModel.kt @@ -3,17 +3,21 @@ package org.wordpress.android.ui.accounts import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginHelper import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.util.BuildConfigWrapper import org.wordpress.android.viewmodel.Event - import javax.inject.Inject class LoginEpilogueViewModel @Inject constructor( private val appPrefsWrapper: AppPrefsWrapper, private val buildConfigWrapper: BuildConfigWrapper, - private val siteStore: SiteStore + private val siteStore: SiteStore, + private val wpJetpackIndividualPluginHelper: WPJetpackIndividualPluginHelper, ) : ViewModel() { private val _navigationEvents = MediatorLiveData>() val navigationEvents: LiveData> = _navigationEvents @@ -52,4 +56,21 @@ class LoginEpilogueViewModel @Inject constructor( fun onLoginFinished(doLoginUpdate: Boolean) { if (doLoginUpdate && !siteStore.hasSite()) handleNoSitesFound() } + + fun onSiteListLoaded() { + // don't check if already shown + if (_navigationEvents.value?.peekContent() == LoginNavigationEvents.ShowJetpackIndividualPluginOverlay) return + + viewModelScope.launch { + val showOverlay = wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay() + if (showOverlay) { + delay(DELAY_BEFORE_SHOWING_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY) + _navigationEvents.postValue(Event(LoginNavigationEvents.ShowJetpackIndividualPluginOverlay)) + } + } + } + + companion object { + private const val DELAY_BEFORE_SHOWING_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY = 500L + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginNavigationEvents.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginNavigationEvents.kt index 7d6d7d9278d1..f40cdd64de71 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginNavigationEvents.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginNavigationEvents.kt @@ -13,4 +13,5 @@ sealed class LoginNavigationEvents { object CloseWithResultOk : LoginNavigationEvents() object ShowEmailLoginScreen : LoginNavigationEvents() object ShowLoginViaSiteAddressScreen : LoginNavigationEvents() + object ShowJetpackIndividualPluginOverlay : LoginNavigationEvents() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/PostSignupInterstitialActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/PostSignupInterstitialActivity.kt index 63f831845130..bc86e7841c6e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/PostSignupInterstitialActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/PostSignupInterstitialActivity.kt @@ -1,21 +1,25 @@ package org.wordpress.android.ui.accounts import android.os.Bundle +import androidx.activity.addCallback import androidx.lifecycle.ViewModelProvider import com.google.android.material.button.MaterialButton +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R -import org.wordpress.android.WordPress import org.wordpress.android.databinding.PostSignupInterstitialActivityBinding import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.LocaleAwareActivity +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginFragment import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel.NavigationAction import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel.NavigationAction.DISMISS +import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel.NavigationAction.SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel.NavigationAction.START_SITE_CONNECTION_FLOW import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel.NavigationAction.START_SITE_CREATION_FLOW import javax.inject.Inject +@AndroidEntryPoint class PostSignupInterstitialActivity : LocaleAwareActivity() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -23,7 +27,6 @@ class PostSignupInterstitialActivity : LocaleAwareActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - (application as WordPress).component().inject(this) LoginFlowThemeHelper.injectMissingCustomAttributes(theme) @@ -31,6 +34,9 @@ class PostSignupInterstitialActivity : LocaleAwareActivity() { .get(PostSignupInterstitialViewModel::class.java) val binding = PostSignupInterstitialActivityBinding.inflate(layoutInflater) setContentView(binding.root) + + onBackPressedDispatcher.addCallback(this) { viewModel.onBackButtonPressed() } + with(binding) { viewModel.onInterstitialShown() createNewSiteButton().setOnClickListener { viewModel.onCreateNewSiteButtonPressed() } @@ -38,7 +44,7 @@ class PostSignupInterstitialActivity : LocaleAwareActivity() { dismissButton().setOnClickListener { viewModel.onDismissButtonPressed() } } - viewModel.navigationAction.observe(this, { executeAction(it) }) + viewModel.navigationAction.observe(this) { executeAction(it) } } private fun PostSignupInterstitialActivityBinding.createNewSiteButton() = @@ -50,13 +56,10 @@ class PostSignupInterstitialActivity : LocaleAwareActivity() { private fun PostSignupInterstitialActivityBinding.dismissButton() = root.findViewById(R.id.dismiss_button) - override fun onBackPressed() { - viewModel.onBackButtonPressed() - } - private fun executeAction(navigationAction: NavigationAction) = when (navigationAction) { START_SITE_CREATION_FLOW -> startSiteCreationFlow() START_SITE_CONNECTION_FLOW -> startSiteConnectionFlow() + SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY -> showJetpackIndividualPluginOverlay() DISMISS -> dismiss() } @@ -74,4 +77,8 @@ class PostSignupInterstitialActivity : LocaleAwareActivity() { ActivityLauncher.viewReader(this) finish() } + + private fun showJetpackIndividualPluginOverlay() { + WPJetpackIndividualPluginFragment.show(supportFragmentManager) + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java index 07731e1ba802..f156effdb53e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/SignupEpilogueActivity.java @@ -5,7 +5,6 @@ import androidx.fragment.app.FragmentTransaction; import org.wordpress.android.R; -import org.wordpress.android.WordPress; import org.wordpress.android.fluxc.store.SiteStore; import org.wordpress.android.ui.ActivityLauncher; import org.wordpress.android.ui.LocaleAwareActivity; @@ -14,6 +13,9 @@ import javax.inject.Inject; +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint public class SignupEpilogueActivity extends LocaleAwareActivity implements SignupEpilogueListener { public static final String EXTRA_SIGNUP_DISPLAY_NAME = "EXTRA_SIGNUP_DISPLAY_NAME"; public static final String EXTRA_SIGNUP_EMAIL_ADDRESS = "EXTRA_SIGNUP_EMAIL_ADDRESS"; @@ -28,7 +30,6 @@ public class SignupEpilogueActivity extends LocaleAwareActivity implements Signu @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ((WordPress) getApplication()).component().inject(this); LoginFlowThemeHelper.injectMissingCustomAttributes(getTheme()); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginEpilogueFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginEpilogueFragment.java index 345d10911ca3..9e58fd67aef0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginEpilogueFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/LoginEpilogueFragment.java @@ -228,6 +228,8 @@ public void onAfterLoad() { mBottomShadow.setVisibility(View.GONE); } } + + mParentViewModel.onSiteListLoaded(); }); } }; @@ -396,4 +398,9 @@ protected void onLoginFinished() { mParentViewModel.onLoginFinished(mDoLoginUpdate); } + + @Override + protected boolean isJetpackAppLogin() { + return mDoLoginUpdate && mBuildConfigWrapper.isJetpackApp(); + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesFragment.kt index cc9b6983e869..2c024a02a88f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesFragment.kt @@ -3,7 +3,7 @@ package org.wordpress.android.ui.accounts.login.jetpack import android.content.Context import android.os.Bundle import android.view.View -import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint @@ -42,7 +42,7 @@ class LoginNoSitesFragment : Fragment(R.layout.jetpack_login_empty_view) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - initBackPressHandler() + requireActivity().onBackPressedDispatcher.addCallback(this) { viewModel.onBackPressed() } with(JetpackLoginEmptyViewBinding.bind(view)) { initContentViews() initClickListeners() @@ -141,16 +141,4 @@ class LoginNoSitesFragment : Fragment(R.layout.jetpack_login_empty_view) { super.onResume() viewModel.onFragmentResume() } - - private fun initBackPressHandler() { - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback( - true - ) { - override fun handleOnBackPressed() { - viewModel.onBackPressed() - } - }) - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesViewModel.kt index b879e5c0bc9f..a3f2ae9dc7b9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesViewModel.kt @@ -17,6 +17,7 @@ import org.wordpress.android.ui.accounts.UnifiedLoginTracker import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Step import org.wordpress.android.ui.accounts.login.jetpack.LoginNoSitesViewModel.State.NoUser import org.wordpress.android.ui.accounts.login.jetpack.LoginNoSitesViewModel.State.ShowUser +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import java.io.Serializable @@ -58,8 +59,9 @@ class LoginNoSitesViewModel @Inject constructor( _uiModel.postValue(UiModel(state = state)) } - private fun buildStateFromSavedInstanceState(savedInstanceState: Bundle) = - savedInstanceState.getSerializable(KEY_STATE) as State + private fun buildStateFromSavedInstanceState(savedInstanceState: Bundle) = requireNotNull( + savedInstanceState.getSerializableCompat(KEY_STATE) + ) private fun buildStateFromAccountStore() = accountStore.account?.let { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginSiteCheckErrorFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginSiteCheckErrorFragment.kt index 8e78cdb78f0d..81646d32dd90 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginSiteCheckErrorFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/jetpack/LoginSiteCheckErrorFragment.kt @@ -3,7 +3,7 @@ package org.wordpress.android.ui.accounts.login.jetpack import android.content.Context import android.os.Bundle import android.view.View -import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import dagger.hilt.android.AndroidEntryPoint @@ -54,7 +54,7 @@ class LoginSiteCheckErrorFragment : Fragment(R.layout.jetpack_login_empty_view) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - initBackPressHandler() + requireActivity().onBackPressedDispatcher.addCallback(this) { viewModel.onBackPressed() } with(JetpackLoginEmptyViewBinding.bind(view)) { ActivityUtils.hideKeyboardForced(view) initErrorMessageView() @@ -113,16 +113,4 @@ class LoginSiteCheckErrorFragment : Fragment(R.layout.jetpack_login_empty_view) unifiedLoginTracker.track(step = Step.NOT_A_JETPACK_SITE) } - - private fun initBackPressHandler() { - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback( - true - ) { - override fun handleOnBackPressed() { - viewModel.onBackPressed() - } - }) - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/BaseUsernameChangerFullScreenDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/BaseUsernameChangerFullScreenDialogFragment.java index fe42b57b488a..3411b33eaffb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/BaseUsernameChangerFullScreenDialogFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/BaseUsernameChangerFullScreenDialogFragment.java @@ -17,6 +17,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.text.HtmlCompat; +import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SimpleItemAnimator; @@ -49,13 +50,11 @@ import javax.inject.Inject; -import dagger.android.support.DaggerFragment; - /** * Created so that the base suggestions functionality can become shareable as similar functionality is being used in the * the Account settings & sign-up flow to change the username. */ -public abstract class BaseUsernameChangerFullScreenDialogFragment extends DaggerFragment implements +public abstract class BaseUsernameChangerFullScreenDialogFragment extends Fragment implements FullScreenDialogContent, OnUsernameSelectedListener { private ProgressBar mProgressBar; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SettingsUsernameChangerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SettingsUsernameChangerFragment.kt index 755a9b252a18..ebc92a99a3bc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SettingsUsernameChangerFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/SettingsUsernameChangerFragment.kt @@ -14,6 +14,7 @@ import androidx.appcompat.app.AlertDialog import androidx.core.text.HtmlCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import dagger.hilt.android.AndroidEntryPoint import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.wordpress.android.R @@ -33,6 +34,7 @@ import org.wordpress.android.widgets.WPDialogSnackbar /** * Allows the user to change their username from the Account Settings screen. */ +@AndroidEntryPoint class SettingsUsernameChangerFragment : BaseUsernameChangerFullScreenDialogFragment() { private lateinit var dialogController: FullScreenDialogController diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/UsernameChangerFullScreenDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/UsernameChangerFullScreenDialogFragment.kt index 7a2751bfd8cd..c0f5da2259e4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/UsernameChangerFullScreenDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/signup/UsernameChangerFullScreenDialogFragment.kt @@ -3,6 +3,7 @@ package org.wordpress.android.ui.accounts.signup import android.os.Bundle import android.text.Spanned import androidx.core.text.HtmlCompat +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker.Stat.SIGNUP_SOCIAL_EPILOGUE_USERNAME_SUGGESTIONS_FAILED import org.wordpress.android.ui.FullScreenDialogFragment.FullScreenDialogController @@ -10,6 +11,7 @@ import org.wordpress.android.ui.FullScreenDialogFragment.FullScreenDialogControl /** * Implements functionality specific to the Username Changer functionality in the sign-up flow. */ +@AndroidEntryPoint class UsernameChangerFullScreenDialogFragment : BaseUsernameChangerFullScreenDialogFragment() { override fun getSuggestionsFailedStat() = SIGNUP_SOCIAL_EPILOGUE_USERNAME_SUGGESTIONS_FAILED override fun canHeaderTextLiveUpdate() = true diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailActivity.kt index 38c9e1c9924f..981d9833b900 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailActivity.kt @@ -25,7 +25,7 @@ class ActivityLogDetailActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailFragment.kt index b542ce477863..ee3a5268b50e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailFragment.kt @@ -28,11 +28,14 @@ import org.wordpress.android.ui.notifications.utils.NotificationsUtilsWrapper import org.wordpress.android.ui.reader.tracker.ReaderTracker import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.JetpackBrandingUtils +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.models.JetpackPoweredScreen import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.image.ImageType.AVATAR_WITH_BACKGROUND import org.wordpress.android.viewmodel.activitylog.ACTIVITY_LOG_ARE_BUTTONS_VISIBLE_KEY import org.wordpress.android.viewmodel.activitylog.ACTIVITY_LOG_ID_KEY +import org.wordpress.android.viewmodel.activitylog.ACTIVITY_LOG_IS_DASHBOARD_CARD_ENTRY_KEY import org.wordpress.android.viewmodel.activitylog.ACTIVITY_LOG_IS_RESTORE_HIDDEN_KEY import org.wordpress.android.viewmodel.activitylog.ActivityLogDetailViewModel import org.wordpress.android.viewmodel.observeEvent @@ -83,8 +86,9 @@ class ActivityLogDetailFragment : Fragment(R.layout.activity_log_item_detail) { val (site, activityLogId) = sideAndActivityId(savedInstanceState, activity.intent) val areButtonsVisible = areButtonsVisible(savedInstanceState, activity.intent) val isRestoreHidden = isRestoreHidden(savedInstanceState, activity.intent) + val isDashboardCardEntry = isDashboardCardEntry(savedInstanceState, activity.intent) - viewModel.start(site, activityLogId, areButtonsVisible, isRestoreHidden) + viewModel.start(site, activityLogId, areButtonsVisible, isRestoreHidden, isDashboardCardEntry) } if (jetpackBrandingUtils.shouldShowJetpackBranding()) { @@ -109,21 +113,21 @@ class ActivityLogDetailFragment : Fragment(R.layout.activity_log_item_detail) { } private fun ActivityLogItemDetailBinding.setupObservers() { - viewModel.activityLogItem.observe(viewLifecycleOwner, { activityLogModel -> + viewModel.activityLogItem.observe(viewLifecycleOwner) { activityLogModel -> loadLogItem(activityLogModel, requireActivity()) - }) + } - viewModel.restoreVisible.observe(viewLifecycleOwner, { available -> + viewModel.restoreVisible.observe(viewLifecycleOwner) { available -> activityRestoreButton.visibility = if (available == true) View.VISIBLE else View.GONE - }) - viewModel.downloadBackupVisible.observe(viewLifecycleOwner, { available -> + } + viewModel.downloadBackupVisible.observe(viewLifecycleOwner) { available -> activityDownloadBackupButton.visibility = if (available == true) View.VISIBLE else View.GONE - }) - viewModel.multisiteVisible.observe(viewLifecycleOwner, { available -> + } + viewModel.multisiteVisible.observe(viewLifecycleOwner) { available -> checkAndShowMultisiteMessage(available) - }) + } - viewModel.navigationEvents.observeEvent(viewLifecycleOwner, { + viewModel.navigationEvents.observeEvent(viewLifecycleOwner) { when (it) { is ShowBackupDownload -> ActivityLauncher.showBackupDownloadForResult( requireActivity(), @@ -141,9 +145,9 @@ class ActivityLogDetailFragment : Fragment(R.layout.activity_log_item_detail) { ) is ShowDocumentationPage -> ActivityLauncher.openUrlExternal(requireContext(), it.url) } - }) + } - viewModel.handleFormattableRangeClick.observe(viewLifecycleOwner, { range -> + viewModel.handleFormattableRangeClick.observe(viewLifecycleOwner) { range -> if (range != null) { formattableContentClickHandler.onClick( requireActivity(), @@ -151,7 +155,7 @@ class ActivityLogDetailFragment : Fragment(R.layout.activity_log_item_detail) { ReaderTracker.SOURCE_ACTIVITY_LOG_DETAIL ) } - }) + } viewModel.showJetpackPoweredBottomSheet.observeEvent(viewLifecycleOwner) { JetpackPoweredBottomSheetFragment @@ -221,7 +225,9 @@ class ActivityLogDetailFragment : Fragment(R.layout.activity_log_item_detail) { private fun sideAndActivityId(savedInstanceState: Bundle?, intent: Intent?) = when { savedInstanceState != null -> { - val site = savedInstanceState.getSerializable(WordPress.SITE) as SiteModel + val site = requireNotNull( + savedInstanceState.getSerializableCompat(WordPress.SITE) + ) val activityLogId = requireNotNull( savedInstanceState.getString( ACTIVITY_LOG_ID_KEY @@ -230,7 +236,9 @@ class ActivityLogDetailFragment : Fragment(R.layout.activity_log_item_detail) { site to activityLogId } intent != null -> { - val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel + val site = requireNotNull( + intent.getSerializableExtraCompat(WordPress.SITE) + ) val activityLogId = intent.getStringExtra(ACTIVITY_LOG_ID_KEY) as String site to activityLogId } @@ -253,12 +261,21 @@ class ActivityLogDetailFragment : Fragment(R.layout.activity_log_item_detail) { else -> throw Throwable("Couldn't initialize Activity Log view model") } + private fun isDashboardCardEntry(savedInstanceState: Bundle?, intent: Intent?) = when { + savedInstanceState != null -> + requireNotNull(savedInstanceState.getBoolean(ACTIVITY_LOG_IS_DASHBOARD_CARD_ENTRY_KEY, false)) + intent != null -> + intent.getBooleanExtra(ACTIVITY_LOG_IS_DASHBOARD_CARD_ENTRY_KEY, false) + else -> throw Throwable("Couldn't initialize Activity Log view model") + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putSerializable(WordPress.SITE, viewModel.site) outState.putString(ACTIVITY_LOG_ID_KEY, viewModel.activityLogId) outState.putBoolean(ACTIVITY_LOG_ARE_BUTTONS_VISIBLE_KEY, viewModel.areButtonsVisible) outState.putBoolean(ACTIVITY_LOG_IS_RESTORE_HIDDEN_KEY, viewModel.isRestoreHidden) + outState.putBoolean(ACTIVITY_LOG_IS_DASHBOARD_CARD_ENTRY_KEY, viewModel.isDashboardCardEntry) } private fun ActivityLogItemDetailBinding.setActorIcon(actorIcon: String?, showJetpackIcon: Boolean?) { 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 b944b25eadcd..abcb2aa59998 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 @@ -108,7 +108,7 @@ class ActivityLogListActivity : LocaleAwareActivity(), ScrollableViewInitialized override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) 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 473037a21c0b..c2a1e68566f3 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 @@ -29,6 +29,8 @@ import org.wordpress.android.ui.prefs.EmptyViewRecyclerView import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.NetworkUtils import org.wordpress.android.util.WPSwipeToRefreshHelper.buildSwipeToRefreshHelper +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.util.helpers.SwipeToRefreshHelper import org.wordpress.android.viewmodel.activitylog.ACTIVITY_LOG_REWINDABLE_ONLY_KEY import org.wordpress.android.viewmodel.activitylog.ActivityLogViewModel @@ -73,7 +75,7 @@ class ActivityLogListFragment : Fragment(R.layout.activity_log_list_fragment) { viewModel = ViewModelProvider( this@ActivityLogListFragment, viewModelFactory - ).get(ActivityLogViewModel::class.java) + )[ActivityLogViewModel::class.java] with(ActivityLogListFragmentBinding.bind(view)) { listView = logListView @@ -87,12 +89,14 @@ class ActivityLogListFragment : Fragment(R.layout.activity_log_list_fragment) { } } - val site = if (savedInstanceState == null) { - val nonNullIntent = checkNotNull(nonNullActivity.intent) - nonNullIntent.getSerializableExtra(WordPress.SITE) as SiteModel - } else { - savedInstanceState.getSerializable(WordPress.SITE) as SiteModel - } + val site = requireNotNull( + if (savedInstanceState == null) { + val nonNullIntent = checkNotNull(nonNullActivity.intent) + nonNullIntent.getSerializableExtraCompat(WordPress.SITE) + } else { + savedInstanceState.getSerializableCompat(WordPress.SITE) + } + ) val rewindableOnly = nonNullActivity.intent.getBooleanExtra(ACTIVITY_LOG_REWINDABLE_ONLY_KEY, false) logListView.setEmptyView(actionableEmptyView) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeFeatureUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeFeatureUtils.kt index c1ecb48e680a..4d87abb5a0d6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeFeatureUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeFeatureUtils.kt @@ -36,12 +36,13 @@ class BlazeFeatureUtils @Inject constructor( postModel.password.isEmpty() } - fun shouldShowBlazeEntryPoint(blazeStatusModel: BlazeStatusModel?, siteId: Long): Boolean { - val isEligible = blazeStatusModel?.isEligible == true - return isBlazeEnabled() && - isEligible && - !isPromoteWithBlazeCardHiddenByUser(siteId) - } + fun shouldShowBlazeCardEntryPoint(blazeStatusModel: BlazeStatusModel?, siteId: Long) = + isBlazeEnabled() && + blazeStatusModel?.isEligible == true && + !isPromoteWithBlazeCardHiddenByUser(siteId) + + fun shouldShowBlazeMenuEntryPoint(blazeStatusModel: BlazeStatusModel?) = + isBlazeEnabled() && blazeStatusModel?.isEligible == true fun track(stat: AnalyticsTracker.Stat, source: BlazeFlowSource) { analyticsTrackerWrapper.track( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeParentActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeParentActivity.kt index dcddc767ea3c..031fdf84bf64 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeParentActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/BlazeParentActivity.kt @@ -8,6 +8,8 @@ import org.wordpress.android.R import org.wordpress.android.ui.blaze.ui.blazeoverlay.BlazeOverlayFragment import org.wordpress.android.ui.blaze.ui.blazeoverlay.BlazeViewModel import org.wordpress.android.ui.blaze.ui.blazewebview.BlazeWebViewFragment +import org.wordpress.android.util.extensions.getParcelableExtraCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat const val ARG_EXTRA_BLAZE_UI_MODEL = "blaze_ui_model" const val ARG_BLAZE_FLOW_SOURCE = "blaze_flow_source" @@ -45,10 +47,10 @@ class BlazeParentActivity : AppCompatActivity() { } private fun getSource(): BlazeFlowSource { - return intent.getSerializableExtra(ARG_BLAZE_FLOW_SOURCE) as BlazeFlowSource + return requireNotNull(intent.getSerializableExtraCompat(ARG_BLAZE_FLOW_SOURCE)) } private fun getBlazeUiModel(): BlazeUIModel? { - return intent.getParcelableExtra(ARG_EXTRA_BLAZE_UI_MODEL) + return intent.getParcelableExtraCompat(ARG_EXTRA_BLAZE_UI_MODEL) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazeoverlay/BlazeOverlayFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazeoverlay/BlazeOverlayFragment.kt index 5e281b8c9127..24d30f4c9687 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazeoverlay/BlazeOverlayFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazeoverlay/BlazeOverlayFragment.kt @@ -13,14 +13,15 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -71,6 +72,9 @@ private val darkModePrimaryButtonColor = Color(0xFF1C1C1E) @Stable private val lightModePostThumbnailBackground = Color(0xD000000) +@Stable +private val darkModePostThumbnailBackground = Color(0xDFFFFFF) + @Stable private val bulletedTextColor = Color(0xFF666666) @@ -106,9 +110,7 @@ class BlazeOverlayFragment : Fragment() { } Scaffold( topBar = { OverlayTopBar(blazeUIModel) }, - ) { - BlazeOverlayContent(blazeUIModel, isDarkTheme) - } + ) { BlazeOverlayContent(blazeUIModel, isDarkTheme) } } @Composable @@ -146,11 +148,10 @@ class BlazeOverlayFragment : Fragment() { ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding( - top = Margin.ExtraLarge.value, - start = Margin.ExtraLarge.value, - end = Margin.ExtraLarge.value - ) + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(Margin.ExtraLarge.value) ) { Image( painterResource(id = R.drawable.ic_blaze_overlay_image), @@ -244,8 +245,23 @@ class BlazeOverlayFragment : Fragment() { end.linkTo(postContainer.end, 15.dp) }) Title( - title = uiModel.title, modifier = Modifier.constrainAs(title) { - top.linkTo(postContainer.top, 15.dp) + title = uiModel.title, modifier = Modifier + .constrainAs(title) { + top.linkTo(postContainer.top, 15.dp) + start.linkTo(postContainer.start, 20.dp) + uiModel.featuredImageUrl?.run { + end.linkTo(featuredImage.start, margin = 15.dp) + } ?: run { + end.linkTo(postContainer.end, margin = 20.dp) + } + width = Dimension.fillToConstraints + } + .wrapContentHeight() + ) + val url = createRef() + Url(url = uiModel.url, modifier = Modifier + .constrainAs(url) { + top.linkTo(title.bottom) start.linkTo(postContainer.start, 20.dp) uiModel.featuredImageUrl?.run { end.linkTo(featuredImage.start, margin = 15.dp) @@ -253,25 +269,14 @@ class BlazeOverlayFragment : Fragment() { end.linkTo(postContainer.end, margin = 20.dp) } width = Dimension.fillToConstraints - }.wrapContentHeight() - ) - val url = createRef() - Url(url = uiModel.url, modifier = Modifier.constrainAs(url) { - top.linkTo(title.bottom) - start.linkTo(postContainer.start, 20.dp) - uiModel.featuredImageUrl?.run { - end.linkTo(featuredImage.start, margin = 15.dp) - } ?: run { - end.linkTo(postContainer.end, margin = 20.dp) + height = Dimension.wrapContent } - width = Dimension.fillToConstraints - height = Dimension.wrapContent - }.padding(bottom = 15.dp)) + .padding(bottom = 15.dp)) } } private fun getThumbnailBackground(isInDarkTheme: Boolean): Color { - return if (isInDarkTheme) AppColor.DarkGray + return if (isInDarkTheme) darkModePostThumbnailBackground else lightModePostThumbnailBackground } @@ -320,16 +325,14 @@ class BlazeOverlayFragment : Fragment() { @Composable fun Subtitles(list: List, modifier: Modifier = Modifier) { - LazyColumn( + Column( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = modifier.padding( top = Margin.ExtraLarge.value, bottom = Margin.ExtraLarge.value ) ) { - items(list) { - BulletedText(it) - } + list.forEach { BulletedText(it) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazewebview/BlazeWebViewFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazewebview/BlazeWebViewFragment.kt index 069e7277464a..754127500c87 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazewebview/BlazeWebViewFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazewebview/BlazeWebViewFragment.kt @@ -184,7 +184,7 @@ class BlazeWebViewFragment: Fragment(), OnBlazeWebViewClientListener, viewLifecycleOwner, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { - // no op + blazeWebViewViewModel.handleOnBackPressed() } } ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazewebview/BlazeWebViewViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazewebview/BlazeWebViewViewModel.kt index a72f8e234a62..884da424fbac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazewebview/BlazeWebViewViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/blaze/ui/blazewebview/BlazeWebViewViewModel.kt @@ -218,6 +218,20 @@ class BlazeWebViewViewModel @Inject constructor( }?: return false } + fun handleOnBackPressed() { + val nonDismissableStep = nonDismissableHashConfig.getValue() + val completedStep = completedStepHashConfig.getValue() + + if (blazeFlowStep.label == nonDismissableStep) return + + if (blazeFlowStep.label == completedStep || blazeFlowStep == BlazeFlowStep.CAMPAIGNS_LIST) { + blazeFeatureUtils.trackBlazeFlowCompleted(blazeFlowSource) + } else { + blazeFeatureUtils.trackFlowCanceled(blazeFlowSource, blazeFlowStep) + } + postActionEvent(BlazeActionEvent.FinishActivity) + } + companion object { const val WPCOM_LOGIN_URL = "https://wordpress.com/wp-login.php" const val WPCOM_DOMAIN = ".wordpress.com" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt index b2fc0f2d9b60..9def06e2f14e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProvider.kt @@ -2,6 +2,7 @@ package org.wordpress.android.ui.bloggingprompts import org.wordpress.android.models.ReaderTag import org.wordpress.android.models.ReaderTagType +import org.wordpress.android.ui.reader.services.post.ReaderPostLogic object BloggingPromptsPostTagProvider { const val BLOGGING_PROMPT_TAG = "dailyprompt" @@ -13,7 +14,7 @@ object BloggingPromptsPostTagProvider { promptIdTag(promptId), promptIdTag(promptId), promptIdTag(promptId), - "", - ReaderTagType.SEARCH, + ReaderPostLogic.formatFullEndpointForTag(promptIdTag(promptId)), + ReaderTagType.FOLLOWED, ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingDialogFragment.kt index 7771b6f351bc..5e2ac3f8c626 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingDialogFragment.kt @@ -41,6 +41,7 @@ import org.wordpress.android.util.SnackbarItem.Action import org.wordpress.android.util.SnackbarItem.Info import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.extensions.exhaustive +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -96,9 +97,7 @@ class BloggingPromptsOnboardingDialogFragment : FeatureIntroductionDialogFragmen @Suppress("UseCheckOrError") override fun onAttach(context: Context) { super.onAttach(context) - arguments?.let { - dialogType = it.getSerializable(KEY_DIALOG_TYPE) as DialogType - } + arguments?.let { dialogType = requireNotNull(it.getSerializableCompat(KEY_DIALOG_TYPE)) } (requireActivity().applicationContext as WordPress).component().inject(this) if (dialogType == ONBOARDING && context !is BloggingPromptsReminderSchedulerListener) { throw IllegalStateException( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingUiStateMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingUiStateMapper.kt index 2cf3609acc83..70a4ff4ab21e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingUiStateMapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingUiStateMapper.kt @@ -10,11 +10,11 @@ import org.wordpress.android.ui.bloggingprompts.onboarding.BloggingPromptsOnboar import org.wordpress.android.ui.bloggingprompts.onboarding.BloggingPromptsOnboardingUiState.Ready import org.wordpress.android.ui.utils.UiString.UiStringPluralRes import org.wordpress.android.ui.utils.UiString.UiStringRes -import org.wordpress.android.util.config.BloggingPromptsEnhancementsFeatureConfig +import org.wordpress.android.util.config.BloggingPromptsSocialFeatureConfig import javax.inject.Inject class BloggingPromptsOnboardingUiStateMapper @Inject constructor( - private val bloggingPromptsEnhancementsFeatureConfig: BloggingPromptsEnhancementsFeatureConfig + private val bloggingPromptsSocialFeatureConfig: BloggingPromptsSocialFeatureConfig ) { @Suppress("MagicNumber") fun mapReady( @@ -30,7 +30,7 @@ class BloggingPromptsOnboardingUiStateMapper @Inject constructor( dummyRespondent ) - val trailingLabel = if (bloggingPromptsEnhancementsFeatureConfig.isEnabled()) { + val trailingLabel = if (bloggingPromptsSocialFeatureConfig.isEnabled()) { UiStringRes( R.string.my_site_blogging_prompt_card_view_answers ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/BloggingPromptsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/BloggingPromptsListActivity.kt index 1f47dbdfd643..f8f13b932d6e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/BloggingPromptsListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingprompts/promptslist/BloggingPromptsListActivity.kt @@ -26,7 +26,11 @@ class BloggingPromptsListActivity : LocaleAwareActivity() { setContent { AppTheme { val uiState by viewModel.uiStateFlow.collectAsState() - BloggingPromptsListScreen(uiState, ::onBackPressed, viewModel::onPromptListItemClicked) + BloggingPromptsListScreen( + uiState, + { onBackPressedDispatcher.onBackPressed() }, + viewModel::onPromptListItemClicked + ) } } observeActions() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/BloggingReminderBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/BloggingReminderBottomSheetFragment.kt index 46c150846f4c..7356d5aff0d3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/BloggingReminderBottomSheetFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/BloggingReminderBottomSheetFragment.kt @@ -1,7 +1,9 @@ package org.wordpress.android.ui.bloggingreminders +import android.Manifest import android.content.Context import android.content.DialogInterface +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -19,6 +21,8 @@ import org.wordpress.android.databinding.RecyclerViewPrimaryButtonBottomSheetBin import org.wordpress.android.ui.bloggingprompts.onboarding.BloggingPromptsOnboardingDialogFragment import org.wordpress.android.ui.bloggingprompts.onboarding.BloggingPromptsOnboardingDialogFragment.DialogType.INFORMATION import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.PermissionUtils +import org.wordpress.android.util.WPPermissionUtils import org.wordpress.android.util.extensions.disableAnimation import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -61,8 +65,10 @@ class BloggingReminderBottomSheetFragment : BottomSheetDialogFragment() { } } }) - viewModel = - ViewModelProvider(requireActivity(), viewModelFactory).get(BloggingRemindersViewModel::class.java) + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[BloggingRemindersViewModel::class.java] + + setPermissionState() + viewModel.uiState.observe(this@BloggingReminderBottomSheetFragment) { uiState -> (contentRecyclerView.adapter as? BloggingRemindersAdapter)?.submitList(uiState?.uiItems ?: listOf()) if (uiState?.primaryButton != null) { @@ -86,11 +92,10 @@ class BloggingReminderBottomSheetFragment : BottomSheetDialogFragment() { BloggingReminderUtils.observeTimePicker( viewModel.isTimePickerShowing, viewLifecycleOwner, - BloggingReminderTimePicker.TAG, - { - requireActivity().supportFragmentManager - } - ) + BloggingReminderTimePicker.TAG + ) { + requireActivity().supportFragmentManager + } savedInstanceState?.let { viewModel.restoreState(it) } @@ -101,6 +106,60 @@ class BloggingReminderBottomSheetFragment : BottomSheetDialogFragment() { } } + override fun onResume() { + super.onResume() + val hasPermission = PermissionUtils.checkNotificationsPermission(activity) + if (hasPermission) { + viewModel.onPermissionGranted() + } + } + + @Suppress("OVERRIDE_DEPRECATION") + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + val granted = WPPermissionUtils.setPermissionListAsked( + requireActivity(), + requestCode, + permissions, + grantResults, + false + ) + if (granted) { + viewModel.onPermissionGranted() + } else { + val isAlwaysDenied = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + WPPermissionUtils.isPermissionAlwaysDenied( + requireActivity(), + Manifest.permission.POST_NOTIFICATIONS + ) + viewModel.onPermissionDenied(isAlwaysDenied) + } + } + + private fun setPermissionState() { + val isAlwaysDenied = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + WPPermissionUtils.isPermissionAlwaysDenied( + requireActivity(), + Manifest.permission.POST_NOTIFICATIONS + ) + viewModel.setPermissionState(PermissionUtils.checkNotificationsPermission(activity), isAlwaysDenied) + + @Suppress("DEPRECATION") + viewModel.requestPermission.observeEvent(this@BloggingReminderBottomSheetFragment) { request: Boolean -> + if (request && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissions( + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + WPPermissionUtils.NOTIFICATIONS_PERMISSION_REQUEST_CODE + ) + } + } + viewModel.showDevicePermissionSettings + .observeEvent(this@BloggingReminderBottomSheetFragment) { show: Boolean -> + if (show) { + WPPermissionUtils.showNotificationsSettings(requireActivity()) + } + } + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) viewModel.saveState(outState) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/BloggingRemindersViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/BloggingRemindersViewModel.kt index 0d84aa28fba8..3378085a404e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/BloggingRemindersViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/BloggingRemindersViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.distinctUntilChanged import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import org.wordpress.android.R import org.wordpress.android.fluxc.store.BloggingRemindersStore import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.modules.UI_THREAD @@ -18,9 +19,11 @@ import org.wordpress.android.ui.bloggingreminders.BloggingRemindersAnalyticsTrac import org.wordpress.android.ui.bloggingreminders.BloggingRemindersAnalyticsTracker.Source.PUBLISH_FLOW import org.wordpress.android.ui.utils.ListItemInteraction import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.merge import org.wordpress.android.util.perform import org.wordpress.android.viewmodel.Event +import org.wordpress.android.viewmodel.ResourceProvider import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.workers.reminder.ReminderScheduler import java.time.DayOfWeek @@ -34,11 +37,13 @@ class BloggingRemindersViewModel @Inject constructor( private val prologueBuilder: PrologueBuilder, private val daySelectionBuilder: DaySelectionBuilder, private val epilogueBuilder: EpilogueBuilder, + private val notificationsPermissionBuilder: NotificationsPermissionBuilder, private val dayLabelUtils: DayLabelUtils, private val analyticsTracker: BloggingRemindersAnalyticsTracker, private val reminderScheduler: ReminderScheduler, private val mapper: BloggingRemindersModelMapper, - private val siteStore: SiteStore + private val siteStore: SiteStore, + private val resourceProvider: ResourceProvider ) : ScopedViewModel(mainDispatcher) { private val _isBottomSheetShowing = MutableLiveData>() val isBottomSheetShowing = _isBottomSheetShowing as LiveData> @@ -49,11 +54,19 @@ class BloggingRemindersViewModel @Inject constructor( private val _showBloggingPromptHelpDialogVisible = MutableLiveData>() val showBloggingPromptHelpDialogVisible = _showBloggingPromptHelpDialogVisible as LiveData> + private val _requestPermission = MutableLiveData>() + val requestPermission = _requestPermission as LiveData> + + private val _showDevicePermissionSettings = MutableLiveData>() + val showDevicePermissionSettings = _showDevicePermissionSettings as LiveData> + private val _selectedScreen = MutableLiveData() private val selectedScreen = _selectedScreen.perform { onScreenChanged(it) } private val _bloggingRemindersModel = MutableLiveData() private val _isFirstTimeFlow = MutableLiveData() + private var hasNotificationsPermissionState = false + private var notificationsPermissionAlwaysDeniedState = false val uiState: LiveData = merge( selectedScreen, @@ -71,6 +84,12 @@ class BloggingRemindersViewModel @Inject constructor( this::togglePromptSwitch, this::showBloggingPromptDialog ) + Screen.NOTIFICATIONS_PERMISSION -> { + notificationsPermissionBuilder.buildUiItems( + appName = resourceProvider.getString(R.string.app_name), + showAppSettingsGuide = notificationsPermissionAlwaysDeniedState + ) + } Screen.EPILOGUE -> epilogueBuilder.buildUiItems(bloggingRemindersModel) } val primaryButton = when (screen) { @@ -83,6 +102,9 @@ class BloggingRemindersViewModel @Inject constructor( isFirstTimeFlow == true, this::onSelectionButtonClick ) + Screen.NOTIFICATIONS_PERMISSION -> { + notificationsPermissionBuilder.buildPrimaryButton(onPermissionButtonTapped) + } Screen.EPILOGUE -> epilogueBuilder.buildPrimaryButton(finish) } UiState(uiItems, primaryButton) @@ -102,6 +124,14 @@ class BloggingRemindersViewModel @Inject constructor( _isBottomSheetShowing.value = Event(false) } + private val onPermissionButtonTapped: () -> Unit = { + if (notificationsPermissionAlwaysDeniedState) { + _showDevicePermissionSettings.value = Event(true) + } else { + _requestPermission.value = Event(true) + } + } + private fun onScreenChanged(screen: Screen) { analyticsTracker.trackScreenShown(screen) } @@ -134,7 +164,7 @@ class BloggingRemindersViewModel @Inject constructor( } } - fun selectDay(day: DayOfWeek) { + private fun selectDay(day: DayOfWeek) { val currentState = _bloggingRemindersModel.value!! val enabledDays = currentState.enabledDays.toMutableSet() if (enabledDays.contains(day)) { @@ -145,7 +175,12 @@ class BloggingRemindersViewModel @Inject constructor( _bloggingRemindersModel.value = currentState.copy(enabledDays = enabledDays) } - fun selectTime() { + fun setPermissionState(hasNotificationsPermission: Boolean, notificationsPermissionAlwaysDenied: Boolean) { + hasNotificationsPermissionState = hasNotificationsPermission + notificationsPermissionAlwaysDeniedState = notificationsPermissionAlwaysDenied + } + + private fun selectTime() { _isTimePickerShowing.value = Event(true) } @@ -177,12 +212,16 @@ class BloggingRemindersViewModel @Inject constructor( analyticsTracker.trackPrimaryButtonPressed(Screen.SELECTION) if (bloggingRemindersModel != null) { launch { + val daysCount = bloggingRemindersModel.enabledDays.size + if (!checkPermission()) { + // There is no permission + return@launch + } bloggingRemindersStore.updateBloggingReminders( mapper.toDomainModel( bloggingRemindersModel ) ) - val daysCount = bloggingRemindersModel.enabledDays.size if (daysCount > 0) { reminderScheduler.schedule( bloggingRemindersModel.siteId, @@ -202,6 +241,20 @@ class BloggingRemindersViewModel @Inject constructor( } } + private fun checkPermission(): Boolean { + return when { + !hasNotificationsPermissionState && notificationsPermissionAlwaysDeniedState -> { + _selectedScreen.value = Screen.NOTIFICATIONS_PERMISSION + false + } + !hasNotificationsPermissionState -> { + _requestPermission.value = Event(true) + false + } + else -> true // Already has permission + } + } + fun saveState(outState: Bundle) { _selectedScreen.value?.let { outState.putSerializable(SELECTED_SCREEN, it) @@ -220,9 +273,7 @@ class BloggingRemindersViewModel @Inject constructor( } fun restoreState(state: Bundle) { - state.getSerializable(SELECTED_SCREEN)?.let { - _selectedScreen.value = it as Screen - } + state.getSerializableCompat(SELECTED_SCREEN)?.let { _selectedScreen.value = it } val siteId = state.getInt(SITE_ID) if (siteId != 0) { val enabledDays = state.getStringArrayList(SELECTED_DAYS)?.map { DayOfWeek.valueOf(it) }?.toSet() ?: setOf() @@ -275,16 +326,37 @@ class BloggingRemindersViewModel @Inject constructor( when (val screen = selectedScreen.value) { Screen.PROLOGUE, Screen.PROLOGUE_SETTINGS, + Screen.NOTIFICATIONS_PERMISSION, Screen.SELECTION -> analyticsTracker.trackFlowDismissed(screen) Screen.EPILOGUE -> analyticsTracker.trackFlowCompleted() null -> Unit // Do nothing } } + fun onPermissionGranted() { + if (!hasNotificationsPermissionState) { + // Permission state is changed. + hasNotificationsPermissionState = true + notificationsPermissionAlwaysDeniedState = false + + if (_selectedScreen.value == Screen.NOTIFICATIONS_PERMISSION) { + onSelectionButtonClick(_bloggingRemindersModel.value) + } + } + } + + fun onPermissionDenied(isAlwaysDenied: Boolean) { + hasNotificationsPermissionState = false + notificationsPermissionAlwaysDeniedState = isAlwaysDenied + + _selectedScreen.value = Screen.NOTIFICATIONS_PERMISSION + } + enum class Screen(val trackingName: String) { PROLOGUE("main"), // displayed after post is published PROLOGUE_SETTINGS("main"), // displayed from Site Settings before showing cadence selector SELECTION("day_picker"), // cadence selector + NOTIFICATIONS_PERMISSION("notifications_permission"), EPILOGUE("all_set") } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/NotificationsPermissionBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/NotificationsPermissionBuilder.kt new file mode 100644 index 000000000000..e008d9447cd7 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/bloggingreminders/NotificationsPermissionBuilder.kt @@ -0,0 +1,43 @@ +package org.wordpress.android.ui.bloggingreminders + +import org.wordpress.android.R.drawable +import org.wordpress.android.R.string +import org.wordpress.android.ui.bloggingreminders.BloggingRemindersItem.Caption +import org.wordpress.android.ui.bloggingreminders.BloggingRemindersItem.EmphasizedText +import org.wordpress.android.ui.bloggingreminders.BloggingRemindersItem.HighEmphasisText +import org.wordpress.android.ui.bloggingreminders.BloggingRemindersItem.Illustration +import org.wordpress.android.ui.bloggingreminders.BloggingRemindersItem.Title +import org.wordpress.android.ui.bloggingreminders.BloggingRemindersViewModel.UiState.PrimaryButton +import org.wordpress.android.ui.utils.ListItemInteraction +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.ui.utils.UiString.UiStringRes +import javax.inject.Inject + +class NotificationsPermissionBuilder @Inject constructor() { + fun buildUiItems(appName: String, showAppSettingsGuide: Boolean): List { + val title = UiStringRes(string.blogging_reminders_notifications_permission_title) + + val body = UiString.UiStringResWithParams( + string.blogging_reminders_notifications_permission_description, + UiString.UiStringText(appName) + ) + + val uiItems = mutableListOf( + Illustration(drawable.img_illustration_bell_yellow_96dp), + Title(title), + Caption(UiStringRes(string.blogging_reminders_notifications_permission_caption)) + ) + if (showAppSettingsGuide) { + uiItems.add(HighEmphasisText(EmphasizedText(body, false))) + } + return uiItems + } + + fun buildPrimaryButton(onDone: () -> Unit): PrimaryButton { + return PrimaryButton( + UiStringRes(string.blogging_reminders_notifications_permission_primary_button), + enabled = true, + ListItemInteraction.create(onDone) + ) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsDetailActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsDetailActivity.java index cd37f943beb6..f831d976dc20 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsDetailActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsDetailActivity.java @@ -8,6 +8,7 @@ import android.view.View; import android.widget.ProgressBar; +import androidx.activity.OnBackPressedCallback; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import androidx.viewpager.widget.ViewPager; @@ -31,13 +32,14 @@ import org.wordpress.android.ui.LocaleAwareActivity; import org.wordpress.android.ui.ScrollableViewInitializedListener; import org.wordpress.android.ui.comments.unified.CommentConstants; -import org.wordpress.android.ui.comments.unified.OnLoadMoreListener; import org.wordpress.android.ui.comments.unified.CommentsStoreAdapter; +import org.wordpress.android.ui.comments.unified.OnLoadMoreListener; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; import org.wordpress.android.util.analytics.AnalyticsUtils.AnalyticsCommentActionSource; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import org.wordpress.android.widgets.WPViewPager; import org.wordpress.android.widgets.WPViewPagerTransformer; @@ -45,12 +47,15 @@ import static org.wordpress.android.ui.comments.unified.CommentConstants.COMMENTS_PER_PAGE; +import dagger.hilt.android.AndroidEntryPoint; + /** * @deprecated * Comments are being refactored as part of Comments Unification project. If you are adding any * features or modifying this class, please ping develric or klymyam */ @Deprecated +@AndroidEntryPoint public class CommentsDetailActivity extends LocaleAwareActivity implements OnLoadMoreListener, CommentActions.OnCommentActionListener, ScrollableViewInitializedListener { @@ -73,27 +78,29 @@ public class CommentsDetailActivity extends LocaleAwareActivity private boolean mIsUpdatingComments; private boolean mCanLoadMoreComments = true; - @Override - public void onBackPressed() { - CollapseFullScreenDialogFragment fragment = (CollapseFullScreenDialogFragment) - getSupportFragmentManager().findFragmentByTag(CollapseFullScreenDialogFragment.TAG); - - if (fragment != null) { - fragment.onBackPressed(); - } else { - super.onBackPressed(); - } - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ((WordPress) getApplication()).component().inject(this); mCommentsStoreAdapter.register(this); AppLog.i(AppLog.T.COMMENTS, "Creating CommentsDetailActivity"); setContentView(R.layout.comments_detail_activity); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + CollapseFullScreenDialogFragment fragment = (CollapseFullScreenDialogFragment) + getSupportFragmentManager().findFragmentByTag(CollapseFullScreenDialogFragment.TAG); + + if (fragment != null) { + fragment.collapse(); + } else { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + Toolbar toolbar = findViewById(R.id.toolbar_main); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java index 83a59cc8fbf2..af88e79c01ae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java @@ -13,6 +13,7 @@ import android.widget.EditText; import android.widget.ProgressBar; +import androidx.activity.OnBackPressedCallback; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; @@ -41,6 +42,7 @@ import org.wordpress.android.util.EditTextUtils; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import javax.inject.Inject; @@ -73,6 +75,19 @@ public void onCreate(Bundle icicle) { ((WordPress) getApplication()).component().inject(this); setContentView(R.layout.comment_edit_activity); + + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (isCommentEdited()) { + cancelEditCommentConfirmation(); + } else { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + Toolbar toolbar = findViewById(R.id.toolbar_main); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); @@ -194,7 +209,7 @@ public boolean onCreateOptionsMenu(Menu menu) { public boolean onOptionsItemSelected(final MenuItem item) { int i = item.getItemId(); if (i == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } else if (i == R.id.menu_save_comment) { saveComment(); @@ -293,15 +308,6 @@ private void setFetchProgressVisible(boolean progressVisible) { editContainer.setVisibility(progressVisible ? View.GONE : View.VISIBLE); } - @Override - public void onBackPressed() { - if (isCommentEdited()) { - cancelEditCommentConfirmation(); - } else { - super.onBackPressed(); - } - } - private void cancelEditCommentConfirmation() { if (mCancelEditCommentDialog != null) { mCancelEditCommentDialog.show(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentListActionModeCallback.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentListActionModeCallback.kt index 0db7bbf278e0..05708b9972ae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentListActionModeCallback.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentListActionModeCallback.kt @@ -73,9 +73,10 @@ class CommentListActionModeCallback( private fun setItemEnabled(menuItem: MenuItem, actionUiModel: ActionUiModel) { menuItem.isVisible = actionUiModel.isVisible menuItem.isEnabled = actionUiModel.isEnabled - if (menuItem.icon != null) { + val currentIcon = menuItem.icon + currentIcon?.let { // must mutate the drawable to avoid affecting other instances of it - val icon = menuItem.icon.mutate() + val icon = it.mutate() icon.alpha = if (actionUiModel.isEnabled) ICON_ALPHA_ENABLED else ICON_ALPHA_DISABLED menuItem.icon = icon } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentListFragment.kt index 67259b28128d..3c91ccf6ea39 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentListFragment.kt @@ -33,6 +33,7 @@ import org.wordpress.android.util.SnackbarItem.Info import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.WPSwipeToRefreshHelper import org.wordpress.android.util.config.UnifiedCommentsDetailFeatureConfig +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.helpers.SwipeToRefreshHelper import javax.inject.Inject @@ -70,13 +71,13 @@ class UnifiedCommentListFragment : Fragment(R.layout.unified_comment_list_fragme override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (requireActivity().application as WordPress).component().inject(this) - viewModel = ViewModelProvider(this, viewModelFactory).get(UnifiedCommentListViewModel::class.java) + viewModel = ViewModelProvider(this, viewModelFactory)[UnifiedCommentListViewModel::class.java] activityViewModel = ViewModelProvider( requireActivity(), viewModelFactory - ).get(UnifiedCommentActivityViewModel::class.java) + )[UnifiedCommentActivityViewModel::class.java] arguments?.let { - commentListFilter = it.getSerializable(KEY_COMMENT_LIST_FILTER) as CommentFilter + commentListFilter = requireNotNull(it.getSerializableCompat(KEY_COMMENT_LIST_FILTER)) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsActivity.kt index e87cc567be71..4381983c9e73 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsActivity.kt @@ -116,7 +116,7 @@ class UnifiedCommentsActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditActivity.kt index 807718b2c3a3..d8a4e70dab6f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditActivity.kt @@ -8,6 +8,8 @@ import org.wordpress.android.WordPress import org.wordpress.android.databinding.UnifiedCommentsEditActivityBinding import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.LocaleAwareActivity +import org.wordpress.android.util.extensions.getParcelableExtraCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat class UnifiedCommentsEditActivity : LocaleAwareActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -17,8 +19,10 @@ class UnifiedCommentsEditActivity : LocaleAwareActivity() { setContentView(root) } - val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel - val commentIdentifier = requireNotNull(intent.getParcelableExtra(KEY_COMMENT_IDENTIFIER)) + val site = requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE)) + val commentIdentifier = requireNotNull( + intent.getParcelableExtraCompat(KEY_COMMENT_IDENTIFIER) + ) val fm = supportFragmentManager var editCommentFragment = fm.findFragmentByTag( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditFragment.kt index 704f3c1d03b1..90d5addf8199 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditFragment.kt @@ -6,7 +6,7 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.view.MenuProvider import androidx.core.widget.doAfterTextChanged @@ -34,6 +34,8 @@ import org.wordpress.android.util.SnackbarItem import org.wordpress.android.util.SnackbarItem.Action import org.wordpress.android.util.SnackbarItem.Info import org.wordpress.android.util.SnackbarSequencer +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -61,11 +63,9 @@ class UnifiedCommentsEditFragment : Fragment(R.layout.unified_comments_edit_frag super.onViewCreated(view, savedInstanceState) requireActivity().addMenuProvider(this, viewLifecycleOwner) - val site = requireArguments().getSerializable(WordPress.SITE) as SiteModel + val site = requireNotNull(arguments?.getSerializableCompat(WordPress.SITE)) val commentIdentifier = requireNotNull( - requireArguments().getParcelable( - KEY_COMMENT_IDENTIFIER - ) + requireArguments().getParcelableCompat(KEY_COMMENT_IDENTIFIER) ) UnifiedCommentsEditFragmentBinding.bind(view).apply { @@ -82,15 +82,7 @@ class UnifiedCommentsEditFragment : Fragment(R.layout.unified_comments_edit_frag it.setDisplayHomeAsUpEnabled(true) it.setHomeAsUpIndicator(R.drawable.ic_cross_white_24dp) } - activity.onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback( - true - ) { - override fun handleOnBackPressed() { - viewModel.onBackPressed() - } - }) + activity.onBackPressedDispatcher.addCallback(this@UnifiedCommentsEditFragment) { viewModel.onBackPressed() } } private fun hideKeyboard() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/ImageButton.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/ImageButton.kt index 94f795bfb2d3..b48e648319ac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/ImageButton.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/ImageButton.kt @@ -48,7 +48,6 @@ fun PreviewDrawButton() { ) } - @Composable fun ImageButton( modifier: Modifier = Modifier, @@ -59,7 +58,8 @@ fun ImageButton( button: Button, onClick: () -> Unit ) { - ConstraintLayout(modifier = modifier) { + ConstraintLayout(modifier = modifier + .clickable { onClick.invoke() }) { val (buttonTextRef) = createRefs() Box(modifier = Modifier .constrainAs(buttonTextRef) { @@ -69,7 +69,6 @@ fun ImageButton( end.linkTo(parent.end, drawableRight?.iconSize ?: 0.dp) width = Dimension.wrapContent } - .clickable { onClick.invoke() } ) { val buttonTextValue: String = uiStringText(button.text) Text( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/PrimaryButton.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/PrimaryButton.kt index f2666f2473ce..250d0d9ba7d7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/PrimaryButton.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/PrimaryButton.kt @@ -1,19 +1,24 @@ package org.wordpress.android.ui.compose.components.buttons import android.content.res.Configuration +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.Button +import androidx.compose.material.ButtonColors import androidx.compose.material.ButtonDefaults import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.MaterialTheme +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.material.LocalTextStyle import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wordpress.android.R @@ -26,19 +31,20 @@ fun PrimaryButton( onClick: () -> Unit, modifier: Modifier = Modifier, isInProgress: Boolean = false, - useDefaultMargins: Boolean = true, + colors: ButtonColors = ButtonDefaults.buttonColors( + contentColor = AppColor.White, + disabledContentColor = AppColor.White.copy(alpha = ContentAlpha.disabled), + disabledBackgroundColor = colorResource(R.color.jetpack_green_70), + ), + padding: PaddingValues = PaddingValues( + start = dimensionResource(R.dimen.jp_migration_buttons_padding_horizontal), + top = 20.dp, + end = dimensionResource(R.dimen.jp_migration_buttons_padding_horizontal), + bottom = 10.dp + ), + textStyle: TextStyle = LocalTextStyle.current, buttonSize: ButtonSize = ButtonSize.NORMAL, ) { - var computedModifier: Modifier = modifier - - if (useDefaultMargins) { - computedModifier = computedModifier - .padding(top = 20.dp, bottom = 10.dp) - .padding(horizontal = dimensionResource(R.dimen.jp_migration_buttons_padding_horizontal)) - } - - computedModifier = computedModifier.defaultMinSize(minHeight = buttonSize.height) - Button( onClick = onClick, enabled = !isInProgress, @@ -46,21 +52,20 @@ fun PrimaryButton( defaultElevation = 0.dp, pressedElevation = 0.dp, ), - colors = ButtonDefaults.buttonColors( - contentColor = AppColor.White, - disabledBackgroundColor = colorResource(R.color.jetpack_green_70), - ), - modifier = computedModifier + colors = colors, + modifier = modifier + .padding(padding) + .defaultMinSize(minHeight = buttonSize.height) .fillMaxWidth(), ) { if (isInProgress) { CircularProgressIndicator( - color = MaterialTheme.colors.onPrimary, + color = LocalContentColor.current, strokeWidth = 2.dp, modifier = Modifier.size(20.dp), ) } else { - Text(text = text) + Text(text, style = textStyle) } } } @@ -83,15 +88,6 @@ private fun PrimaryButtonInProgressPreview() { } } -@Preview -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PrimaryButtonNoDefaultMarginsPreview() { - AppTheme { - PrimaryButton(text = "Continue", onClick = {}, useDefaultMargins = false) - } -} - @Preview @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/SecondaryButton.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/SecondaryButton.kt index 7f2f2a3164c7..2b5234c16e21 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/SecondaryButton.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/components/buttons/SecondaryButton.kt @@ -1,17 +1,21 @@ package org.wordpress.android.ui.compose.components.buttons import android.content.res.Configuration +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Button +import androidx.compose.material.ButtonColors import androidx.compose.material.ButtonDefaults +import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.wordpress.android.R @@ -23,35 +27,36 @@ fun SecondaryButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - useDefaultMargins: Boolean = true, + colors: ButtonColors = ButtonDefaults.buttonColors( + backgroundColor = Color.Transparent, + contentColor = MaterialTheme.colors.primary, + disabledBackgroundColor = Color.Transparent, + disabledContentColor = MaterialTheme.colors.primary, + ), + padding: PaddingValues = PaddingValues( + start = dimensionResource(R.dimen.jp_migration_buttons_padding_horizontal), + end = dimensionResource(R.dimen.jp_migration_buttons_padding_horizontal), + bottom = 10.dp, + ), + textStyle: TextStyle = LocalTextStyle.current, buttonSize: ButtonSize = ButtonSize.NORMAL, + trailingContent: @Composable (() -> Unit)? = null, ) { - var computedModifier: Modifier = modifier - - if (useDefaultMargins) { - computedModifier = computedModifier - .padding(bottom = 10.dp) - .padding(horizontal = dimensionResource(R.dimen.jp_migration_buttons_padding_horizontal)) - } - - computedModifier = computedModifier.defaultMinSize(minHeight = buttonSize.height) - Button( - onClick = onClick, + onClick, enabled = enabled, + colors = colors, elevation = ButtonDefaults.elevation( defaultElevation = 0.dp, pressedElevation = 0.dp, ), - colors = ButtonDefaults.buttonColors( - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.colors.primary, - disabledBackgroundColor = Color.Transparent, - disabledContentColor = MaterialTheme.colors.primary, - ), - modifier = computedModifier.fillMaxWidth() + modifier = modifier + .padding(padding) + .defaultMinSize(minHeight = buttonSize.height) + .fillMaxWidth() ) { - Text(text = text) + Text(text, style = textStyle) + trailingContent?.invoke() } } @@ -64,15 +69,6 @@ private fun SecondaryButtonPreview() { } } -@Preview -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SecondaryButtonNoDefaultMarginsPreview() { - AppTheme { - SecondaryButton(text = "Continue", onClick = {}, useDefaultMargins = false) - } -} - @Preview @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/theme/AppColor.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/theme/AppColor.kt index e5d71cc8a6ed..b4449e6b46d7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/theme/AppColor.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/theme/AppColor.kt @@ -46,7 +46,7 @@ object AppColor { // Jetpack Greens (Automattic Color Studio) @Stable - val JetpackGreen40 = Color(0xFF069E08) + val JetpackGreen30 = Color(0xFF2FB41F) @Stable val JetpackGreen50 = Color(0xFF008710) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/theme/JetpackColors.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/theme/JetpackColors.kt index e998fde58cdf..6a3860a22e24 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/theme/JetpackColors.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/theme/JetpackColors.kt @@ -9,9 +9,9 @@ import androidx.compose.runtime.Composable @SuppressLint("ConflictingOnColor") val JpLightColorPalette = lightColors( primary = AppColor.JetpackGreen50, - primaryVariant = AppColor.JetpackGreen40, + primaryVariant = AppColor.JetpackGreen30, secondary = AppColor.JetpackGreen50, - secondaryVariant = AppColor.JetpackGreen40, + secondaryVariant = AppColor.JetpackGreen30, background = AppColor.White, surface = AppColor.White, error = AppColor.Red50, @@ -24,9 +24,9 @@ val JpLightColorPalette = lightColors( @SuppressLint("ConflictingOnColor") val JpDarkColorPalette = darkColors( - primary = AppColor.JetpackGreen40, + primary = AppColor.JetpackGreen30, primaryVariant = AppColor.JetpackGreen50, - secondary = AppColor.JetpackGreen40, + secondary = AppColor.JetpackGreen30, secondaryVariant = AppColor.JetpackGreen50, background = AppColor.DarkGray, surface = AppColor.DarkGray, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/compose/utils/ComposeUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/compose/utils/ComposeUtils.kt index bfa0b715a962..9c3ca0393396 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/compose/utils/ComposeUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/compose/utils/ComposeUtils.kt @@ -1,5 +1,6 @@ package org.wordpress.android.ui.compose.utils +import android.annotation.SuppressLint import android.content.res.Configuration import android.os.Build import androidx.compose.material.LocalContentAlpha @@ -34,8 +35,9 @@ fun withFullContentAlpha(content: @Composable () -> Unit): @Composable () -> Uni * @param content The Composable function to be rendered with the overridden locale. */ -@Suppress("DEPRECATION") @Composable +@Suppress("DEPRECATION") +@SuppressLint("AppBundleLocaleChanges") fun LocaleAwareComposable( locale: Locale = Locale.getDefault(), onLocaleChange: (Locale) -> Unit = {}, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsActivity.kt index f344f01d6256..54a779873e1c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsActivity.kt @@ -2,9 +2,11 @@ package org.wordpress.android.ui.debug import android.os.Bundle import android.view.MenuItem +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.databinding.DebugSettingsActivityBinding import org.wordpress.android.ui.LocaleAwareActivity +@AndroidEntryPoint class DebugSettingsActivity : LocaleAwareActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -13,7 +15,7 @@ class DebugSettingsActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsFragment.kt index b15b578caa9e..a95cb07bb368 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsFragment.kt @@ -3,20 +3,24 @@ package org.wordpress.android.ui.debug import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import dagger.android.support.DaggerFragment +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.databinding.DebugSettingsFragmentBinding import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.debug.DebugSettingsViewModel.NavigationAction.DebugCookies +import org.wordpress.android.ui.debug.DebugSettingsViewModel.NavigationAction.PreviewFragment +import org.wordpress.android.ui.debug.previews.PreviewFragmentActivity.Companion.previewFragmentInActivity import org.wordpress.android.util.DisplayUtils import org.wordpress.android.viewmodel.observeEvent import org.wordpress.android.widgets.RecyclerItemDecoration import javax.inject.Inject -class DebugSettingsFragment : DaggerFragment(R.layout.debug_settings_fragment) { +@AndroidEntryPoint +class DebugSettingsFragment : Fragment(R.layout.debug_settings_fragment) { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory private lateinit var viewModel: DebugSettingsViewModel @@ -55,6 +59,7 @@ class DebugSettingsFragment : DaggerFragment(R.layout.debug_settings_fragment) { viewModel.onNavigation.observeEvent(viewLifecycleOwner) { when (it) { DebugCookies -> ActivityLauncher.viewDebugCookies(requireContext()) + is PreviewFragment -> previewFragmentInActivity(it.name) } } viewModel.start() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsItemViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsItemViewHolder.kt index 3574f9a5cec9..b9afe75d002d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsItemViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsItemViewHolder.kt @@ -8,7 +8,9 @@ import android.widget.CheckBox import android.widget.ImageView import android.widget.TextView import androidx.annotation.LayoutRes +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.google.android.material.button.MaterialButton import org.wordpress.android.R import org.wordpress.android.databinding.DebugSettingsRemoteFieldBinding import org.wordpress.android.databinding.DebugSettingsRowBinding @@ -40,6 +42,7 @@ sealed class DebugSettingsItemViewHolder( private val title = itemView.findViewById(R.id.feature_title) private val enabled = itemView.findViewById(R.id.feature_enabled) private val unknown = itemView.findViewById(R.id.unknown_icon) + private val preview = itemView.findViewById(R.id.preview_icon) fun bind(item: UiItem.Feature) { title.text = item.title enabled.visibility = View.GONE @@ -60,6 +63,8 @@ sealed class DebugSettingsItemViewHolder( } enabled.setOnCheckedChangeListener { _, _ -> item.toggleAction.toggle() } itemView.setOnClickListener { item.toggleAction.toggle() } + preview.isVisible = item.preview != null + preview.setOnClickListener { item.preview?.invoke() } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsViewModel.kt index a2b9d147ef68..3fd4eb7a3bd1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/debug/DebugSettingsViewModel.kt @@ -20,6 +20,7 @@ import org.wordpress.android.ui.debug.DebugSettingsViewModel.UiItem.Type.BUTTON import org.wordpress.android.ui.debug.DebugSettingsViewModel.UiItem.Type.FEATURE import org.wordpress.android.ui.debug.DebugSettingsViewModel.UiItem.Type.HEADER import org.wordpress.android.ui.debug.DebugSettingsViewModel.UiItem.Type.ROW +import org.wordpress.android.ui.debug.previews.PREVIEWS import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper import org.wordpress.android.ui.notifications.NotificationManagerWrapper import org.wordpress.android.ui.utils.ListItemInteraction @@ -49,7 +50,7 @@ class DebugSettingsViewModel private val weeklyRoundupNotifier: WeeklyRoundupNotifier, private val notificationManager: NotificationManagerWrapper, private val contextProvider: ContextProvider, - private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper + private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper, ) : ScopedViewModel(mainDispatcher) { private val _uiState = MutableLiveData() val uiState: LiveData = _uiState @@ -65,7 +66,11 @@ class DebugSettingsViewModel private fun refresh() { val uiItems = mutableListOf() - val remoteFeatures = buildRemoteFeatures() + val remoteFeatures = buildRemoteFeatures().map { + it.apply { + preview = { onFeaturePreviewClick(title) }.takeIf { state == ENABLED && PREVIEWS.contains(title) } + } + } if (remoteFeatures.isNotEmpty()) { uiItems.add(Header(R.string.debug_settings_remote_features)) uiItems.addAll(remoteFeatures) @@ -94,8 +99,12 @@ class DebugSettingsViewModel _onNavigation.value = Event(DebugCookies) } + private fun onFeaturePreviewClick(key: String) { + _onNavigation.value = Event(NavigationAction.PreviewFragment(key)) + } + private fun onForceShowWeeklyRoundupClick() = launch(bgDispatcher) { - if(!jetpackFeatureRemovalPhaseHelper.shouldShowNotifications()) + if (!jetpackFeatureRemovalPhaseHelper.shouldShowNotifications()) return@launch weeklyRoundupNotifier.buildNotifications().forEach { notificationManager.notify(it.id, it.asNotificationCompatBuilder(contextProvider.getContext()).build()) @@ -165,6 +174,9 @@ class DebugSettingsViewModel ) enum class State { ENABLED, DISABLED, UNKNOWN } + + @Suppress("DataClassShouldBeImmutable") // We're not in prod code or diffing here, the rule is moot + var preview: (() -> Unit)? = null } data class Field(val remoteFieldKey: String, val remoteFieldValue: String, val remoteFieldSource: String) : @@ -187,5 +199,6 @@ class DebugSettingsViewModel sealed class NavigationAction { object DebugCookies : NavigationAction() + data class PreviewFragment(val name: String) : NavigationAction() } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/debug/cookies/DebugCookiesActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/debug/cookies/DebugCookiesActivity.kt index 569b62f095ea..1772ba413e06 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/debug/cookies/DebugCookiesActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/debug/cookies/DebugCookiesActivity.kt @@ -13,7 +13,7 @@ class DebugCookiesActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { android.R.id.home -> { - onBackPressed() + onBackPressedDispatcher.onBackPressed() true } else -> super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/debug/previews/PreviewFragmentActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/debug/previews/PreviewFragmentActivity.kt new file mode 100644 index 000000000000..d1d042d414e6 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/debug/previews/PreviewFragmentActivity.kt @@ -0,0 +1,40 @@ +package org.wordpress.android.ui.debug.previews + +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.commit +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.ui.debug.DebugSettingsFragment +import org.wordpress.android.ui.main.jetpack.staticposter.JetpackStaticPosterFragment +import org.wordpress.android.ui.main.jetpack.staticposter.UiData +import org.wordpress.android.util.config.JetpackFeatureRemovalStaticPostersConfig.Companion.JETPACK_FEATURE_REMOVAL_STATIC_POSTERS_REMOTE_FIELD + +@AndroidEntryPoint +class PreviewFragmentActivity : FragmentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + supportFragmentManager.commit { + val key = requireNotNull(intent.getStringExtra(KEY)) + val factory = requireNotNull(PREVIEWS[key]) + add(android.R.id.content, factory.invoke()) + } + } + + companion object { + const val KEY = "KEY" + + fun DebugSettingsFragment.previewFragmentInActivity(key: String) { + startActivity( + Intent(requireContext(), this@Companion::class.java.enclosingClass).apply { + putExtra(KEY, key) + } + ) + } + } +} + +val PREVIEWS = mapOf( + JETPACK_FEATURE_REMOVAL_STATIC_POSTERS_REMOTE_FIELD to { JetpackStaticPosterFragment.newInstance(UiData.STATS) }, +) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkNavigator.kt b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkNavigator.kt index bbbada1a2a19..b49a54c321b7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkNavigator.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkNavigator.kt @@ -81,6 +81,8 @@ class DeepLinkNavigator OpenLoginPrologue -> ActivityLauncher.showLoginPrologue(activity) is OpenJetpackForDeepLink -> ActivityLauncher.openJetpackForDeeplink(activity, navigateAction.action, navigateAction.uri) + is NavigateAction.OpenJetpackStaticPosterView -> + ActivityLauncher.showJetpackStaticPoster(activity) } if (navigateAction != LoginForResult) { activity.finish() @@ -111,5 +113,6 @@ class DeepLinkNavigator object OpenMySite : NavigateAction() object OpenLoginPrologue : NavigateAction() data class OpenJetpackForDeepLink(val action: String?, val uri: UriWrapper) : NavigateAction() + object OpenJetpackStaticPosterView : NavigateAction() } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkingIntentReceiverActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkingIntentReceiverActivity.java index 5da594ae58eb..28066e7cf893 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkingIntentReceiverActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkingIntentReceiverActivity.java @@ -4,6 +4,7 @@ import android.net.Uri; import android.os.Bundle; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.lifecycle.ViewModelProvider; @@ -20,6 +21,7 @@ import org.wordpress.android.util.PackageManagerWrapper; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.UriWrapper; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import javax.inject.Inject; @@ -49,6 +51,16 @@ public class DeepLinkingIntentReceiverActivity extends LocaleAwareActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + finish(); + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + mViewModel = new ViewModelProvider(this).get(DeepLinkingIntentReceiverViewModel.class); mJetpackFullScreenViewModel = new ViewModelProvider(this).get(JetpackFeatureFullScreenOverlayViewModel.class); setupObservers(); @@ -139,10 +151,4 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { finish(); } } - - @Override - public void onBackPressed() { - super.onBackPressed(); - finish(); - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkingIntentReceiverViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkingIntentReceiverViewModel.kt index 1d9c7c6e06c1..978c941d4251 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkingIntentReceiverViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/DeepLinkingIntentReceiverViewModel.kt @@ -20,6 +20,7 @@ import org.wordpress.android.ui.deeplinks.handlers.DeepLinkHandlers import org.wordpress.android.ui.deeplinks.handlers.ServerTrackingHandler import org.wordpress.android.util.UriWrapper import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject @@ -147,12 +148,11 @@ class DeepLinkingIntentReceiverViewModel private fun extractSavedInstanceStateIfNeeded(savedInstanceState: Bundle?) { savedInstanceState?.let { - val uri: Uri? = savedInstanceState.getParcelable(URI_KEY) + val uri = savedInstanceState.getParcelableCompat(URI_KEY) uriWrapper = uri?.let { UriWrapper(it) } - deepLinkEntryPoint = - DeepLinkEntryPoint.valueOf( - savedInstanceState.getString(DEEP_LINK_ENTRY_POINT_KEY, DeepLinkEntryPoint.DEFAULT.name) - ) + deepLinkEntryPoint = DeepLinkEntryPoint.valueOf( + savedInstanceState.getString(DEEP_LINK_ENTRY_POINT_KEY, DeepLinkEntryPoint.DEFAULT.name) + ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandler.kt index 1b0a33990377..ceed1dbb5b12 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandler.kt @@ -6,17 +6,20 @@ import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenS import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenStatsForSite import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenStatsForSiteAndTimeframe import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenStatsForTimeframe +import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction.OpenJetpackStaticPosterView import org.wordpress.android.ui.deeplinks.DeepLinkUriUtils import org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverViewModel.Companion.APPLINK_SCHEME import org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverViewModel.Companion.HOST_WORDPRESS_COM import org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverViewModel.Companion.SITE_DOMAIN +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper import org.wordpress.android.ui.stats.StatsTimeframe import org.wordpress.android.util.UriWrapper import javax.inject.Inject class StatsLinkHandler @Inject constructor( - private val deepLinkUriUtils: DeepLinkUriUtils + private val deepLinkUriUtils: DeepLinkUriUtils, + private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper ) : DeepLinkHandler { /** * Builds navigate action from URL like: @@ -31,6 +34,7 @@ class StatsLinkHandler val site = pathSegments.getOrNull(length - 1)?.toSite() val statsTimeframe = pathSegments.getOrNull(length - 2)?.toStatsTimeframe() return when { + jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage() -> OpenJetpackStaticPosterView site != null && statsTimeframe != null -> { OpenStatsForSiteAndTimeframe(site, statsTimeframe) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainProductDetails.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainProductDetails.kt index 054ad9c81517..b6eaad296252 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainProductDetails.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainProductDetails.kt @@ -1,11 +1,9 @@ package org.wordpress.android.ui.domains -import android.annotation.SuppressLint import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -@SuppressLint("ParcelCreator") data class DomainProductDetails( val productId: Int, val domainName: String diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationActivity.kt index 30e777de2edf..a32402184cac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationActivity.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.MenuItem import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.databinding.DomainRegistrationActivityBinding @@ -17,9 +18,11 @@ import org.wordpress.android.ui.domains.DomainRegistrationNavigationAction.OpenD import org.wordpress.android.ui.domains.DomainRegistrationNavigationAction.OpenDomainRegistrationDetails import org.wordpress.android.ui.domains.DomainRegistrationNavigationAction.OpenDomainRegistrationResult import org.wordpress.android.ui.domains.DomainRegistrationNavigationAction.OpenDomainSuggestions +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject +@AndroidEntryPoint class DomainRegistrationActivity : LocaleAwareActivity(), ScrollableViewInitializedListener { enum class DomainRegistrationPurpose { AUTOMATED_TRANSFER, @@ -45,14 +48,14 @@ class DomainRegistrationActivity : LocaleAwareActivity(), ScrollableViewInitiali override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - (application as WordPress).component().inject(this) with(DomainRegistrationActivityBinding.inflate(layoutInflater)) { setContentView(root) binding = this - val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel - val domainRegistrationPurpose = intent.getSerializableExtra(DOMAIN_REGISTRATION_PURPOSE_KEY) - as DomainRegistrationPurpose + val site = requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE)) + val domainRegistrationPurpose = requireNotNull( + intent.getSerializableExtraCompat(DOMAIN_REGISTRATION_PURPOSE_KEY) + ) setupToolbar() setupViewModel(site, domainRegistrationPurpose) @@ -134,7 +137,7 @@ class DomainRegistrationActivity : LocaleAwareActivity(), ScrollableViewInitiali override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationDetailsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationDetailsFragment.kt index 459c93964a19..530d424c31a4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationDetailsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationDetailsFragment.kt @@ -4,7 +4,6 @@ package org.wordpress.android.ui.domains import android.app.Dialog import android.app.ProgressDialog -import android.content.Context import android.os.Bundle import android.text.Editable import android.text.TextUtils @@ -21,7 +20,7 @@ import androidx.lifecycle.ViewModelProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout -import dagger.android.support.AndroidSupportInjection +import dagger.hilt.android.AndroidEntryPoint import org.apache.commons.text.StringEscapeUtils import org.wordpress.android.R import org.wordpress.android.WordPress @@ -47,6 +46,7 @@ import org.wordpress.android.ui.domains.DomainRegistrationDetailsViewModel.Domai import org.wordpress.android.util.StringUtils import org.wordpress.android.util.ToastUtils import org.wordpress.android.util.WPUrlUtils +import org.wordpress.android.util.extensions.getSerializableExtraCompat import javax.inject.Inject class DomainRegistrationDetailsFragment : Fragment() { @@ -89,10 +89,11 @@ class DomainRegistrationDetailsFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mainViewModel = ViewModelProvider(requireActivity(), viewModelFactory) - .get(DomainRegistrationMainViewModel::class.java) - viewModel = ViewModelProvider(this, viewModelFactory) - .get(DomainRegistrationDetailsViewModel::class.java) + mainViewModel = ViewModelProvider( + requireActivity(), + viewModelFactory + )[DomainRegistrationMainViewModel::class.java] + viewModel = ViewModelProvider(this, viewModelFactory)[DomainRegistrationDetailsViewModel::class.java] with(DomainRegistrationDetailsFragmentBinding.bind(view)) { binding = this setupObservers() @@ -100,7 +101,7 @@ class DomainRegistrationDetailsFragment : Fragment() { val domainProductDetails = requireNotNull( arguments?.getParcelable(EXTRA_DOMAIN_PRODUCT_DETAILS) ) - val site = requireActivity().intent?.getSerializableExtra(WordPress.SITE) as SiteModel + val site = requireNotNull(activity?.intent?.getSerializableExtraCompat(WordPress.SITE)) viewModel.start(site, domainProductDetails) @@ -444,6 +445,7 @@ class DomainRegistrationDetailsFragment : Fragment() { } } + @AndroidEntryPoint class StatePickerDialogFragment : DialogFragment() { private lateinit var states: ArrayList @@ -490,13 +492,9 @@ class DomainRegistrationDetailsFragment : Fragment() { return builder.create() } - - override fun onAttach(context: Context) { - super.onAttach(context) - AndroidSupportInjection.inject(this) - } } + @AndroidEntryPoint class CountryPickerDialogFragment : DialogFragment() { private lateinit var countries: ArrayList @@ -543,11 +541,6 @@ class DomainRegistrationDetailsFragment : Fragment() { return builder.create() } - - override fun onAttach(context: Context) { - super.onAttach(context) - AndroidSupportInjection.inject(this) - } } override fun onResume() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationResultFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationResultFragment.kt index b0691a90e18d..68f8fa0d1ae1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationResultFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainRegistrationResultFragment.kt @@ -2,7 +2,7 @@ package org.wordpress.android.ui.domains import android.os.Bundle import android.view.View -import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.text.HtmlCompat.FROM_HTML_MODE_COMPACT import androidx.core.text.parseAsHtml @@ -49,8 +49,8 @@ class DomainRegistrationResultFragment : Fragment(R.layout.domain_registration_r with(DomainRegistrationResultFragmentBinding.bind(view)) { setupViews(domainName, email) - setupObservers(domainName, email) } + requireActivity().onBackPressedDispatcher.addCallback(this) { finishRegistration(domainName, email) } } private fun setupWindow() = with(requireAppCompatActivity()) { @@ -78,14 +78,6 @@ class DomainRegistrationResultFragment : Fragment(R.layout.domain_registration_r ).parseAsHtml(FROM_HTML_MODE_COMPACT) } - private fun setupObservers(domainName: String, email: String) = with(requireActivity()) { - onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - finishRegistration(domainName, email) - } - }) - } - private fun finishRegistration(domainName: String, email: String) { mainViewModel.finishDomainRegistration(DomainRegistrationCompletedEvent(domainName, email)) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainSuggestionsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainSuggestionsFragment.kt index 09d64744f0b1..c96d63dedaf4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainSuggestionsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainSuggestionsFragment.kt @@ -18,6 +18,7 @@ import org.wordpress.android.ui.ScrollableViewInitializedListener import org.wordpress.android.ui.domains.DomainRegistrationActivity.Companion.DOMAIN_REGISTRATION_PURPOSE_KEY import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistrationPurpose import org.wordpress.android.util.ToastUtils +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -38,17 +39,19 @@ class DomainSuggestionsFragment : Fragment(R.layout.domain_suggestions_fragment) super.onViewCreated(view, savedInstanceState) (requireActivity().application as WordPress).component().inject(this) - mainViewModel = ViewModelProvider(requireActivity(), viewModelFactory) - .get(DomainRegistrationMainViewModel::class.java) + mainViewModel = ViewModelProvider( + requireActivity(), + viewModelFactory + )[DomainRegistrationMainViewModel::class.java] - viewModel = ViewModelProvider(this, viewModelFactory) - .get(DomainSuggestionsViewModel::class.java) + viewModel = ViewModelProvider(this, viewModelFactory)[DomainSuggestionsViewModel::class.java] with(DomainSuggestionsFragmentBinding.bind(view)) { val intent = requireActivity().intent - val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel - val domainRegistrationPurpose = intent.getSerializableExtra(DOMAIN_REGISTRATION_PURPOSE_KEY) - as DomainRegistrationPurpose + val site = requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE)) + val domainRegistrationPurpose = requireNotNull( + intent.getSerializableExtraCompat(DOMAIN_REGISTRATION_PURPOSE_KEY) + ) setupViews() setupObservers() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainSuggestionsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainSuggestionsViewModel.kt index 33de362b3ab1..9481a6e11951 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainSuggestionsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainSuggestionsViewModel.kt @@ -25,7 +25,6 @@ import org.wordpress.android.util.AppLog.T import org.wordpress.android.util.SiteUtils import org.wordpress.android.util.config.SiteDomainsFeatureConfig import org.wordpress.android.util.extensions.isOnSale -import org.wordpress.android.util.extensions.saleCostForDisplay import org.wordpress.android.util.helpers.Debouncer import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel @@ -186,7 +185,7 @@ class DomainSuggestionsViewModel @Inject constructor( domainName = it.domain_name, cost = it.cost, isOnSale = product.isOnSale(), - saleCost = product.saleCostForDisplay(), + saleCost = product?.combinedSaleCostDisplay.orEmpty(), isFree = it.is_free, supportsPrivacy = it.supports_privacy, productId = it.product_id, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardActivity.kt index b7a81a7e8f6b..9a8b55e2331e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardActivity.kt @@ -24,7 +24,7 @@ class DomainsDashboardActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardFragment.kt index b656a32509b2..79dff8216353 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/domains/DomainsDashboardFragment.kt @@ -18,6 +18,7 @@ import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistr import org.wordpress.android.ui.domains.DomainsDashboardNavigationAction.ClaimDomain import org.wordpress.android.ui.domains.DomainsDashboardNavigationAction.GetDomain import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -46,8 +47,8 @@ class DomainsDashboardFragment : Fragment(R.layout.domains_dashboard_fragment) { private fun setupViewModel() { val intent = requireActivity().intent - val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel - viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(DomainsDashboardViewModel::class.java) + val site = requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE)) + viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[DomainsDashboardViewModel::class.java] viewModel.start(site) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListActivity.kt index f96bcad38ac6..36c4417a872a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListActivity.kt @@ -7,6 +7,7 @@ import org.wordpress.android.WordPress import org.wordpress.android.databinding.EngagedPeopleListActivityBinding import org.wordpress.android.ui.LocaleAwareActivity import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper +import org.wordpress.android.util.extensions.getParcelableExtraCompat import javax.inject.Inject class EngagedPeopleListActivity : LocaleAwareActivity() { @@ -22,7 +23,7 @@ class EngagedPeopleListActivity : LocaleAwareActivity() { setSupportActionBar(toolbarMain) } - val listScenario = intent.getParcelableExtra(KEY_LIST_SCENARIO) + val listScenario = intent.getParcelableExtraCompat(KEY_LIST_SCENARIO) ?: throw IllegalArgumentException( "List Scenario cannot be null. Make sure to pass a valid List Scenario in the intent" ) @@ -59,7 +60,7 @@ class EngagedPeopleListActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListFragment.kt index fb0b3bbbed83..3c5937d693f3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/engagement/EngagedPeopleListFragment.kt @@ -39,6 +39,7 @@ import org.wordpress.android.util.SnackbarItem.Info import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.WPUrlUtils import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.ResourceProvider import org.wordpress.android.viewmodel.observeEvent @@ -93,17 +94,17 @@ class EngagedPeopleListFragment : Fragment() { loadingView = view.findViewById(R.id.loading_view) emptyView = view.findViewById(R.id.actionable_empty_view) - val listScenario = requireArguments().getParcelable(KEY_LIST_SCENARIO) + val listScenario = requireNotNull(arguments?.getParcelableCompat(KEY_LIST_SCENARIO)) val layoutManager = LinearLayoutManager(activity) - savedInstanceState?.getParcelable(KEY_LIST_STATE)?.let { + savedInstanceState?.getParcelableCompat(KEY_LIST_STATE)?.let { layoutManager.onRestoreInstanceState(it) } recycler.layoutManager = layoutManager - userProfileViewModel.onBottomSheetAction.observeEvent(viewLifecycleOwner, { state -> + userProfileViewModel.onBottomSheetAction.observeEvent(viewLifecycleOwner) { state -> var bottomSheet = childFragmentManager.findFragmentByTag(USER_PROFILE_BOTTOM_SHEET_TAG) as? UserProfileBottomSheetFragment @@ -118,33 +119,33 @@ class EngagedPeopleListFragment : Fragment() { bottomSheet?.apply { this.dismiss() } } } - }) + } - viewModel.uiState.observe(viewLifecycleOwner, { state -> + viewModel.uiState.observe(viewLifecycleOwner) { state -> if (!isAdded) return@observe updateUiState(state) - }) + } - viewModel.onNavigationEvent.observeEvent(viewLifecycleOwner, { event -> + viewModel.onNavigationEvent.observeEvent(viewLifecycleOwner) { event -> if (!isAdded) return@observeEvent manageNavigation(event) - }) + } - viewModel.onSnackbarMessage.observeEvent(viewLifecycleOwner, { messageHolder -> + viewModel.onSnackbarMessage.observeEvent(viewLifecycleOwner) { messageHolder -> if (!isAdded || !lifecycle.currentState.isAtLeast(State.RESUMED)) return@observeEvent showSnackbar(messageHolder) - }) + } - viewModel.onServiceRequestEvent.observeEvent(viewLifecycleOwner, { serviceRequest -> + viewModel.onServiceRequestEvent.observeEvent(viewLifecycleOwner) { serviceRequest -> if (!isAdded) return@observeEvent manageServiceRequest(serviceRequest) - }) + } - viewModel.start(listScenario!!) + viewModel.start(listScenario) } @Suppress("ForbiddenComment") diff --git a/WordPress/src/main/java/org/wordpress/android/ui/featureintroduction/FeatureIntroductionDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/featureintroduction/FeatureIntroductionDialogFragment.kt index 2806fdf6fc22..f2e8a2e41dbf 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/featureintroduction/FeatureIntroductionDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/featureintroduction/FeatureIntroductionDialogFragment.kt @@ -5,6 +5,8 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.ComponentDialog +import androidx.activity.addCallback import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.fragment.app.DialogFragment @@ -13,6 +15,7 @@ import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.databinding.FeatureIntroductionDialogFragmentBinding import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.extensions.onBackPressedCompat import org.wordpress.android.util.extensions.setStatusBarAsSurfaceColor import javax.inject.Inject @@ -31,12 +34,11 @@ abstract class FeatureIntroductionDialogFragment : DialogFragment() { } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = - object : Dialog(requireContext(), theme) { - override fun onBackPressed() { + super.onCreateDialog(savedInstanceState).apply { + (this as ComponentDialog).onBackPressedDispatcher.addCallback(this@FeatureIntroductionDialogFragment) { viewModel.onBackButtonClick() - super.onBackPressed() + onBackPressedDispatcher.onBackPressedCompat(this) } - }.apply { setStatusBarAsSurfaceColor() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailActivity.kt index 1f8537ee3772..09f053a4982a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailActivity.kt @@ -1,12 +1,15 @@ package org.wordpress.android.ui.history import android.os.Bundle +import androidx.activity.addCallback import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.databinding.HistoryDetailActivityBinding import org.wordpress.android.ui.LocaleAwareActivity import org.wordpress.android.ui.history.HistoryListItem.Revision +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.onBackPressedCompat class HistoryDetailActivity : LocaleAwareActivity() { companion object { @@ -19,10 +22,16 @@ class HistoryDetailActivity : LocaleAwareActivity() { setContentView(root) setSupportActionBar(toolbarMain) } + + onBackPressedDispatcher.addCallback(this) { + AnalyticsTracker.track(Stat.REVISIONS_DETAIL_CANCELLED) + onBackPressedDispatcher.onBackPressedCompat(this) + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) val extras = requireNotNull(intent.extras) - val revision = extras.getParcelable(HistoryDetailContainerFragment.EXTRA_CURRENT_REVISION) + val revision = extras.getParcelableCompat(HistoryDetailContainerFragment.EXTRA_CURRENT_REVISION) val previousRevisionsIds = extras.getLongArray(HistoryDetailContainerFragment.EXTRA_PREVIOUS_REVISIONS_IDS) val postId = extras.getLong(HistoryDetailContainerFragment.EXTRA_POST_ID) @@ -44,9 +53,4 @@ class HistoryDetailActivity : LocaleAwareActivity() { finish() return true } - - override fun onBackPressed() { - AnalyticsTracker.track(Stat.REVISIONS_DETAIL_CANCELLED) - super.onBackPressed() - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailFragment.kt index 3b1db67f62cc..64c8a4072b0c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/history/HistoryDetailFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import org.wordpress.android.R import org.wordpress.android.ui.history.HistoryListItem.Revision +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.widgets.DiffView class HistoryDetailFragment : Fragment() { @@ -16,9 +17,9 @@ class HistoryDetailFragment : Fragment() { super.onCreate(savedInstanceState) mRevision = if (savedInstanceState != null) { - savedInstanceState.getParcelable(KEY_REVISION) + savedInstanceState.getParcelableCompat(KEY_REVISION) } else { - arguments?.getParcelable(EXTRA_REVISION) + arguments?.getParcelableCompat(EXTRA_REVISION) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadActivity.kt index 1b84f71d463f..e240c9bea30b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadActivity.kt @@ -18,7 +18,7 @@ class BackupDownloadActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadFragment.kt index fafe0b4f4249..7c8914fee3fd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadFragment.kt @@ -5,7 +5,7 @@ import android.app.Activity.RESULT_OK import android.content.Intent import android.os.Bundle import android.view.View -import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider @@ -27,6 +27,7 @@ import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.observeEvent import org.wordpress.android.widgets.WPSnackbar @@ -51,7 +52,9 @@ class BackupDownloadFragment : Fragment(R.layout.jetpack_backup_restore_fragment super.onViewCreated(view, savedInstanceState) with(JetpackBackupRestoreFragmentBinding.bind(view)) { initDagger() - initBackPressHandler() + requireActivity().onBackPressedDispatcher.addCallback(this@BackupDownloadFragment) { + viewModel.onBackPressed() + } initAdapter() initViewModel(savedInstanceState) } @@ -61,22 +64,6 @@ class BackupDownloadFragment : Fragment(R.layout.jetpack_backup_restore_fragment (requireActivity().application as WordPress).component().inject(this) } - private fun initBackPressHandler() { - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback( - true - ) { - override fun handleOnBackPressed() { - onBackPressed() - } - }) - } - - private fun onBackPressed() { - viewModel.onBackPressed() - } - private fun JetpackBackupRestoreFragmentBinding.initAdapter() { recyclerView.adapter = JetpackBackupRestoreAdapter(imageManager, uiHelpers) recyclerView.itemAnimator = null @@ -89,11 +76,13 @@ class BackupDownloadFragment : Fragment(R.layout.jetpack_backup_restore_fragment viewModel = ViewModelProvider( this@BackupDownloadFragment, viewModelFactory - ).get(BackupDownloadViewModel::class.java) + )[BackupDownloadViewModel::class.java] val (site, activityId) = when { requireActivity().intent?.extras != null -> { - val site = requireNotNull(requireActivity().intent.extras).getSerializable(WordPress.SITE) as SiteModel + val site = requireNotNull( + requireActivity().intent.extras?.getSerializableCompat(WordPress.SITE) + ) val activityId = requireNotNull(requireActivity().intent.extras).getString( KEY_BACKUP_DOWNLOAD_ACTIVITY_ID_KEY ) as String diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadViewModel.kt index 3ea0109e2d30..cbbefc8ec3e5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadViewModel.kt @@ -58,6 +58,7 @@ import org.wordpress.android.ui.jetpack.usecases.GetActivityLogItemUseCase import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.ui.utils.UiString.UiStringText +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.text.PercentFormatter import org.wordpress.android.util.wizard.WizardManager import org.wordpress.android.util.wizard.WizardNavigationTarget @@ -139,7 +140,7 @@ class BackupDownloadViewModel @Inject constructor( // Show the next step only if it's a fresh activity so we can handle the navigation wizardManager.showNextStep() } else { - backupDownloadState = requireNotNull(savedInstanceState.getParcelable(KEY_BACKUP_DOWNLOAD_STATE)) + backupDownloadState = requireNotNull(savedInstanceState.getParcelableCompat(KEY_BACKUP_DOWNLOAD_STATE)) val currentStepIndex = savedInstanceState.getInt(KEY_BACKUP_DOWNLOAD_CURRENT_STEP) wizardManager.setCurrentStepIndex(currentStepIndex) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreActivity.kt index 705aa2ed00b1..436ae6875f5e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreActivity.kt @@ -17,7 +17,7 @@ class RestoreActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return false diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreFragment.kt index 49f05461e035..a9fe86a5eff9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreFragment.kt @@ -5,7 +5,7 @@ import android.app.Activity.RESULT_OK import android.content.Intent import android.os.Bundle import android.view.View -import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider @@ -27,6 +27,7 @@ import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.observeEvent import org.wordpress.android.widgets.WPSnackbar @@ -50,7 +51,7 @@ class RestoreFragment : Fragment(R.layout.jetpack_backup_restore_fragment) { super.onViewCreated(view, savedInstanceState) with(JetpackBackupRestoreFragmentBinding.bind(view)) { initDagger() - initBackPressHandler() + requireActivity().onBackPressedDispatcher.addCallback(this@RestoreFragment) { viewModel.onBackPressed() } initAdapter() initViewModel(savedInstanceState) } @@ -60,22 +61,6 @@ class RestoreFragment : Fragment(R.layout.jetpack_backup_restore_fragment) { (requireActivity().application as WordPress).component().inject(this) } - private fun initBackPressHandler() { - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback( - true - ) { - override fun handleOnBackPressed() { - onBackPressed() - } - }) - } - - private fun onBackPressed() { - viewModel.onBackPressed() - } - private fun JetpackBackupRestoreFragmentBinding.initAdapter() { recyclerView.adapter = JetpackBackupRestoreAdapter(imageManager, uiHelpers) recyclerView.itemAnimator = null @@ -85,11 +70,11 @@ class RestoreFragment : Fragment(R.layout.jetpack_backup_restore_fragment) { } private fun JetpackBackupRestoreFragmentBinding.initViewModel(savedInstanceState: Bundle?) { - viewModel = ViewModelProvider(this@RestoreFragment, viewModelFactory).get(RestoreViewModel::class.java) + viewModel = ViewModelProvider(this@RestoreFragment, viewModelFactory)[RestoreViewModel::class.java] val (site, activityId) = when { requireActivity().intent?.extras != null -> { - val site = requireNotNull(requireActivity().intent.extras).getSerializable(WordPress.SITE) as SiteModel + val site = requireNotNull(activity?.intent?.extras?.getSerializableCompat(WordPress.SITE)) val activityId = requireNotNull(requireActivity().intent.extras).getString( KEY_RESTORE_ACTIVITY_ID_KEY ) as String diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreViewModel.kt index 39f3d2422fea..744e81f45b16 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/restore/RestoreViewModel.kt @@ -61,6 +61,7 @@ import org.wordpress.android.ui.jetpack.usecases.GetActivityLogItemUseCase import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.ui.utils.UiString.UiStringText +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.text.PercentFormatter import org.wordpress.android.util.wizard.WizardManager import org.wordpress.android.util.wizard.WizardNavigationTarget @@ -142,7 +143,7 @@ class RestoreViewModel @Inject constructor( // Show the next step only if it's a fresh activity so we can handle the navigation wizardManager.showNextStep() } else { - restoreState = requireNotNull(savedInstanceState.getParcelable(KEY_RESTORE_STATE)) + restoreState = requireNotNull(savedInstanceState.getParcelableCompat(KEY_RESTORE_STATE)) val currentStepIndex = savedInstanceState.getInt(KEY_RESTORE_CURRENT_STEP) wizardManager.setCurrentStepIndex(currentStepIndex) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/ScanActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/ScanActivity.kt index bca93e35c0fe..5cad6f80f220 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/ScanActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/ScanActivity.kt @@ -11,13 +11,13 @@ import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.databinding.ScanActivityBinding -import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.ScrollableViewInitializedListener import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredBottomSheetFragment import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.models.JetpackPoweredScreen +import org.wordpress.android.util.extensions.getSerializableExtraCompat import javax.inject.Inject @AndroidEntryPoint @@ -83,11 +83,11 @@ class ScanActivity : AppCompatActivity(), ScrollableViewInitializedListener { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } else if (item.itemId == R.id.menu_scan_history) { // todo malinjir is it worth introducing a vm? - ActivityLauncher.viewScanHistory(this, intent.getSerializableExtra(WordPress.SITE) as SiteModel) + ActivityLauncher.viewScanHistory(this, intent.getSerializableExtraCompat(WordPress.SITE)) } return super.onOptionsItemSelected(item) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/ScanFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/ScanFragment.kt index eda56fb2aa2c..305967806c9c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/ScanFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/ScanFragment.kt @@ -30,6 +30,8 @@ import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.prefs.EmptyViewRecyclerView import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.ColorUtils +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.observeEvent import org.wordpress.android.widgets.WPSnackbar @@ -175,11 +177,14 @@ class ScanFragment : Fragment(R.layout.scan_fragment) { } private fun getSite(savedInstanceState: Bundle?): SiteModel { - return if (savedInstanceState == null) { - requireActivity().intent.getSerializableExtra(WordPress.SITE) as SiteModel - } else { - savedInstanceState.getSerializable(WordPress.SITE) as SiteModel - } + val site = requireNotNull( + if (savedInstanceState == null) { + requireActivity().intent.getSerializableExtraCompat(WordPress.SITE) + } else { + savedInstanceState.getSerializableCompat(WordPress.SITE) + } + ) + return site } override fun onSaveInstanceState(outState: Bundle) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/details/ThreatDetailsActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/details/ThreatDetailsActivity.kt index eb7b96ab3ff2..14f3c0043f1a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/details/ThreatDetailsActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/details/ThreatDetailsActivity.kt @@ -20,7 +20,7 @@ class ThreatDetailsActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/details/ThreatDetailsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/details/ThreatDetailsFragment.kt index 9aa46a8794ed..85fdefdedeb3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/details/ThreatDetailsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/details/ThreatDetailsFragment.kt @@ -22,6 +22,7 @@ import org.wordpress.android.ui.jetpack.scan.details.ThreatDetailsViewModel.UiSt import org.wordpress.android.ui.jetpack.scan.details.adapters.ThreatDetailsAdapter import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.observeEvent import org.wordpress.android.widgets.WPSnackbar @@ -68,39 +69,33 @@ class ThreatDetailsFragment : Fragment(R.layout.threat_details_fragment) { private fun ThreatDetailsFragmentBinding.setupObservers() { viewModel.uiState.observe( - viewLifecycleOwner, - { uiState -> - if (uiState is Content) { - refreshContentScreen(uiState) - } + viewLifecycleOwner + ) { uiState -> + if (uiState is Content) { + refreshContentScreen(uiState) } - ) - - viewModel.snackbarEvents.observeEvent(viewLifecycleOwner, { it.showSnackbar() }) - - viewModel.navigationEvents.observeEvent( - viewLifecycleOwner, - { events -> - when (events) { - is OpenThreatActionDialog -> showThreatActionDialog(events) - - is ShowUpdatedScanStateWithMessage -> { - val site = requireNotNull(requireActivity().intent.extras) - .getSerializable(WordPress.SITE) as SiteModel - ActivityLauncher.viewScanRequestScanState(requireActivity(), site, events.messageRes) - } - is ShowUpdatedFixState -> { - val site = requireNotNull(requireActivity().intent.extras) - .getSerializable(WordPress.SITE) as SiteModel - ActivityLauncher.viewScanRequestFixState(requireActivity(), site, events.threatId) - } - is ShowGetFreeEstimate -> { - ActivityLauncher.openUrlExternal(context, events.url()) - } - is ShowJetpackSettings -> ActivityLauncher.openUrlExternal(context, events.url) + } + + viewModel.snackbarEvents.observeEvent(viewLifecycleOwner) { it.showSnackbar() } + + viewModel.navigationEvents.observeEvent(viewLifecycleOwner) { events -> + when (events) { + is OpenThreatActionDialog -> showThreatActionDialog(events) + + is ShowUpdatedScanStateWithMessage -> { + val site = requireNotNull(activity?.intent?.extras).getSerializableCompat(WordPress.SITE) + ActivityLauncher.viewScanRequestScanState(requireActivity(), site, events.messageRes) + } + is ShowUpdatedFixState -> { + val site = requireNotNull(activity?.intent?.extras).getSerializableCompat(WordPress.SITE) + ActivityLauncher.viewScanRequestFixState(requireActivity(), site, events.threatId) } + is ShowGetFreeEstimate -> { + ActivityLauncher.openUrlExternal(context, events.url()) + } + is ShowJetpackSettings -> ActivityLauncher.openUrlExternal(context, events.url) } - ) + } } private fun ThreatDetailsFragmentBinding.refreshContentScreen(content: Content) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/history/ScanHistoryFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/history/ScanHistoryFragment.kt index 50174c191801..67ca756018e3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/history/ScanHistoryFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/history/ScanHistoryFragment.kt @@ -20,6 +20,7 @@ import org.wordpress.android.WordPress import org.wordpress.android.databinding.FullscreenErrorWithRetryBinding import org.wordpress.android.databinding.ScanHistoryFragmentBinding import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.models.JetpackPoweredScreen import org.wordpress.android.ui.ScrollableViewInitializedListener import org.wordpress.android.ui.jetpack.scan.history.ScanHistoryViewModel.TabUiState import org.wordpress.android.ui.jetpack.scan.history.ScanHistoryViewModel.UiState.ContentUiState @@ -27,8 +28,9 @@ import org.wordpress.android.ui.jetpack.scan.history.ScanHistoryViewModel.UiStat import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredBottomSheetFragment import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.JetpackBrandingUtils -import org.wordpress.android.models.JetpackPoweredScreen import org.wordpress.android.util.LocaleManagerWrapper +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import javax.inject.Inject @AndroidEntryPoint @@ -121,11 +123,14 @@ class ScanHistoryFragment : Fragment(R.layout.scan_history_fragment), MenuProvid } private fun getSite(savedInstanceState: Bundle?): SiteModel { - return if (savedInstanceState == null) { - requireActivity().intent.getSerializableExtra(WordPress.SITE) as SiteModel - } else { - savedInstanceState.getSerializable(WordPress.SITE) as SiteModel - } + val site = requireNotNull( + if (savedInstanceState == null) { + requireActivity().intent.getSerializableExtraCompat(WordPress.SITE) + } else { + savedInstanceState.getSerializableCompat(WordPress.SITE) + } + ) + return site } override fun onSaveInstanceState(outState: Bundle) { @@ -139,7 +144,7 @@ class ScanHistoryFragment : Fragment(R.layout.scan_history_fragment), MenuProvid override fun onMenuItemSelected(menuItem: MenuItem) = when (menuItem.itemId) { android.R.id.home -> { - requireActivity().onBackPressed() + requireActivity().onBackPressedDispatcher.onBackPressed() true } else -> false diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/history/ScanHistoryListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/history/ScanHistoryListFragment.kt index 65346ae58a5d..4017c1043a73 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/history/ScanHistoryListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpack/scan/history/ScanHistoryListFragment.kt @@ -17,6 +17,9 @@ import org.wordpress.android.ui.jetpack.scan.history.ScanHistoryListViewModel.Sc import org.wordpress.android.ui.jetpack.scan.history.ScanHistoryListViewModel.ScanHistoryUiState.EmptyUiState.EmptyHistory import org.wordpress.android.ui.jetpack.scan.history.ScanHistoryViewModel.ScanHistoryTabType import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -78,14 +81,17 @@ class ScanHistoryListFragment : ViewPagerFragment(R.layout.scan_history_list_fra } private fun getSite(savedInstanceState: Bundle?): SiteModel { - return if (savedInstanceState == null) { - requireActivity().intent.getSerializableExtra(WordPress.SITE) as SiteModel - } else { - savedInstanceState.getSerializable(WordPress.SITE) as SiteModel - } + val site = requireNotNull( + if (savedInstanceState == null) { + requireActivity().intent.getSerializableExtraCompat(WordPress.SITE) + } else { + savedInstanceState.getSerializableCompat(WordPress.SITE) + } + ) + return site } - private fun getTabType(): ScanHistoryTabType = requireNotNull(arguments?.getParcelable(ARG_TAB_TYPE)) + private fun getTabType() = requireNotNull(arguments?.getParcelableCompat(ARG_TAB_TYPE)) override fun getScrollableViewForUniqueIdProvision(): View? = binding?.recyclerView diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureFullScreenOverlayFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureFullScreenOverlayFragment.kt index 0d12c1e9d646..0f87850bff38 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureFullScreenOverlayFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureFullScreenOverlayFragment.kt @@ -1,13 +1,10 @@ package org.wordpress.android.ui.jetpackoverlay -import android.content.res.Resources import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout import androidx.fragment.app.activityViewModels -import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint @@ -27,6 +24,8 @@ import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.RtlUtils import org.wordpress.android.util.UrlUtils import org.wordpress.android.util.extensions.exhaustive +import org.wordpress.android.util.extensions.fillScreen +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.extensions.setVisible import javax.inject.Inject @@ -65,51 +64,24 @@ class JetpackFeatureFullScreenOverlayFragment : BottomSheetDialogFragment() { RtlUtils.isRtl(view.context) ) binding.setupObservers() - - (dialog as? BottomSheetDialog)?.apply { - setOnShowListener { - val bottomSheet: FrameLayout = dialog?.findViewById( - com.google.android.material.R.id.design_bottom_sheet - ) ?: return@setOnShowListener - val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) - bottomSheetBehavior.maxWidth = ViewGroup.LayoutParams.MATCH_PARENT - bottomSheetBehavior.isDraggable = false - if (bottomSheet.layoutParams != null) { - showFullScreenBottomSheet(bottomSheet) - } - expandBottomSheet(bottomSheetBehavior) - } - } - } - - private fun showFullScreenBottomSheet(bottomSheet: FrameLayout) { - val layoutParams = bottomSheet.layoutParams - layoutParams.height = Resources.getSystem().displayMetrics.heightPixels - bottomSheet.layoutParams = layoutParams - } - - private fun expandBottomSheet(bottomSheetBehavior: BottomSheetBehavior) { - bottomSheetBehavior.skipCollapsed = true - bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + (dialog as? BottomSheetDialog)?.fillScreen() } - private fun getSiteScreen() = - arguments?.getSerializable(OVERLAY_SCREEN_TYPE) as JetpackFeatureOverlayScreenType? + private fun getSiteScreen() = arguments?.getSerializableCompat(OVERLAY_SCREEN_TYPE) - private fun getIfSiteCreationOverlay() = - arguments?.getSerializable(IS_SITE_CREATION_OVERLAY) as Boolean + private fun getIfSiteCreationOverlay() = arguments?.getBoolean(IS_SITE_CREATION_OVERLAY) ?: false - private fun getIfDeepLinkOverlay() = - arguments?.getSerializable(IS_DEEP_LINK_OVERLAY) as Boolean + private fun getIfDeepLinkOverlay() = arguments?.getBoolean(IS_DEEP_LINK_OVERLAY) ?: false - private fun getSiteCreationSource() = - arguments?.getSerializable(SITE_CREATION_OVERLAY_SOURCE) as SiteCreationSource + private fun getSiteCreationSource() = requireNotNull( + arguments?.getSerializableCompat(SITE_CREATION_OVERLAY_SOURCE) + ) - private fun getIfFeatureCollectionOverlay() = - arguments?.getSerializable(IS_FEATURE_COLLECTION_OVERLAY) as Boolean + private fun getIfFeatureCollectionOverlay() = arguments?.getBoolean(IS_FEATURE_COLLECTION_OVERLAY) ?: false - private fun getFeatureCollectionOverlaysSource() = - arguments?.getSerializable(FEATURE_COLLECTION_OVERLAY_SOURCE) as JetpackFeatureCollectionOverlaySource + private fun getFeatureCollectionOverlaysSource() = requireNotNull( + arguments?.getSerializableCompat(FEATURE_COLLECTION_OVERLAY_SOURCE) + ) private fun JetpackFeatureRemovalOverlayBinding.setupObservers() { viewModel.uiState.observe(viewLifecycleOwner) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtil.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtil.kt index 5186207eec5b..cd3b272ec97b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtil.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtil.kt @@ -12,6 +12,7 @@ import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseF import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseOne import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseThree import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseTwo +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseStaticPosters import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringPluralRes import org.wordpress.android.ui.utils.UiString.UiStringRes @@ -28,17 +29,20 @@ class JetpackFeatureRemovalBrandingUtil @Inject constructor( private val jpDeadlineConfig: JPDeadlineConfig, private val dateTimeUtilsWrapper: DateTimeUtilsWrapper ) { - private val jpDeadlineDate: String? by lazy { + private val jpDeadlineDate: String by lazy { jpDeadlineConfig.getValue() } fun isInRemovalPhase() = jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures() + fun shouldShowBrandingInDashboard() = jetpackFeatureRemovalPhaseHelper.shouldShowJetpackBrandingInDashboard() + fun shouldShowPhaseOneBranding(): Boolean { return when (jetpackFeatureRemovalPhaseHelper.getCurrentPhase()) { PhaseOne, PhaseTwo, PhaseThree, + PhaseStaticPosters, PhaseFour -> true else -> false } @@ -48,6 +52,7 @@ class JetpackFeatureRemovalBrandingUtil @Inject constructor( return when (jetpackFeatureRemovalPhaseHelper.getCurrentPhase()) { PhaseTwo, PhaseThree, + PhaseStaticPosters, PhaseFour -> true else -> false } @@ -55,6 +60,7 @@ class JetpackFeatureRemovalBrandingUtil @Inject constructor( fun getBrandingTextByPhase(screen: JetpackPoweredScreen): UiString { return when (jetpackFeatureRemovalPhaseHelper.getCurrentPhase()) { + PhaseStaticPosters -> UiStringRes(R.string.wp_jetpack_feature_removal_static_posters_phase) PhaseThree -> (screen as? JetpackPoweredScreen.WithDynamicText)?.let { screenWithDynamicText -> getDynamicBrandingForScreen(screenWithDynamicText) } ?: UiStringRes(JetpackBrandingUiState.RES_JP_POWERED) @@ -85,7 +91,7 @@ class JetpackFeatureRemovalBrandingUtil @Inject constructor( } } - private fun retrieveDeadline(): LocalDate? = jpDeadlineDate?.let { + private fun retrieveDeadline(): LocalDate? = jpDeadlineDate.takeIf { it.isNotBlank() }?.let { dateTimeUtilsWrapper.parseDateString(it, JETPACK_OVERLAY_ORIGINAL_DATE_FORMAT)?.toLocalDate() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalOverlayUtil.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalOverlayUtil.kt index 39b3c5caaf3b..104a107e020b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalOverlayUtil.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalOverlayUtil.kt @@ -14,6 +14,7 @@ import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseO import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseSelfHostedUsers import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseThree import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseTwo +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseStaticPosters import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource import org.wordpress.android.util.BuildConfigWrapper @@ -85,7 +86,7 @@ class JetpackFeatureRemovalOverlayUtil @Inject constructor( when (jetpackFeatureRemovalPhaseHelper.getCurrentPhase()) { null -> false PhaseOne, PhaseTwo, PhaseThree -> true - PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false + PhaseStaticPosters, PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelper.kt index e1d91a9decb1..f7488d1ef302 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelper.kt @@ -6,6 +6,7 @@ import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseO import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseThree import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseTwo import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseSelfHostedUsers +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhase.PhaseStaticPosters import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalSiteCreationPhase.PHASE_ONE import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalSiteCreationPhase.PHASE_TWO import org.wordpress.android.util.BuildConfigWrapper @@ -15,6 +16,7 @@ import org.wordpress.android.util.config.JetpackFeatureRemovalPhaseOneConfig import org.wordpress.android.util.config.JetpackFeatureRemovalPhaseThreeConfig import org.wordpress.android.util.config.JetpackFeatureRemovalPhaseTwoConfig import org.wordpress.android.util.config.JetpackFeatureRemovalSelfHostedUsersConfig +import org.wordpress.android.util.config.JetpackFeatureRemovalStaticPostersConfig import javax.inject.Inject private const val PHASE_ONE_GLOBAL_OVERLAY_FREQUENCY_IN_DAYS = 2 @@ -35,12 +37,14 @@ class JetpackFeatureRemovalPhaseHelper @Inject constructor( private val jetpackFeatureRemovalPhaseThreeConfig: JetpackFeatureRemovalPhaseThreeConfig, private val jetpackFeatureRemovalPhaseFourConfig: JetpackFeatureRemovalPhaseFourConfig, private val jetpackFeatureRemovalNewUsersConfig: JetpackFeatureRemovalNewUsersConfig, - private val jetpackFeatureRemovalSelfHostedUsersConfig: JetpackFeatureRemovalSelfHostedUsersConfig + private val jetpackFeatureRemovalSelfHostedUsersConfig: JetpackFeatureRemovalSelfHostedUsersConfig, + private val jetpackFeatureRemovalStaticPostersConfig: JetpackFeatureRemovalStaticPostersConfig ) { fun getCurrentPhase(): JetpackFeatureRemovalPhase? { return if (buildConfigWrapper.isJetpackApp) null else if (jetpackFeatureRemovalSelfHostedUsersConfig.isEnabled()) PhaseSelfHostedUsers else if (jetpackFeatureRemovalNewUsersConfig.isEnabled()) PhaseNewUsers + else if (jetpackFeatureRemovalStaticPostersConfig.isEnabled()) PhaseStaticPosters else if (jetpackFeatureRemovalPhaseFourConfig.isEnabled()) PhaseFour else if (jetpackFeatureRemovalPhaseThreeConfig.isEnabled()) PhaseThree else if (jetpackFeatureRemovalPhaseTwoConfig.isEnabled()) PhaseTwo @@ -52,14 +56,14 @@ class JetpackFeatureRemovalPhaseHelper @Inject constructor( val currentPhase = getCurrentPhase() ?: return null return when (currentPhase) { is PhaseOne, PhaseTwo, PhaseThree -> PHASE_ONE - is PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> PHASE_TWO + is PhaseFour, PhaseStaticPosters, PhaseNewUsers, PhaseSelfHostedUsers -> PHASE_TWO } } fun getDeepLinkPhase(): JetpackFeatureRemovalSiteCreationPhase? { val currentPhase = getCurrentPhase() ?: return null return when (currentPhase) { - is PhaseOne, PhaseTwo, PhaseThree -> PHASE_ONE + is PhaseOne, PhaseTwo, PhaseThree, PhaseStaticPosters -> PHASE_ONE is PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> PHASE_TWO } } @@ -67,8 +71,64 @@ class JetpackFeatureRemovalPhaseHelper @Inject constructor( fun shouldRemoveJetpackFeatures(): Boolean { val currentPhase = getCurrentPhase() ?: return false return when (currentPhase) { - is PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers-> true - is PhaseOne, PhaseTwo, PhaseThree -> false + is PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> true + is PhaseOne, PhaseTwo, PhaseThree, PhaseStaticPosters -> false + } + } + + fun shouldShowDashboard(): Boolean { + val currentPhase = getCurrentPhase() ?: return true + return when (currentPhase) { + is PhaseStaticPosters, PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false + else -> true + } + } + + fun shouldShowStoryPost(): Boolean { + val currentPhase = getCurrentPhase() ?: return true + return when (currentPhase) { + is PhaseStaticPosters, PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false + else -> true + } + } + + fun shouldShowJetpackPoweredEditorFeatures(): Boolean { + val currentPhase = getCurrentPhase() ?: return true + return when (currentPhase) { + is PhaseStaticPosters, PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false + else -> true + } + } + + fun shouldShowTemplateSelectionInPages(): Boolean { + val currentPhase = getCurrentPhase() ?: return true + return when (currentPhase) { + is PhaseStaticPosters, PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false + else -> true + } + } + + fun shouldShowPublishedPostStatsButton(): Boolean { + val currentPhase = getCurrentPhase() ?: return true + return when (currentPhase) { + is PhaseStaticPosters, PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false + else -> true + } + } + + fun shouldShowJetpackBrandingInDashboard(): Boolean { + val currentPhase = getCurrentPhase() ?: return false + return when (currentPhase) { + is PhaseStaticPosters, PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false + else -> true + } + } + + fun shouldShowStaticPage(): Boolean { + val currentPhase = getCurrentPhase() ?: return false + return when (currentPhase) { + is PhaseStaticPosters -> true + is PhaseOne, PhaseTwo, PhaseThree, PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false } } @@ -76,7 +136,23 @@ class JetpackFeatureRemovalPhaseHelper @Inject constructor( val currentPhase = getCurrentPhase() ?: return true return when (currentPhase) { is PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false - is PhaseOne, PhaseTwo, PhaseThree -> true + is PhaseOne, PhaseTwo, PhaseThree, PhaseStaticPosters -> true + } + } + + fun shouldShowQuickStart(): Boolean { + val currentPhase = getCurrentPhase() ?: return true + return when (currentPhase) { + is PhaseStaticPosters, PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false + else -> true + } + } + + fun shouldShowHelpAndSupportOnEditor(): Boolean { + val currentPhase = getCurrentPhase() ?: return true + return when (currentPhase) { + is PhaseStaticPosters, PhaseFour, PhaseNewUsers, PhaseSelfHostedUsers -> false + else -> true } } } @@ -108,6 +184,7 @@ sealed class JetpackFeatureRemovalPhase( "three" ) + object PhaseStaticPosters : JetpackFeatureRemovalPhase(trackingName = "static_posters") object PhaseFour : JetpackFeatureRemovalPhase(trackingName = "four") object PhaseNewUsers : JetpackFeatureRemovalPhase(trackingName = "new_users") object PhaseSelfHostedUsers : JetpackFeatureRemovalPhase(trackingName = "self_hosted") diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalWidgetHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalWidgetHelper.kt index c209d2b2b21e..ecf6f8ad0f6d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalWidgetHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalWidgetHelper.kt @@ -13,7 +13,8 @@ class JetpackFeatureRemovalWidgetHelper @Inject constructor( ) fun disableWidgetReceiversIfNeeded() { - if (jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()) { + if (jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures() || + jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { widgetReceivers.forEach { packageManagerWrapper.disableComponentEnabledSetting(it) } } else { widgetReceivers.forEach { packageManagerWrapper.enableComponentEnabledSetting(it) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackStaticPosterActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackStaticPosterActivity.kt new file mode 100644 index 000000000000..e19ca548f3f8 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/JetpackStaticPosterActivity.kt @@ -0,0 +1,46 @@ +package org.wordpress.android.ui.jetpackoverlay + +import android.os.Bundle +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.Composable +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.ViewCompat +import androidx.fragment.app.commit +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.models.JetpackPoweredScreen +import org.wordpress.android.ui.main.jetpack.staticposter.JetpackStaticPosterFragment +import org.wordpress.android.util.extensions.setContent + +@AndroidEntryPoint +class JetpackStaticPosterActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { ComposeFrame() } + } + + @Composable + fun ComposeFrame() { + AndroidView( + factory = { context -> + FrameLayout(context).apply { + id = ViewCompat.generateViewId() + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + }, + update = { fragment -> + supportFragmentManager.commit { + replace( + fragment.id, + JetpackStaticPosterFragment.newInstance(JetpackPoweredScreen.WithStaticPoster.STATS) + ) + } + } + ) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginAnalyticsTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginAnalyticsTracker.kt new file mode 100644 index 000000000000..8d0e2645b12d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginAnalyticsTracker.kt @@ -0,0 +1,21 @@ +package org.wordpress.android.ui.jetpackoverlay.individualplugin + +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import javax.inject.Inject + +class WPJetpackIndividualPluginAnalyticsTracker @Inject constructor( + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, +) { + fun trackScreenShown() = analyticsTrackerWrapper.track( + AnalyticsTracker.Stat.WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_SHOWN, emptyMap() + ) + + fun trackScreenDismissed() = analyticsTrackerWrapper.track( + AnalyticsTracker.Stat.WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_DISMISSED, emptyMap() + ) + + fun trackPrimaryButtonClick() = analyticsTrackerWrapper.track( + AnalyticsTracker.Stat.WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_PRIMARY_TAPPED, emptyMap() + ) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginFragment.kt new file mode 100644 index 000000000000..7620d77b110a --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginFragment.kt @@ -0,0 +1,102 @@ +package org.wordpress.android.ui.jetpackoverlay.individualplugin + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.wordpress.android.ui.ActivityLauncherWrapper +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginViewModel.ActionEvent +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginViewModel.UiState +import org.wordpress.android.ui.jetpackoverlay.individualplugin.compose.WPJetpackIndividualPluginOverlayScreen +import org.wordpress.android.util.extensions.exhaustive +import org.wordpress.android.util.extensions.fillScreen +import javax.inject.Inject + +@AndroidEntryPoint +class WPJetpackIndividualPluginFragment : BottomSheetDialogFragment() { + private val viewModel: WPJetpackIndividualPluginViewModel by viewModels() + + @Inject + lateinit var activityLauncher: ActivityLauncherWrapper + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = ComposeView(requireContext()).apply { + setContent { + AppTheme { + val uiState by viewModel.uiState.collectAsState() + when (val state = uiState) { + is UiState.Loaded -> { + WPJetpackIndividualPluginOverlayScreen( + state.sites, + onCloseClick = viewModel::onDismissScreenClick, + onPrimaryButtonClick = viewModel::onPrimaryButtonClick, + onSecondaryButtonClick = viewModel::onDismissScreenClick, + ) + } + + is UiState.None -> {} + } + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.onScreenShown() + observeActionEvents() + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + super.onCreateDialog(savedInstanceState).apply { + (this as? BottomSheetDialog)?.fillScreen() + } + + override fun onCancel(dialog: DialogInterface) { + // called when user hits the back button + viewModel.onDismissScreenClick() + } + + private fun observeActionEvents() { + viewModel.actionEvents.onEach(this::handleActionEvents).launchIn(lifecycleScope) + } + + private fun handleActionEvents(actionEvent: ActionEvent) { + when (actionEvent) { + is ActionEvent.PrimaryButtonClick -> activityLauncher.openPlayStoreLink( + requireActivity(), + ActivityLauncherWrapper.JETPACK_PACKAGE_NAME + ) + + is ActionEvent.Dismiss -> dismiss() + }.exhaustive + } + + companion object { + const val TAG = "WP_JETPACK_INDIVIDUAL_PLUGIN_FRAGMENT" + + @JvmStatic + fun newInstance(): WPJetpackIndividualPluginFragment = WPJetpackIndividualPluginFragment() + + @JvmStatic + fun show(fm: FragmentManager): WPJetpackIndividualPluginFragment = newInstance().also { + it.show(fm, TAG) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginHelper.kt new file mode 100644 index 000000000000..4dead6498e0d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginHelper.kt @@ -0,0 +1,86 @@ +package org.wordpress.android.ui.jetpackoverlay.individualplugin + +import org.wordpress.android.fluxc.persistence.JetpackCPConnectedSiteModel +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.util.StringUtils +import org.wordpress.android.util.UrlUtils +import org.wordpress.android.util.config.WPIndividualPluginOverlayFeatureConfig +import org.wordpress.android.util.config.WPIndividualPluginOverlayMaxShownConfig +import org.wordpress.android.util.extensions.activeIndividualJetpackPluginNames +import org.wordpress.android.util.extensions.isJetpackIndividualPluginConnectedWithoutFullPlugin +import javax.inject.Inject + +class WPJetpackIndividualPluginHelper @Inject constructor( + private val siteStore: SiteStore, + private val appPrefs: AppPrefsWrapper, + private val wpIndividualPluginOverlayFeatureConfig: WPIndividualPluginOverlayFeatureConfig, + private val wpIndividualPluginOverlayMaxShownConfig: WPIndividualPluginOverlayMaxShownConfig, +) { + suspend fun shouldShowJetpackIndividualPluginOverlay(): Boolean { + return wpIndividualPluginOverlayFeatureConfig.isEnabled() && + hasIndividualPluginJetpackConnectedSites() && + !wasOverlayShownOverMaxTimes() && + !wasOverlayShownRecently() + } + + suspend fun getJetpackConnectedSitesWithIndividualPlugins(): List { + val individualPluginConnectedSites = getIndividualPluginJetpackConnectedSites() + return individualPluginConnectedSites.map { site -> + SiteWithIndividualJetpackPlugins( + name = site.name, + url = StringUtils.removeTrailingSlash(UrlUtils.removeScheme(site.url)), + individualPluginNames = site.activeIndividualJetpackPluginNames(), + ) + } + } + + fun onJetpackIndividualPluginOverlayShown() { + appPrefs.incrementWPJetpackIndividualPluginOverlayShownCount() + appPrefs.wpJetpackIndividualPluginOverlayLastShownTimestamp = System.currentTimeMillis() + } + + private suspend fun hasIndividualPluginJetpackConnectedSites(): Boolean { + val individualPluginConnectedSites = getIndividualPluginJetpackConnectedSites() + return individualPluginConnectedSites.isNotEmpty() + } + + private suspend fun getIndividualPluginJetpackConnectedSites(): List { + return siteStore.getJetpackCPConnectedSites() + .filter { it.isJetpackIndividualPluginConnectedWithoutFullPlugin() } + } + + private fun wasOverlayShownOverMaxTimes(): Boolean { + val overlayMaxShownCount = wpIndividualPluginOverlayMaxShownConfig.getValue() + return appPrefs.wpJetpackIndividualPluginOverlayShownCount >= overlayMaxShownCount + } + + private fun wasOverlayShownRecently(): Boolean { + val lastShownTimestamp = appPrefs.wpJetpackIndividualPluginOverlayLastShownTimestamp + val shownCount = appPrefs.wpJetpackIndividualPluginOverlayShownCount + val delayBetweenOverlays = getDelayBetweenOverlays(shownCount) + return System.currentTimeMillis() - lastShownTimestamp < delayBetweenOverlays + } + + companion object { + private const val DAY_IN_MILLIS = 24 * 60 * 60 * 1000L + private const val DELAY_BETWEEN_OVERLAYS_FIRST_TIME = 1 * DAY_IN_MILLIS + private const val DELAY_BETWEEN_OVERLAYS_SECOND_TIME = 3 * DAY_IN_MILLIS + private const val DELAY_BETWEEN_OVERLAYS_OTHER_TIMES = 7 * DAY_IN_MILLIS + + private fun getDelayBetweenOverlays(shownCount: Int): Long { + if (shownCount < 1) return 0L + return when (shownCount) { + 1 -> DELAY_BETWEEN_OVERLAYS_FIRST_TIME + 2 -> DELAY_BETWEEN_OVERLAYS_SECOND_TIME + else -> DELAY_BETWEEN_OVERLAYS_OTHER_TIMES + } + } + } +} + +data class SiteWithIndividualJetpackPlugins( + val name: String, + val url: String, + val individualPluginNames: List, +) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginViewModel.kt new file mode 100644 index 000000000000..449c58f0a6a2 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginViewModel.kt @@ -0,0 +1,61 @@ +package org.wordpress.android.ui.jetpackoverlay.individualplugin + +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.viewmodel.ScopedViewModel +import javax.inject.Inject +import javax.inject.Named + +@HiltViewModel +class WPJetpackIndividualPluginViewModel @Inject constructor( + private val wpJetpackIndividualPluginHelper: WPJetpackIndividualPluginHelper, + private val analyticsTracker: WPJetpackIndividualPluginAnalyticsTracker, + @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, +) : ScopedViewModel(bgDispatcher) { + private val _uiState = MutableStateFlow(UiState.None) + val uiState = _uiState.asStateFlow() + + private val _actionEvents = MutableSharedFlow() + val actionEvents = _actionEvents + + fun onScreenShown() { + if (_uiState.value != UiState.None) return + launch { + val sites = wpJetpackIndividualPluginHelper.getJetpackConnectedSitesWithIndividualPlugins() + _uiState.update { UiState.Loaded(sites) } + wpJetpackIndividualPluginHelper.onJetpackIndividualPluginOverlayShown() + analyticsTracker.trackScreenShown() + } + } + + fun onDismissScreenClick() { + postActionEvent(ActionEvent.Dismiss) + analyticsTracker.trackScreenDismissed() + } + + fun onPrimaryButtonClick() { + postActionEvent(ActionEvent.PrimaryButtonClick) + analyticsTracker.trackPrimaryButtonClick() + } + + private fun postActionEvent(actionEvent: ActionEvent) { + launch { _actionEvents.emit(actionEvent) } + } + + sealed class UiState { + object None : UiState() + data class Loaded( + val sites: List, + ) : UiState() + } + + sealed class ActionEvent { + object PrimaryButtonClick : ActionEvent() + object Dismiss : ActionEvent() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/compose/MultipleSitesContent.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/compose/MultipleSitesContent.kt new file mode 100644 index 000000000000..f9766c5b0f3c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/compose/MultipleSitesContent.kt @@ -0,0 +1,70 @@ +package org.wordpress.android.ui.jetpackoverlay.individualplugin.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.LocalContentColor +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.wordpress.android.R +import org.wordpress.android.ui.compose.utils.htmlToAnnotatedString +import org.wordpress.android.ui.jetpackoverlay.individualplugin.SiteWithIndividualJetpackPlugins + +@Composable +fun MultipleSitesContent( + sites: List +) { + Text( + text = stringResource(R.string.wp_jetpack_individual_plugin_overlay_multiple_sites_content_1), + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(R.string.wp_jetpack_individual_plugin_overlay_content_2) + ) + Spacer(modifier = Modifier.height(16.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + ) { + sites.forEach { site -> + MultipleSitesContentItem(site = site) + } + } +} + +@Composable +private fun MultipleSitesContentItem( + site: SiteWithIndividualJetpackPlugins +) { + val text = if (site.individualPluginNames.size > 1) { + stringResource( + R.string.wp_jetpack_individual_plugin_overlay_multiple_sites_content_item_multiple_plugins, + site.url, + site.individualPluginNames.size, + ) + } else { + stringResource( + R.string.wp_jetpack_individual_plugin_overlay_multiple_sites_content_item_single_plugin, + site.url, + site.individualPluginNames.firstOrNull().orEmpty(), + ) + } + + Text( + text = htmlToAnnotatedString(text), + modifier = Modifier.padding(vertical = 12.dp) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(LocalContentColor.current.copy(alpha = 0.1f)) + ) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/compose/SingleSiteContent.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/compose/SingleSiteContent.kt new file mode 100644 index 000000000000..6385e4c1e123 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/compose/SingleSiteContent.kt @@ -0,0 +1,34 @@ +package org.wordpress.android.ui.jetpackoverlay.individualplugin.compose + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.wordpress.android.R +import org.wordpress.android.ui.compose.utils.htmlToAnnotatedString +import org.wordpress.android.ui.jetpackoverlay.individualplugin.SiteWithIndividualJetpackPlugins + +@Composable +fun SingleSiteContent( + site: SiteWithIndividualJetpackPlugins +) { + val firstParagraphContent = if (site.individualPluginNames.size > 1) { + stringResource( + R.string.wp_jetpack_individual_plugin_overlay_single_site_multiple_plugins_content_1, + site.url, + ) + } else { + stringResource( + R.string.wp_jetpack_individual_plugin_overlay_single_site_single_plugin_content_1, + site.url, + site.individualPluginNames.firstOrNull().orEmpty(), + ) + } + + Text(htmlToAnnotatedString(firstParagraphContent)) + Spacer(modifier = Modifier.height(16.dp)) + Text(stringResource(R.string.wp_jetpack_individual_plugin_overlay_content_2)) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/compose/WPJetpackIndividualPluginOverlayScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/compose/WPJetpackIndividualPluginOverlayScreen.kt new file mode 100644 index 000000000000..f9b85431953c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/compose/WPJetpackIndividualPluginOverlayScreen.kt @@ -0,0 +1,248 @@ +package org.wordpress.android.ui.jetpackoverlay.individualplugin.compose + +import android.content.res.Configuration +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.wordpress.android.R +import org.wordpress.android.ui.compose.components.MainTopAppBar +import org.wordpress.android.ui.compose.components.NavigationIcons +import org.wordpress.android.ui.compose.components.buttons.ButtonSize +import org.wordpress.android.ui.compose.components.buttons.PrimaryButton +import org.wordpress.android.ui.compose.components.buttons.SecondaryButton +import org.wordpress.android.ui.compose.theme.AppColor +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.theme.JpColorPalette +import org.wordpress.android.ui.jetpackoverlay.individualplugin.SiteWithIndividualJetpackPlugins +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.compose.component.JPInstallFullPluginAnimation + +private val TitleTextStyle + @ReadOnlyComposable + @Composable + get() = TextStyle( + fontSize = 28.sp, + lineHeight = 36.sp, + fontWeight = FontWeight.Bold, + ) + +private val ContentTextStyle + @ReadOnlyComposable + @Composable + get() = TextStyle( + fontSize = 15.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ) + +private val ContentMargin = 20.dp + +@Composable +fun WPJetpackIndividualPluginOverlayScreen( + sites: List, + onCloseClick: () -> Unit, + onPrimaryButtonClick: () -> Unit, + onSecondaryButtonClick: () -> Unit, +) { + Scaffold( + topBar = { + MainTopAppBar( + title = null, + navigationIcon = NavigationIcons.CloseIcon, + onNavigationIconClick = onCloseClick + ) + } + ) { + val orientation = LocalConfiguration.current.orientation + val isLandscape = remember(orientation) { orientation == Configuration.ORIENTATION_LANDSCAPE } + + Column( + modifier = Modifier + .fillMaxSize() + .let { + if (isLandscape) it.verticalScroll(rememberScrollState()) else it + } + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .let { + if (!isLandscape) { + it + .weight(1f) + .verticalScroll(rememberScrollState()) + } else { + it + } + } + .padding(ContentMargin), + verticalArrangement = Arrangement.Center, + ) { + // Icon + JPInstallFullPluginAnimation( + modifier = Modifier + .align(Alignment.Start) + .let { + if (isLandscape) it.height(48.dp) else it + } + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // Title + Text( + text = getTitle(siteCount = sites.size), + style = TitleTextStyle, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Content + CompositionLocalProvider(LocalTextStyle provides ContentTextStyle) { + when { + sites.size > 1 -> MultipleSitesContent(sites) + sites.size == 1 -> SingleSiteContent(sites.first()) + } + } + } + + // Buttons + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp, bottom = ContentMargin) + .padding(horizontal = ContentMargin), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + PrimaryButton( + text = stringResource(R.string.wp_jetpack_individual_plugin_overlay_primary_button), + onClick = onPrimaryButtonClick, + buttonSize = ButtonSize.LARGE, + padding = PaddingValues(0.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = JpColorPalette().primary, + contentColor = AppColor.White, + ), + ) + SecondaryButton( + text = stringResource(R.string.wp_jetpack_continue_without_jetpack), + onClick = onSecondaryButtonClick, + buttonSize = ButtonSize.LARGE, + padding = PaddingValues(0.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color.Transparent, + contentColor = JpColorPalette().primary, + ), + ) + } + } + } +} + +@ReadOnlyComposable +@Composable +private fun getTitle(siteCount: Int): String = if (siteCount > 1) { + stringResource(R.string.wp_jetpack_individual_plugin_overlay_multiple_sites_title) +} else { + stringResource(R.string.wp_jetpack_individual_plugin_overlay_single_site_title) +} + +@Preview +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Preview(widthDp = 720, heightDp = 360) +@Composable +fun WPJetpackIndividualPluginOverlayScreenSingleSiteSinglePluginPreview() { + AppTheme { + WPJetpackIndividualPluginOverlayScreen( + sites = listOf( + SiteWithIndividualJetpackPlugins( + name = "Site 1", + url = "site1.wordpress.com", + individualPluginNames = listOf("Jetpack Social") + ), + ), + onCloseClick = {}, + onPrimaryButtonClick = {}, + onSecondaryButtonClick = {}, + ) + } +} + +@Preview +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Preview(widthDp = 720, heightDp = 360) +@Composable +fun WPJetpackIndividualPluginOverlayScreenSingleSiteMultiplePluginsPreview() { + AppTheme { + WPJetpackIndividualPluginOverlayScreen( + sites = listOf( + SiteWithIndividualJetpackPlugins( + name = "Site 1", + url = "site1.wordpress.com", + individualPluginNames = listOf("Jetpack Social", "Jetpack Search") + ), + ), + onCloseClick = {}, + onPrimaryButtonClick = {}, + onSecondaryButtonClick = {}, + ) + } +} + +@Preview +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Preview(widthDp = 360, heightDp = 600) +@Preview(widthDp = 720, heightDp = 360) +@Composable +fun WPJetpackIndividualPluginOverlayScreenMultipleSitesPreview() { + AppTheme { + WPJetpackIndividualPluginOverlayScreen( + sites = listOf( + SiteWithIndividualJetpackPlugins( + name = "Site 1", + url = "site1.wordpress.com", + individualPluginNames = listOf("Jetpack Social", "Jetpack Search") + ), + SiteWithIndividualJetpackPlugins( + name = "Site 2", + url = "site2.wordpress.com", + individualPluginNames = listOf("Jetpack Boost") + ), + SiteWithIndividualJetpackPlugins( + name = "Site 3", + url = "site3.wordpress.com", + individualPluginNames = listOf("Jetpack Social") + ), + ), + onCloseClick = {}, + onPrimaryButtonClick = {}, + onSecondaryButtonClick = {}, + ) + } +} + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/GetShowJetpackFullPluginInstallOnboardingUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/GetShowJetpackFullPluginInstallOnboardingUseCase.kt similarity index 73% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/GetShowJetpackFullPluginInstallOnboardingUseCase.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/GetShowJetpackFullPluginInstallOnboardingUseCase.kt index 0bc6a3c2da67..672668c525c1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/GetShowJetpackFullPluginInstallOnboardingUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/GetShowJetpackFullPluginInstallOnboardingUseCase.kt @@ -1,9 +1,9 @@ -package org.wordpress.android.ui.jpfullplugininstall +package org.wordpress.android.ui.jetpackplugininstall.fullplugin import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.util.config.JetpackInstallFullPluginFeatureConfig -import org.wordpress.android.util.extensions.isJetpackConnectedWithoutFullPlugin +import org.wordpress.android.util.extensions.isJetpackIndividualPluginConnectedWithoutFullPlugin import javax.inject.Inject class GetShowJetpackFullPluginInstallOnboardingUseCase @Inject constructor( @@ -14,5 +14,5 @@ class GetShowJetpackFullPluginInstallOnboardingUseCase @Inject constructor( siteModel.id != 0 && jetpackInstallFullPluginFeatureConfig.isEnabled() && appPrefsWrapper.getShouldShowJetpackInstallOnboarding(siteModel.id) && - siteModel.isJetpackConnectedWithoutFullPlugin() + siteModel.isJetpackIndividualPluginConnectedWithoutFullPlugin() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/ActionEvent.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/ActionEvent.kt similarity index 80% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/ActionEvent.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/ActionEvent.kt index bd982847b1db..9a348c219a9d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/ActionEvent.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/ActionEvent.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.install +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.install import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.accounts.HelpActivity diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallActivity.kt new file mode 100644 index 000000000000..dc77f6b55782 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallActivity.kt @@ -0,0 +1,78 @@ +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.install + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.wordpress.android.ui.ActivityLauncher +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.jetpackplugininstall.install.compose.JetpackPluginInstallScreen +import org.wordpress.android.util.extensions.exhaustive +import org.wordpress.android.util.extensions.setContent + +@AndroidEntryPoint +class JetpackFullPluginInstallActivity : AppCompatActivity() { + private val viewModel: JetpackFullPluginInstallViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AppTheme { + val uiState by viewModel.uiState.collectAsState() + JetpackPluginInstallScreen( + uiState = uiState, + onDismissScreenClick = viewModel::onDismissScreenClick, + onInitialButtonClick = viewModel::onContinueClick, + onDoneButtonClick = viewModel::onDoneClick, + onRetryButtonClick = viewModel::onRetryClick, + onContactSupportButtonClick = viewModel::onContactSupportClick, + onInitialShown = viewModel::onInitialShown, + onInstallingShown = viewModel::onInstallingShown, + onErrorShown = viewModel::onErrorShown, + ) + } + } + observeActionEvents() + } + + override fun onBackPressed() { + if (!viewModel.uiState.value.showCloseButton) return + + viewModel.onBackPressed() + } + + private fun observeActionEvents() { + viewModel.actionEvents.onEach(this::handleActionEvents).launchIn(lifecycleScope) + } + + private fun handleActionEvents(actionEvent: ActionEvent) { + when (actionEvent) { + is ActionEvent.ContactSupport -> { + ActivityLauncher.viewHelp( + this, + actionEvent.origin, + actionEvent.selectedSite, + null + ) + } + + is ActionEvent.Dismiss -> { + ActivityLauncher.showMainActivity(this) + finish() + } + }.exhaustive + } + + companion object { + @JvmStatic + fun createIntent(context: Context) = + Intent(context, JetpackFullPluginInstallActivity::class.java) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallAnalyticsTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallAnalyticsTracker.kt similarity index 50% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallAnalyticsTracker.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallAnalyticsTracker.kt index 4d353f8a21c2..3dc761840760 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallAnalyticsTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallAnalyticsTracker.kt @@ -1,40 +1,46 @@ -package org.wordpress.android.ui.jpfullplugininstall.install +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.install -import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import javax.inject.Inject +private const val KEY_STATUS_PARAMETER = "status" +private const val KEY_DESCRIPTION_PARAMETER = "description" + class JetpackFullPluginInstallAnalyticsTracker @Inject constructor( private val analyticsTracker: AnalyticsTrackerWrapper ) { - fun trackScreenShown(status: Status) { + fun trackScreenShown(status: Status, description: String? = null) { analyticsTracker.track( - Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_VIEWED, - mapOf(KEY_STATUS_PARAMETER to status.trackingValue) + AnalyticsTracker.Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_VIEWED, + mapOf( + KEY_STATUS_PARAMETER to status.trackingValue, + KEY_DESCRIPTION_PARAMETER to description, + ).filterValues { it != null } ) } fun trackCancelButtonClicked(status: Status) { analyticsTracker.track( - Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_CANCEL_TAPPED, + AnalyticsTracker.Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_CANCEL_TAPPED, mapOf(KEY_STATUS_PARAMETER to status.trackingValue) ) } fun trackInstallButtonClicked() = analyticsTracker.track( - Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_INSTALL_TAPPED, emptyMap() + AnalyticsTracker.Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_INSTALL_TAPPED, emptyMap() ) fun trackRetryButtonClicked() = analyticsTracker.track( - Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_RETRY_TAPPED, emptyMap() + AnalyticsTracker.Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_RETRY_TAPPED, emptyMap() ) fun trackJetpackInstallationSuccess() = analyticsTracker.track( - Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_SUCCESS, emptyMap() + AnalyticsTracker.Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_SUCCESS, emptyMap() ) fun trackDoneButtonClicked() = analyticsTracker.track( - Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_DONE_TAPPED, emptyMap() + AnalyticsTracker.Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_DONE_TAPPED, emptyMap() ) sealed class Status(val trackingValue: String) { @@ -43,5 +49,3 @@ class JetpackFullPluginInstallAnalyticsTracker @Inject constructor( object Error : Status("error") } } - -private const val KEY_STATUS_PARAMETER = "status" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallUiStateMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallUiStateMapper.kt new file mode 100644 index 000000000000..66b2c6bb47d2 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallUiStateMapper.kt @@ -0,0 +1,26 @@ +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.install + +import org.wordpress.android.R +import org.wordpress.android.ui.jetpackplugininstall.install.UiState +import javax.inject.Inject + +class JetpackFullPluginInstallUiStateMapper @Inject constructor() { + fun mapInitial(): UiState.Initial = + UiState.Initial( + buttonText = R.string.jetpack_plugin_install_initial_button, + ) + + fun mapInstalling(): UiState.Installing = UiState.Installing + + fun mapDone(): UiState.Done = + UiState.Done( + descriptionText = R.string.jetpack_plugin_install_full_plugin_done_description, + buttonText = R.string.jetpack_plugin_install_full_plugin_done_button, + ) + + fun mapError(): UiState.Error = + UiState.Error( + retryButtonText = R.string.jetpack_plugin_install_error_button_retry, + contactSupportButtonText = R.string.jetpack_plugin_install_error_button_contact_support, + ) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallViewModel.kt similarity index 83% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallViewModel.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallViewModel.kt index b660c95f13ef..51098dc20a21 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallViewModel.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.install +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.install import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher @@ -12,13 +12,13 @@ import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.generated.PluginActionBuilder import org.wordpress.android.fluxc.generated.SiteActionBuilder import org.wordpress.android.fluxc.store.PluginStore -import org.wordpress.android.fluxc.store.PluginStore.InstallSitePluginPayload import org.wordpress.android.fluxc.store.PluginStore.OnSitePluginConfigured import org.wordpress.android.fluxc.store.PluginStore.OnSitePluginInstalled import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.accounts.HelpActivity -import org.wordpress.android.ui.jpfullplugininstall.install.JetpackFullPluginInstallAnalyticsTracker.Status +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.install.JetpackFullPluginInstallAnalyticsTracker.Status +import org.wordpress.android.ui.jetpackplugininstall.install.UiState import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.util.AppLog import org.wordpress.android.viewmodel.ScopedViewModel @@ -32,7 +32,7 @@ class JetpackFullPluginInstallViewModel @Inject constructor( private val selectedSiteRepository: SelectedSiteRepository, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, // adding pluginStore seems needed to allow the events Subscribe to work - private val pluginStore: PluginStore, + @Suppress("unused") private val pluginStore: PluginStore, private val dispatcher: Dispatcher, private val analyticsTracker: JetpackFullPluginInstallAnalyticsTracker, ) : ScopedViewModel(bgDispatcher) { @@ -42,6 +42,8 @@ class JetpackFullPluginInstallViewModel @Inject constructor( private val _actionEvents = MutableSharedFlow() val actionEvents = _actionEvents + private var trackErrorDescription: String? = null + init { dispatcher.register(this) } @@ -60,7 +62,8 @@ class JetpackFullPluginInstallViewModel @Inject constructor( } fun onErrorShown() { - analyticsTracker.trackScreenShown(Status.Error) + analyticsTracker.trackScreenShown(Status.Error, trackErrorDescription) + trackErrorDescription = null } fun onContinueClick() { @@ -110,7 +113,7 @@ class JetpackFullPluginInstallViewModel @Inject constructor( // Refresh the site regardless any event error if possible event.site?.let { dispatcher.dispatch(SiteActionBuilder.newFetchSiteAction(it)) - } ?: postUiState(uiStateMapper.mapError()) + } ?: postErrorState(event.error?.type?.name, event.error?.message) } @Suppress("unused") @@ -126,7 +129,7 @@ class JetpackFullPluginInstallViewModel @Inject constructor( // Refresh the site regardless any event error if possible event.site?.let { dispatcher.dispatch(SiteActionBuilder.newFetchSiteAction(it)) - } ?: postUiState(uiStateMapper.mapError()) + } ?: postErrorState(event.error?.type?.name, event.error?.message) } @Suppress("unused") @@ -151,15 +154,19 @@ class JetpackFullPluginInstallViewModel @Inject constructor( analyticsTracker.trackJetpackInstallationSuccess() postUiState(uiStateMapper.mapDone()) } else { - postUiState(uiStateMapper.mapError()) + postErrorState(event.error?.type?.name, event.error?.message) } } private fun installJetpackPlugin() { val selectedSite = selectedSiteRepository.getSelectedSite() selectedSite?.let { - val payload = InstallSitePluginPayload(it, "jetpack") - dispatcher.dispatch(PluginActionBuilder.newInstallJpForIndividualPluginSiteAction(payload)) + val payload = PluginStore.InstallSitePluginPayload(it, "jetpack") + dispatcher.dispatch( + PluginActionBuilder.newInstallJpForIndividualPluginSiteAction( + payload + ) + ) } } @@ -175,6 +182,11 @@ class JetpackFullPluginInstallViewModel @Inject constructor( postActionEvent(ActionEvent.Dismiss) } + private fun postErrorState(type: String?, message: String?) { + trackErrorDescription = "${type ?: "EMPTY_TYPE"}: ${message ?: "EMPTY_MESSAGE"}" + postUiState(uiStateMapper.mapError()) + } + private fun postUiState(uiState: UiState) { launch { _uiState.update { uiState } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTracker.kt similarity index 91% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTracker.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTracker.kt index db2f3ffcf4a2..8657da76750d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTracker.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.onboarding +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingDialogFragment.kt similarity index 70% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingDialogFragment.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingDialogFragment.kt index f5941750ae07..006433b8a7de 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingDialogFragment.kt @@ -1,10 +1,12 @@ -package org.wordpress.android.ui.jpfullplugininstall.onboarding +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding import android.app.Dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.ComponentDialog +import androidx.activity.addCallback import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -20,16 +22,17 @@ import org.wordpress.android.R import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.WPWebViewActivity import org.wordpress.android.ui.compose.theme.AppTheme -import org.wordpress.android.ui.jpfullplugininstall.install.JetpackFullPluginInstallActivity -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.ContactSupport -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.Dismiss -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.OpenInstallJetpackFullPlugin -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.OpenTermsAndConditions -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.UiState -import org.wordpress.android.ui.jpfullplugininstall.onboarding.compose.state.LoadedState +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.install.JetpackFullPluginInstallActivity +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.ContactSupport +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.Dismiss +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.OpenInstallJetpackFullPlugin +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.OpenTermsAndConditions +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.UiState +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.compose.state.LoadedState import org.wordpress.android.util.WPUrlUtils import org.wordpress.android.util.extensions.exhaustive +import org.wordpress.android.util.extensions.onBackPressedCompat import org.wordpress.android.util.extensions.setStatusBarAsSurfaceColor @AndroidEntryPoint @@ -59,16 +62,15 @@ class JetpackFullPluginInstallOnboardingDialogFragment : DialogFragment() { } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = - object : Dialog(requireContext(), theme) { - override fun onBackPressed() { - viewModel.onDismissScreenClick() - super.onBackPressed() - } - }.apply { + super.onCreateDialog(savedInstanceState).apply { + (this as ComponentDialog).onBackPressedDispatcher + .addCallback(this@JetpackFullPluginInstallOnboardingDialogFragment) { + viewModel.onDismissScreenClick() + onBackPressedDispatcher.onBackPressedCompat(this) + } setStatusBarAsSurfaceColor() } - @Composable private fun JetpackFullPluginInstallOnboardingScreen( viewModel: JetpackFullPluginInstallOnboardingViewModel = viewModel() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingUiStateMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingUiStateMapper.kt new file mode 100644 index 000000000000..530ace834f78 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingUiStateMapper.kt @@ -0,0 +1,19 @@ +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding + +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.UiState +import org.wordpress.android.ui.mysite.SelectedSiteRepository +import org.wordpress.android.util.SiteUtils +import org.wordpress.android.util.extensions.activeIndividualJetpackPluginNames +import javax.inject.Inject + +class JetpackFullPluginInstallOnboardingUiStateMapper @Inject constructor( + private val selectedSiteRepository: SelectedSiteRepository, +) { + fun mapLoaded(): UiState.Loaded { + val selectedSite = selectedSiteRepository.getSelectedSite() + return UiState.Loaded( + siteUrl = selectedSite?.let { SiteUtils.getHomeURLOrHostName(it) }.orEmpty(), + pluginNames = selectedSite?.activeIndividualJetpackPluginNames().orEmpty(), + ) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingViewModel.kt similarity index 81% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingViewModel.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingViewModel.kt index 0e6c3b9122e4..88a269d3a6be 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingViewModel.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.onboarding +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher @@ -9,10 +9,10 @@ import kotlinx.coroutines.flow.update import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.accounts.HelpActivity -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.ContactSupport -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.Dismiss -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.OpenInstallJetpackFullPlugin -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.OpenTermsAndConditions +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.ContactSupport +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.Dismiss +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.OpenInstallJetpackFullPlugin +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent.OpenTermsAndConditions import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.viewmodel.ScopedViewModel @@ -83,7 +83,7 @@ class JetpackFullPluginInstallOnboardingViewModel @Inject constructor( sealed class UiState { object None : UiState() data class Loaded( - val siteName: String, + val siteUrl: String, val pluginNames: List, ) : UiState() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/component/JPInstallFullPluginAnimation.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/component/JPInstallFullPluginAnimation.kt similarity index 86% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/component/JPInstallFullPluginAnimation.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/component/JPInstallFullPluginAnimation.kt index 1814f4602b46..f345a5b709ac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/component/JPInstallFullPluginAnimation.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/component/JPInstallFullPluginAnimation.kt @@ -1,8 +1,9 @@ -package org.wordpress.android.ui.jpfullplugininstall.onboarding.compose.component +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.compose.component import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Devices @@ -27,7 +28,8 @@ fun JPInstallFullPluginAnimation( val lottieComposition by rememberLottieComposition(LottieCompositionSpec.RawRes(animationRawRes)) LottieAnimation( modifier = modifier, - composition = lottieComposition + composition = lottieComposition, + alignment = Alignment.CenterStart ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/component/PluginDescription.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/component/PluginDescription.kt similarity index 53% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/component/PluginDescription.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/component/PluginDescription.kt index bffc00062ea3..17511ae2db38 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/component/PluginDescription.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/component/PluginDescription.kt @@ -1,16 +1,15 @@ -package org.wordpress.android.ui.jpfullplugininstall.onboarding.compose.component +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.compose.component import android.content.res.Configuration import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp @@ -20,27 +19,26 @@ import org.wordpress.android.ui.compose.theme.AppTheme @Composable fun PluginDescription( modifier: Modifier = Modifier, - siteName: String, + siteString: String, pluginNames: List, + useConciseText: Boolean = false, ) { Text( modifier = modifier, - text = buildPluginDescriptionText(pluginNames, siteName), + text = buildPluginDescriptionText(pluginNames, siteString, useConciseText), fontSize = 17.sp, style = TextStyle(letterSpacing = (-0.01).sp), ) } +@ReadOnlyComposable @Composable private fun buildPluginDescriptionText( pluginNames: List, - siteName: String -) = buildAnnotatedString { - val onboardingText = if (pluginNames.size > 1) { - stringResource(R.string.jetpack_full_plugin_install_onboarding_description_multiple) - } else { - stringResource(R.string.jetpack_full_plugin_install_onboarding_description_single) - } + siteString: String, + useConciseText: Boolean, +): AnnotatedString { + val onboardingText = getOnboardingTextTemplate(pluginNames.size, useConciseText) val pluginText = if (pluginNames.size > 1) { stringResource(R.string.jetpack_full_plugin_install_onboarding_description_multiple_plugins) } else { @@ -53,27 +51,37 @@ private fun buildPluginDescriptionText( stringResource(R.string.jetpack_full_plugin_install_onboarding_description_full_jetpack_plugin) val text = String.format( onboardingText, - siteName, + siteString, pluginText, fullJpPluginText, ) val indexTextList = mutableListOf() indexTextList.add(PluginDescriptionTextPart(text.indexOf(pluginText), pluginText, true)) - indexTextList.add(PluginDescriptionTextPart(text.indexOf(siteName), siteName, true)) + indexTextList.add(PluginDescriptionTextPart(text.indexOf(siteString), siteString, !useConciseText)) indexTextList.add(PluginDescriptionTextPart(text.indexOf(fullJpPluginText), fullJpPluginText, true)) - text.split(pluginText, siteName, fullJpPluginText) - .filter { it.isNotEmpty() } - .forEach { - indexTextList.add(PluginDescriptionTextPart(text.indexOf(it), it, false)) - } - indexTextList.sortedBy { it.index }.forEach { - if (it.isBold) appendBold(it.text) else append(it.text) - } + + return AnnotatedString.Builder(text).apply { + indexTextList + .filter { it.isBold } + .forEach { addStyle(SpanStyle(fontWeight = FontWeight.Bold), it.index, it.index + it.text.length) } + }.toAnnotatedString() } -private fun AnnotatedString.Builder.appendBold(text: String) { - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(text) +@ReadOnlyComposable +@Composable +private fun getOnboardingTextTemplate(pluginCount: Int, useConciseText: Boolean): String { + return if (useConciseText) { + if (pluginCount > 1) { + stringResource(R.string.jetpack_full_plugin_install_concise_description_multiple) + } else { + stringResource(R.string.jetpack_full_plugin_install_concise_description_single) + } + } else { + if (pluginCount > 1) { + stringResource(R.string.jetpack_full_plugin_install_onboarding_description_multiple) + } else { + stringResource(R.string.jetpack_full_plugin_install_onboarding_description_single) + } } } @@ -90,7 +98,7 @@ private data class PluginDescriptionTextPart( private fun PreviewPluginDescriptionOnePlugin() { AppTheme { PluginDescription( - siteName = "wordpress.com", + siteString = "wordpress.com", pluginNames = listOf("Jetpack Search"), ) } @@ -103,8 +111,36 @@ private fun PreviewPluginDescriptionOnePlugin() { private fun PreviewPluginDescriptionMultiplePlugins() { AppTheme { PluginDescription( - siteName = "wordpress.com", + siteString = "wordpress.com", + pluginNames = listOf("Jetpack Search", "Jetpack Protect"), + ) + } +} + +@Preview(showBackground = true, device = Devices.PIXEL_4_XL) +@Preview(showBackground = true, device = Devices.PIXEL_4_XL, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, device = Devices.PIXEL_4_XL, fontScale = 2f) +@Composable +private fun PreviewPluginDescriptionOnePluginConcise() { + AppTheme { + PluginDescription( + siteString = "This site", + pluginNames = listOf("Jetpack Search"), + useConciseText = true, + ) + } +} + +@Preview(showBackground = true, device = Devices.PIXEL_4_XL) +@Preview(showBackground = true, device = Devices.PIXEL_4_XL, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, device = Devices.PIXEL_4_XL, fontScale = 2f) +@Composable +private fun PreviewPluginDescriptionMultiplePluginsConcise() { + AppTheme { + PluginDescription( + siteString = "This site", pluginNames = listOf("Jetpack Search", "Jetpack Protect"), + useConciseText = true, ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/component/TermsAndConditions.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/component/TermsAndConditions.kt similarity index 96% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/component/TermsAndConditions.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/component/TermsAndConditions.kt index 87746d54e801..ad0720082101 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/component/TermsAndConditions.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/component/TermsAndConditions.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.onboarding.compose.component +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.compose.component import android.content.res.Configuration import androidx.compose.foundation.clickable diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/state/LoadedState.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/state/LoadedState.kt similarity index 87% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/state/LoadedState.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/state/LoadedState.kt index cf146e3ee1c4..fb4ef2b558e7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/compose/state/LoadedState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/compose/state/LoadedState.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.onboarding.compose.state +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.compose.state import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement @@ -27,10 +27,10 @@ import org.wordpress.android.ui.compose.components.buttons.SecondaryButton import org.wordpress.android.ui.compose.components.text.Title import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.UiState -import org.wordpress.android.ui.jpfullplugininstall.onboarding.compose.component.JPInstallFullPluginAnimation -import org.wordpress.android.ui.jpfullplugininstall.onboarding.compose.component.PluginDescription -import org.wordpress.android.ui.jpfullplugininstall.onboarding.compose.component.TermsAndConditions +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.UiState +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.compose.component.JPInstallFullPluginAnimation +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.compose.component.PluginDescription +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.compose.component.TermsAndConditions @Composable fun LoadedState( @@ -80,7 +80,7 @@ fun LoadedState( modifier = Modifier .padding(horizontal = 30.dp) .padding(top = 20.dp), - siteName = siteName, + siteString = siteUrl, pluginNames = pluginNames, ) Spacer(Modifier.weight(1f)) @@ -115,7 +115,7 @@ fun LoadedState( private fun PreviewLoadedState() { AppTheme { val uiState = UiState.Loaded( - siteName = "wordpress.com", + siteUrl = "wordpress.com", pluginNames = listOf("Jetpack Search"), ) LoadedState(uiState, {}, {}, {}, {}) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/UiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/UiState.kt similarity index 53% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/UiState.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/UiState.kt index 9852ae23c283..c5c33a223115 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/UiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/UiState.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.install +package org.wordpress.android.ui.jetpackplugininstall.install import androidx.annotation.DrawableRes import androidx.annotation.StringRes @@ -17,28 +17,29 @@ sealed class UiState( ) : UiState( toolbarTitle = R.string.jetpack, image = R.drawable.ic_jetpack_logo_green_24dp, - imageContentDescription = R.string.jetpack_full_plugin_install_jp_logo_content_description, - title = R.string.jetpack_full_plugin_install_initial_title, - description = R.string.jetpack_full_plugin_install_initial_description, + imageContentDescription = R.string.jetpack_plugin_install_jp_logo_content_description, + title = R.string.jetpack_plugin_install_initial_title, + description = R.string.jetpack_plugin_install_initial_description, ) object Installing : UiState( toolbarTitle = R.string.jetpack, image = R.drawable.ic_jetpack_logo_green_24dp, - imageContentDescription = R.string.jetpack_full_plugin_install_jp_logo_content_description, - title = R.string.jetpack_full_plugin_install_initial_title, - description = R.string.jetpack_full_plugin_install_initial_description, + imageContentDescription = R.string.jetpack_plugin_install_jp_logo_content_description, + title = R.string.jetpack_plugin_install_installing_title, + description = R.string.jetpack_plugin_install_installing_description, showCloseButton = false, ) data class Done( + @StringRes val descriptionText: Int, @StringRes val buttonText: Int, ) : UiState( toolbarTitle = R.string.jetpack, image = R.drawable.ic_jetpack_logo_green_24dp, - imageContentDescription = R.string.jetpack_full_plugin_install_jp_logo_content_description, - title = R.string.jetpack_full_plugin_install_done_title, - description = R.string.jetpack_full_plugin_install_done_description, + imageContentDescription = R.string.jetpack_plugin_install_jp_logo_content_description, + title = R.string.jetpack_plugin_install_done_title, + description = descriptionText, showCloseButton = false, ) @@ -48,8 +49,8 @@ sealed class UiState( ) : UiState( toolbarTitle = R.string.jetpack, image = R.drawable.ic_warning, - imageContentDescription = R.string.jetpack_full_plugin_install_error_image_content_description, - title = R.string.jetpack_full_plugin_install_error_title, - description = R.string.jetpack_full_plugin_install_error_description, + imageContentDescription = R.string.jetpack_plugin_install_error_image_content_description, + title = R.string.jetpack_plugin_install_error_title, + description = R.string.jetpack_plugin_install_error_description, ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/JetpackPluginInstallScreen.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/JetpackPluginInstallScreen.kt new file mode 100644 index 000000000000..334c51132fbc --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/JetpackPluginInstallScreen.kt @@ -0,0 +1,69 @@ +package org.wordpress.android.ui.jetpackplugininstall.install.compose + +import androidx.compose.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import org.wordpress.android.ui.compose.components.MainTopAppBar +import org.wordpress.android.ui.compose.components.NavigationIcons +import org.wordpress.android.ui.jetpackplugininstall.install.UiState +import org.wordpress.android.ui.jetpackplugininstall.install.compose.state.DoneState +import org.wordpress.android.ui.jetpackplugininstall.install.compose.state.ErrorState +import org.wordpress.android.ui.jetpackplugininstall.install.compose.state.InitialState +import org.wordpress.android.ui.jetpackplugininstall.install.compose.state.InstallingState +@Composable +fun JetpackPluginInstallScreen( + uiState: UiState, + onDismissScreenClick: () -> Unit, + onInitialButtonClick: () -> Unit, + onDoneButtonClick: () -> Unit, + onRetryButtonClick: () -> Unit, + onContactSupportButtonClick: () -> Unit, + onInitialShown: () -> Unit = {}, + onInstallingShown: () -> Unit = {}, + onErrorShown: () -> Unit = {}, +) { + uiState.apply { + Scaffold( + topBar = { + MainTopAppBar( + title = stringResource(toolbarTitle), + navigationIcon = NavigationIcons.CloseIcon.takeIf { uiState.showCloseButton }, + onNavigationIconClick = onDismissScreenClick + ) + }, + ) { + when (this) { + is UiState.Initial -> { + InitialState( + uiState = this, + onContinueClick = onInitialButtonClick, + ) + onInitialShown() + } + + is UiState.Installing -> { + InstallingState( + uiState = this, + ) + onInstallingShown() + } + + is UiState.Done -> { + DoneState( + uiState = this, + onDoneClick = onDoneButtonClick, + ) + } + + is UiState.Error -> { + ErrorState( + uiState = this, + onRetryClick = onRetryButtonClick, + onContactSupportClick = onContactSupportButtonClick, + ) + onErrorShown() + } + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/BaseState.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/BaseState.kt similarity index 96% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/BaseState.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/BaseState.kt index dbef731d6f10..4cd0d6b292ca 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/BaseState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/BaseState.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.install.compose.state +package org.wordpress.android.ui.jetpackplugininstall.install.compose.state import android.content.res.Configuration import androidx.compose.foundation.Image @@ -30,7 +30,7 @@ import org.wordpress.android.R import org.wordpress.android.ui.compose.components.ContentAlphaProvider import org.wordpress.android.ui.compose.theme.AppTheme import org.wordpress.android.ui.compose.unit.Margin -import org.wordpress.android.ui.jpfullplugininstall.install.UiState +import org.wordpress.android.ui.jetpackplugininstall.install.UiState import org.wordpress.android.util.extensions.fixWidows private val TitleTextStyle diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/DoneState.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/DoneState.kt similarity index 72% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/DoneState.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/DoneState.kt index 92c0299e6c7a..07a4e7e3df8e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/DoneState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/DoneState.kt @@ -1,15 +1,17 @@ -package org.wordpress.android.ui.jpfullplugininstall.install.compose.state +package org.wordpress.android.ui.jetpackplugininstall.install.compose.state import android.content.res.Configuration +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import org.wordpress.android.R import org.wordpress.android.ui.compose.components.buttons.ButtonSize import org.wordpress.android.ui.compose.components.buttons.PrimaryButton import org.wordpress.android.ui.compose.theme.AppTheme -import org.wordpress.android.ui.jpfullplugininstall.install.UiState +import org.wordpress.android.ui.jetpackplugininstall.install.UiState @Composable fun DoneState( @@ -21,7 +23,7 @@ fun DoneState( PrimaryButton( text = stringResource(buttonText), onClick = onDoneClick, - useDefaultMargins = false, + padding = PaddingValues(0.dp), buttonSize = ButtonSize.LARGE, ) } @@ -35,7 +37,8 @@ fun DoneState( private fun PreviewDoneState() { AppTheme { val uiState = UiState.Done( - buttonText = R.string.jetpack_full_plugin_install_done_button, + descriptionText = R.string.jetpack_plugin_install_full_plugin_done_description, + buttonText = R.string.jetpack_plugin_install_full_plugin_done_button, ) DoneState(uiState, {}) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/ErrorState.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/ErrorState.kt similarity index 78% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/ErrorState.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/ErrorState.kt index 79ef4b89016d..ca5e8485e8c5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/ErrorState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/ErrorState.kt @@ -1,6 +1,7 @@ -package org.wordpress.android.ui.jpfullplugininstall.install.compose.state +package org.wordpress.android.ui.jetpackplugininstall.install.compose.state import android.content.res.Configuration +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable @@ -14,7 +15,7 @@ import org.wordpress.android.ui.compose.components.buttons.ButtonSize import org.wordpress.android.ui.compose.components.buttons.PrimaryButton import org.wordpress.android.ui.compose.components.buttons.SecondaryButton import org.wordpress.android.ui.compose.theme.AppTheme -import org.wordpress.android.ui.jpfullplugininstall.install.UiState +import org.wordpress.android.ui.jetpackplugininstall.install.UiState @Composable fun ErrorState( @@ -27,14 +28,14 @@ fun ErrorState( PrimaryButton( text = stringResource(retryButtonText), onClick = onRetryClick, - useDefaultMargins = false, + padding = PaddingValues(0.dp), buttonSize = ButtonSize.LARGE, ) Spacer(modifier = Modifier.height(10.dp)) SecondaryButton( text = stringResource(contactSupportButtonText), onClick = onContactSupportClick, - useDefaultMargins = false, + padding = PaddingValues(0.dp), buttonSize = ButtonSize.LARGE, ) } @@ -48,8 +49,8 @@ fun ErrorState( private fun PreviewErrorState() { AppTheme { val uiState = UiState.Error( - retryButtonText = R.string.jetpack_full_plugin_install_error_button_retry, - contactSupportButtonText = R.string.jetpack_full_plugin_install_error_button_contact_support, + retryButtonText = R.string.jetpack_plugin_install_error_button_retry, + contactSupportButtonText = R.string.jetpack_plugin_install_error_button_contact_support, ) ErrorState(uiState, {}, {}) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/InitialState.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/InitialState.kt similarity index 77% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/InitialState.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/InitialState.kt index f73aaf32ceca..56cec7a4061c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/InitialState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/InitialState.kt @@ -1,15 +1,17 @@ -package org.wordpress.android.ui.jpfullplugininstall.install.compose.state +package org.wordpress.android.ui.jetpackplugininstall.install.compose.state import android.content.res.Configuration +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import org.wordpress.android.R import org.wordpress.android.ui.compose.components.buttons.ButtonSize import org.wordpress.android.ui.compose.components.buttons.PrimaryButton import org.wordpress.android.ui.compose.theme.AppTheme -import org.wordpress.android.ui.jpfullplugininstall.install.UiState +import org.wordpress.android.ui.jetpackplugininstall.install.UiState @Composable fun InitialState( @@ -19,9 +21,9 @@ fun InitialState( with(uiState) { BaseState(this) { PrimaryButton( - useDefaultMargins = false, text = stringResource(buttonText), onClick = onContinueClick, + padding = PaddingValues(0.dp), buttonSize = ButtonSize.LARGE, ) } @@ -35,7 +37,7 @@ fun InitialState( private fun PreviewInitialState() { AppTheme { val uiState = UiState.Initial( - buttonText = R.string.jetpack_full_plugin_install_initial_button, + buttonText = R.string.jetpack_plugin_install_initial_button, ) InitialState(uiState, {}) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/InstallingState.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/InstallingState.kt similarity index 78% rename from WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/InstallingState.kt rename to WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/InstallingState.kt index 9317bdd08269..a687921617db 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/compose/state/InstallingState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/install/compose/state/InstallingState.kt @@ -1,13 +1,15 @@ -package org.wordpress.android.ui.jpfullplugininstall.install.compose.state +package org.wordpress.android.ui.jetpackplugininstall.install.compose.state import android.content.res.Configuration +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import org.wordpress.android.ui.compose.components.buttons.ButtonSize import org.wordpress.android.ui.compose.components.buttons.PrimaryButton import org.wordpress.android.ui.compose.theme.AppTheme -import org.wordpress.android.ui.jpfullplugininstall.install.UiState +import org.wordpress.android.ui.jetpackplugininstall.install.UiState @Composable fun InstallingState( @@ -18,7 +20,7 @@ fun InstallingState( text = "", onClick = {}, isInProgress = true, - useDefaultMargins = false, + padding = PaddingValues(0.dp), buttonSize = ButtonSize.LARGE, ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/remoteplugin/JetpackRemoteInstallActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/remoteplugin/JetpackRemoteInstallActivity.kt new file mode 100644 index 000000000000..5afc83950d4e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/remoteplugin/JetpackRemoteInstallActivity.kt @@ -0,0 +1,150 @@ +package org.wordpress.android.ui.jetpackplugininstall.remoteplugin + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import androidx.activity.addCallback +import androidx.activity.viewModels +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.login.LoginMode +import org.wordpress.android.ui.ActivityLauncher +import org.wordpress.android.ui.JetpackConnectionSource +import org.wordpress.android.ui.JetpackConnectionWebViewActivity +import org.wordpress.android.ui.LocaleAwareActivity +import org.wordpress.android.ui.RequestCodes +import org.wordpress.android.ui.accounts.HelpActivity +import org.wordpress.android.ui.accounts.LoginActivity +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.jetpackplugininstall.install.UiState +import org.wordpress.android.ui.jetpackplugininstall.install.compose.JetpackPluginInstallScreen +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.CONNECT +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.CONTACT_SUPPORT +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.LOGIN +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.MANUAL_INSTALL +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat +import org.wordpress.android.util.extensions.onBackPressedCompat +import org.wordpress.android.util.extensions.setContent + +@AndroidEntryPoint +class JetpackRemoteInstallActivity : LocaleAwareActivity() { + private val viewModel: JetpackRemoteInstallViewModel by viewModels() + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AppTheme { + val uiState by viewModel.liveViewState.observeAsState() + JetpackPluginInstallScreen( + uiState = uiState ?: UiState.Initial(R.string.jetpack_plugin_install_initial_button), + onDismissScreenClick = onBackPressedDispatcher::onBackPressed, + onInitialButtonClick = viewModel::onInitialButtonClick, + onDoneButtonClick = viewModel::onDoneButtonClick, + onRetryButtonClick = viewModel::onRetryButtonClick, + onContactSupportButtonClick = viewModel::onContactSupportButtonClick, + ) + } + } + initViewModel(savedInstanceState) + onBackPressedDispatcher.addCallback(this) { + if (!viewModel.isBackButtonEnabled()) return@addCallback + + val source = requireNotNull(intent.getSerializableExtraCompat(TRACKING_SOURCE_KEY)) + viewModel.onBackPressed(source) + + onBackPressedDispatcher.onBackPressedCompat(this) + } + } + + private fun initViewModel(savedInstanceState: Bundle?) { + val site = requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE)) + val source = requireNotNull(intent.getSerializableExtraCompat(TRACKING_SOURCE_KEY)) + val retrievedState = savedInstanceState?.getSerializableCompat(VIEW_STATE) + viewModel.initialize(site, retrievedState) + + viewModel.liveActionOnResult.observe(this) { result -> + if (result != null) { + when (result.action) { + MANUAL_INSTALL -> onManualInstallResultAction(source, result) + LOGIN -> onLoginResultAction(source) + CONNECT -> onConnectResultAction(source, result) + CONTACT_SUPPORT -> onContactSupportResultAction(result) + } + } + } + } + + private fun onManualInstallResultAction( + source: JetpackConnectionSource, + result: JetpackRemoteInstallViewModel.JetpackResultActionData + ) { + JetpackConnectionWebViewActivity.startManualFlow( + this, + source, + result.site, + result.loggedIn + ) + finish() + } + + @Suppress("DEPRECATION") + private fun onLoginResultAction( + source: JetpackConnectionSource + ) { + val loginIntent = Intent(this, LoginActivity::class.java) + LoginMode.JETPACK_STATS.putInto(loginIntent) + loginIntent.putExtra(LoginActivity.ARG_JETPACK_CONNECT_SOURCE, source) + startActivityForResult(loginIntent, RequestCodes.JETPACK_LOGIN) + } + + private fun onConnectResultAction( + source: JetpackConnectionSource, + result: JetpackRemoteInstallViewModel.JetpackResultActionData + ) { + JetpackConnectionWebViewActivity.startJetpackConnectionFlow( + this, + source, + result.site, + result.loggedIn + ) + finish() + } + + private fun onContactSupportResultAction( + result: JetpackRemoteInstallViewModel.JetpackResultActionData + ) { + val origin = HelpActivity.Origin.JETPACK_REMOTE_INSTALL_PLUGIN_ERROR + ActivityLauncher.viewHelp( + this, + origin, + result.site, + null + ) + } + + @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == RequestCodes.JETPACK_LOGIN && resultCode == Activity.RESULT_OK) { + val site = intent!!.getSerializableExtra(WordPress.SITE) as SiteModel + viewModel.onLogin(site.id) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + viewModel.liveViewState.value?.let { + outState.putSerializable(VIEW_STATE, JetpackRemoteInstallViewModel.Type.fromState(it)) + } + } + + companion object { + const val TRACKING_SOURCE_KEY = "tracking_source_key" + private const val VIEW_STATE = "view_state_key" + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/remoteplugin/JetpackRemoteInstallViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/remoteplugin/JetpackRemoteInstallViewModel.kt new file mode 100644 index 000000000000..97e5b8e3d4e4 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/jetpackplugininstall/remoteplugin/JetpackRemoteInstallViewModel.kt @@ -0,0 +1,229 @@ +package org.wordpress.android.ui.jetpackplugininstall.remoteplugin + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.Stat +import org.wordpress.android.analytics.AnalyticsTracker.Stat.INSTALL_JETPACK_REMOTE_RESTART +import org.wordpress.android.analytics.AnalyticsTracker.Stat.INSTALL_JETPACK_REMOTE_START +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.generated.JetpackActionBuilder +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.fluxc.store.JetpackStore +import org.wordpress.android.fluxc.store.JetpackStore.OnJetpackInstalled +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.ui.JetpackConnectionSource +import org.wordpress.android.ui.JetpackConnectionUtils +import org.wordpress.android.ui.jetpackplugininstall.install.UiState +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.JetpackResultActionData.Action +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.CONNECT +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.CONTACT_SUPPORT +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.LOGIN +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.JetpackResultActionData.Action.MANUAL_INSTALL +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.Type.ERROR +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.Type.INSTALLED +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.Type.INSTALLING +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.Type.START +import org.wordpress.android.viewmodel.SingleLiveEvent +import javax.inject.Inject + +private const val INVALID_CREDENTIALS = "INVALID_CREDENTIALS" +private const val FORBIDDEN = "FORBIDDEN" +private const val INSTALL_FAILURE = "INSTALL_FAILURE" +private const val INSTALL_RESPONSE_ERROR = "INSTALL_RESPONSE_ERROR" +private const val LOGIN_FAILURE = "LOGIN_FAILURE" +private const val SITE_IS_JETPACK = "SITE_IS_JETPACK" +private const val ACTIVATION_ON_INSTALL_FAILURE = "ACTIVATION_ON_INSTALL_FAILURE" +private const val ACTIVATION_RESPONSE_ERROR = "ACTIVATION_RESPONSE_ERROR" +private const val ACTIVATION_FAILURE = "ACTIVATION_FAILURE" +private val BLOCKING_FAILURES = listOf( + FORBIDDEN, + INSTALL_FAILURE, + INSTALL_RESPONSE_ERROR, + LOGIN_FAILURE, + INVALID_CREDENTIALS, + ACTIVATION_ON_INSTALL_FAILURE, + ACTIVATION_RESPONSE_ERROR, + ACTIVATION_FAILURE +) +private const val CONTEXT = "JetpackRemoteInstall" +private const val EMPTY_TYPE = "EMPTY_TYPE" +private const val EMPTY_MESSAGE = "EMPTY_MESSAGE" + +@HiltViewModel +class JetpackRemoteInstallViewModel +@Inject constructor( + private val dispatcher: Dispatcher, + private val accountStore: AccountStore, + private val siteStore: SiteStore, + // JetpackStore needs to be injected here as otherwise FluxC doesn't accept emitted events. + @Suppress("unused") private val jetpackStore: JetpackStore +) : ViewModel() { + private val mutableViewState = MutableLiveData() + val liveViewState: LiveData = mutableViewState + private val mutableActionOnResult = SingleLiveEvent() + val liveActionOnResult: LiveData = mutableActionOnResult + private lateinit var siteModel: SiteModel + + init { + dispatcher.register(this) + } + + fun initialize(site: SiteModel, type: Type?) { + siteModel = site + // Init state only if it's empty + if (mutableViewState.value == null) { + mutableViewState.value = type.toState() + if (type == INSTALLING) startRemoteInstall(site) + } + } + + override fun onCleared() { + super.onCleared() + dispatcher.unregister(this) + } + + fun onLogin(siteId: Int) { + onDoneButtonClick(siteId) + } + + private fun Type?.toState(): UiState { + return when (this) { + null, START -> UiState.Initial(R.string.jetpack_plugin_install_initial_button) + INSTALLING -> UiState.Installing + INSTALLED -> UiState.Done( + R.string.jetpack_plugin_install_remote_plugin_done_description, + R.string.jetpack_plugin_install_remote_plugin_done_button + ) + + ERROR -> UiState.Error( + R.string.jetpack_plugin_install_error_button_retry, + R.string.jetpack_plugin_install_error_button_contact_support + ) + } + } + + fun onInitialButtonClick() { + AnalyticsTracker.track(INSTALL_JETPACK_REMOTE_START) + startRemoteInstall(siteModel) + } + + fun onRetryButtonClick() { + AnalyticsTracker.track(INSTALL_JETPACK_REMOTE_RESTART) + startRemoteInstall(siteModel) + } + + fun onDoneButtonClick(siteId: Int = siteModel.id) { + val hasAccessToken = accountStore.hasAccessToken() + val action = if (hasAccessToken) { + AnalyticsTracker.track(Stat.INSTALL_JETPACK_REMOTE_CONNECT) + CONNECT + } else { + AnalyticsTracker.track(Stat.INSTALL_JETPACK_REMOTE_LOGIN) + LOGIN + } + triggerResultAction(siteId, action, hasAccessToken) + } + + fun onContactSupportButtonClick() { + mutableActionOnResult.postValue( + JetpackResultActionData( + siteModel, + accountStore.hasAccessToken(), + CONTACT_SUPPORT + ) + ) + } + + fun isBackButtonEnabled() = mutableViewState.value?.showCloseButton != false + + fun onBackPressed(source: JetpackConnectionSource) { + JetpackConnectionUtils.trackWithSource( + Stat.INSTALL_JETPACK_CANCELLED, + source + ) + } + + private fun startRemoteInstall(site: SiteModel) { + mutableViewState.postValue(INSTALLING.toState()) + dispatcher.dispatch(JetpackActionBuilder.newInstallJetpackAction(site)) + } + + private fun triggerResultAction( + siteId: Int, + action: Action, + hasAccessToken: Boolean = accountStore.hasAccessToken() + ) { + mutableActionOnResult.postValue( + JetpackResultActionData( + siteStore.getSiteByLocalId(siteId)!!, + hasAccessToken, + action + ) + ) + } + + // Network Callbacks + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onEventsUpdated(event: OnJetpackInstalled) { + val site = siteModel + if (event.isError) { + AnalyticsTracker.track( + Stat.INSTALL_JETPACK_REMOTE_FAILED, + CONTEXT, + event.error?.apiError ?: EMPTY_TYPE, + event.error?.message ?: EMPTY_MESSAGE + ) + when { + event.error?.apiError == SITE_IS_JETPACK -> { + AnalyticsTracker.track(Stat.INSTALL_JETPACK_REMOTE_COMPLETED) + mutableViewState.postValue(INSTALLED.toState()) + } + + BLOCKING_FAILURES.contains(event.error?.apiError) -> { + AnalyticsTracker.track(Stat.INSTALL_JETPACK_REMOTE_START_MANUAL_FLOW) + triggerResultAction(site.id, MANUAL_INSTALL) + } + + else -> mutableViewState.postValue(ERROR.toState()) + } + return + } + if (event.success) { + AnalyticsTracker.track(Stat.INSTALL_JETPACK_REMOTE_COMPLETED) + mutableViewState.postValue(INSTALLED.toState()) + } else { + AnalyticsTracker.track(Stat.INSTALL_JETPACK_REMOTE_FAILED) + mutableViewState.postValue(ERROR.toState()) + } + } + + data class JetpackResultActionData(val site: SiteModel, val loggedIn: Boolean, val action: Action) { + enum class Action { + LOGIN, MANUAL_INSTALL, CONNECT, CONTACT_SUPPORT + } + } + + enum class Type { + START, INSTALLING, INSTALLED, ERROR; + + companion object { + fun fromState(state: UiState): Type { + return when (state) { + is UiState.Initial -> START + is UiState.Installing -> INSTALLING + is UiState.Done -> INSTALLED + is UiState.Error -> ERROR + } + } + } + } +} + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallActivity.kt deleted file mode 100644 index 55a257ca694e..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallActivity.kt +++ /dev/null @@ -1,117 +0,0 @@ -package org.wordpress.android.ui.jpfullplugininstall.install - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.material.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.res.stringResource -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import org.wordpress.android.ui.ActivityLauncher -import org.wordpress.android.ui.compose.components.MainTopAppBar -import org.wordpress.android.ui.compose.components.NavigationIcons -import org.wordpress.android.ui.compose.theme.AppTheme -import org.wordpress.android.ui.jpfullplugininstall.install.compose.state.DoneState -import org.wordpress.android.ui.jpfullplugininstall.install.compose.state.ErrorState -import org.wordpress.android.ui.jpfullplugininstall.install.compose.state.InitialState -import org.wordpress.android.ui.jpfullplugininstall.install.compose.state.InstallingState -import org.wordpress.android.util.extensions.exhaustive -import org.wordpress.android.util.extensions.setContent - -@AndroidEntryPoint -class JetpackFullPluginInstallActivity : AppCompatActivity() { - private val viewModel: JetpackFullPluginInstallViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - AppTheme { - JetpackFullPluginInstallScreen() - } - } - observeActionEvents() - } - - override fun onBackPressed() { - viewModel.onBackPressed() - } - - @Composable - private fun JetpackFullPluginInstallScreen() { - val uiState by viewModel.uiState.collectAsState() - uiState.apply { - Scaffold( - topBar = { - MainTopAppBar( - title = stringResource(toolbarTitle), - navigationIcon = NavigationIcons.CloseIcon.takeIf { uiState.showCloseButton }, - onNavigationIconClick = viewModel::onDismissScreenClick - ) - }, - ) { - when (this) { - is UiState.Initial -> { - InitialState( - uiState = this, - onContinueClick = viewModel::onContinueClick, - ) - viewModel.onInitialShown() - } - is UiState.Installing -> { - InstallingState( - uiState = this, - ) - viewModel.onInstallingShown() - } - is UiState.Done -> { - DoneState( - uiState = this, - onDoneClick = viewModel::onDoneClick, - ) - } - is UiState.Error -> { - ErrorState( - uiState = this, - onRetryClick = viewModel::onRetryClick, - onContactSupportClick = viewModel::onContactSupportClick, - ) - viewModel.onErrorShown() - } - } - } - } - } - - private fun observeActionEvents() { - viewModel.actionEvents.onEach(this::handleActionEvents).launchIn(lifecycleScope) - } - - private fun handleActionEvents(actionEvent: ActionEvent) { - when (actionEvent) { - is ActionEvent.ContactSupport -> { - ActivityLauncher.viewHelp( - this, - actionEvent.origin, - actionEvent.selectedSite, - null - ) - } - is ActionEvent.Dismiss -> { - ActivityLauncher.showMainActivity(this) - finish() - } - }.exhaustive - } - - companion object { - @JvmStatic - fun createIntent(context: Context) = Intent(context, JetpackFullPluginInstallActivity::class.java) - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallUiStateMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallUiStateMapper.kt deleted file mode 100644 index e6a93e1d9d6e..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallUiStateMapper.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.wordpress.android.ui.jpfullplugininstall.install - -import org.wordpress.android.R -import javax.inject.Inject - -class JetpackFullPluginInstallUiStateMapper @Inject constructor() { - fun mapInitial(): UiState.Initial = - UiState.Initial( - buttonText = R.string.jetpack_full_plugin_install_initial_button, - ) - - fun mapInstalling(): UiState.Installing = UiState.Installing - - fun mapDone(): UiState.Done = - UiState.Done( - buttonText = R.string.jetpack_full_plugin_install_done_button, - ) - - fun mapError(): UiState.Error = - UiState.Error( - retryButtonText = R.string.jetpack_full_plugin_install_error_button_retry, - contactSupportButtonText = R.string.jetpack_full_plugin_install_error_button_contact_support, - ) -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingUiStateMapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingUiStateMapper.kt deleted file mode 100644 index fd546da0f8fb..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingUiStateMapper.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.wordpress.android.ui.jpfullplugininstall.onboarding - -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.UiState -import org.wordpress.android.ui.mysite.SelectedSiteRepository -import org.wordpress.android.util.extensions.activeJetpackConnectionPluginNames -import javax.inject.Inject - -class JetpackFullPluginInstallOnboardingUiStateMapper @Inject constructor( - private val selectedSiteRepository: SelectedSiteRepository, -) { - fun mapLoaded(): UiState.Loaded { - val selectedSite = selectedSiteRepository.getSelectedSite() - return UiState.Loaded( - siteName = selectedSite?.name.orEmpty(), - pluginNames = selectedSite?.activeJetpackConnectionPluginNames().orEmpty(), - ) - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerViewModel.kt index 5444b173a926..9a2115fda4a9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutPickerViewModel.kt @@ -15,6 +15,8 @@ import org.wordpress.android.ui.PreviewModeHandler import org.wordpress.android.ui.layoutpicker.LayoutPickerUiState.Content import org.wordpress.android.ui.layoutpicker.LayoutPickerUiState.Error import org.wordpress.android.util.NetworkUtilsWrapper +import org.wordpress.android.util.extensions.getParcelableArrayListCompat +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent @@ -307,11 +309,11 @@ abstract class LayoutPickerViewModel( fun loadSavedState(savedInstanceState: Bundle?) { if (savedInstanceState == null) return - val layouts = savedInstanceState.getParcelableArrayList(FETCHED_LAYOUTS) - val categories = savedInstanceState.getParcelableArrayList(FETCHED_CATEGORIES) + val layouts = savedInstanceState.getParcelableArrayListCompat(FETCHED_LAYOUTS) + val categories = savedInstanceState.getParcelableArrayListCompat(FETCHED_CATEGORIES) val selected = savedInstanceState.getString(SELECTED_LAYOUT) - val selectedCategories = (savedInstanceState.getSerializable(SELECTED_CATEGORIES) as? List<*>) - ?.filterIsInstance() ?: listOf() + val selectedCategories = (savedInstanceState.getSerializableCompat>(SELECTED_CATEGORIES)) + ?: listOf() val previewMode = savedInstanceState.getString(PREVIEW_MODE, MOBILE.name) resetState(selected, ArrayList(selectedCategories.toMutableList()), previewMode) if (layouts == null || categories == null || layouts.isEmpty()) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsRowViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsRowViewHolder.kt index f6a6f69cf547..f63280a6f9b3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsRowViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/layoutpicker/LayoutsRowViewHolder.kt @@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.OnScrollListener import org.wordpress.android.R import org.wordpress.android.R.dimen +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.extensions.setVisible sealed class LayoutsRowViewHolder(view: View) : RecyclerView.ViewHolder(view) @@ -100,7 +101,7 @@ class LayoutsItemViewHolder( private fun restoreScrollState(recyclerView: RecyclerView, key: String) { recyclerView.layoutManager?.apply { - val scrollState = nestedScrollStates.getParcelable(key) + val scrollState = nestedScrollStates.getParcelableCompat(key) if (scrollState != null) { onRestoreInstanceState(scrollState) } else { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/MeActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/MeActivity.kt index d2924eb91d2e..a35fcd146b78 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/MeActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/MeActivity.kt @@ -18,7 +18,7 @@ class MeActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java index 85eb8dc5003a..96f1d84f00f7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/SitePickerActivity.java @@ -43,6 +43,7 @@ import org.wordpress.android.ui.ActivityLauncher; import org.wordpress.android.ui.LocaleAwareActivity; import org.wordpress.android.ui.RequestCodes; +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginFragment; import org.wordpress.android.ui.main.SitePickerAdapter.SiteList; import org.wordpress.android.ui.main.SitePickerAdapter.SitePickerMode; import org.wordpress.android.ui.main.SitePickerAdapter.SiteRecord; @@ -58,6 +59,7 @@ import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.SiteUtils; import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.config.WPIndividualPluginOverlayFeatureConfig; import org.wordpress.android.util.helpers.Debouncer; import org.wordpress.android.util.helpers.SwipeToRefreshHelper; import org.wordpress.android.viewmodel.main.SitePickerViewModel; @@ -78,6 +80,9 @@ import static org.wordpress.android.util.WPSwipeToRefreshHelper.buildSwipeToRefreshHelper; +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint public class SitePickerActivity extends LocaleAwareActivity implements SitePickerAdapter.OnSiteClickListener, SitePickerAdapter.OnSelectedCountChangedListener, @@ -133,11 +138,11 @@ public class SitePickerActivity extends LocaleAwareActivity @Inject StatsStore mStatsStore; @Inject ViewModelProvider.Factory mViewModelFactory; @Inject BuildConfigWrapper mBuildConfigWrapper; + @Inject WPIndividualPluginOverlayFeatureConfig mWPIndividualPluginOverlayFeatureConfig; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ((WordPress) getApplication()).component().inject(this); mViewModel = new ViewModelProvider(this, mViewModelFactory).get(SitePickerViewModel.class); @@ -153,56 +158,59 @@ public void onCreate(Bundle savedInstanceState) { AnalyticsTracker.track(Stat.SITE_SWITCHER_DISPLAYED); } - if (mSitePickerMode.isReblogMode()) { - mViewModel.getOnActionTriggered().observe( - this, - unitEvent -> unitEvent.applyIfNotHandled(action -> { - switch (action.getActionType()) { - case NAVIGATE_TO_STATE: - switch (((NavigateToState) action).getNavigateState()) { - case TO_SITE_SELECTED: - mSitePickerMode = SitePickerMode.REBLOG_CONTINUE_MODE; - if (getAdapter().getIsInSearchMode()) { - disableSearchMode(); - } - - if (mReblogActionMode == null) { - startSupportActionMode(new ReblogActionModeCallback()); - } - - SiteRecord site = ((NavigateToState) action).getSiteForReblog(); - if (site != null) { - mReblogActionMode.setTitle(site.getBlogNameOrHomeURL()); - } - break; - case TO_NO_SITE_SELECTED: - mSitePickerMode = SitePickerMode.REBLOG_SELECT_MODE; - getAdapter().clearReblogSelection(); - break; - } - break; - case CONTINUE_REBLOG_TO: - SiteRecord siteToReblog = ((ContinueReblogTo) action).getSiteForReblog(); - selectSiteAndFinish(siteToReblog); - break; - case ASK_FOR_SITE_SELECTION: - if (BuildConfig.DEBUG) { - throw new IllegalStateException( - "SitePickerActivity > Selected site was null while attempting to reblog" - ); - } else { - AppLog.e( - AppLog.T.READER, - "SitePickerActivity > Selected site was null while attempting to reblog" - ); - ToastUtils.showToast(this, R.string.site_picker_ask_site_select); - } - break; - } - return null; - })); - } - + mViewModel.getOnActionTriggered().observe( + this, + unitEvent -> unitEvent.applyIfNotHandled(action -> { + switch (action.getActionType()) { + case NAVIGATE_TO_STATE: + if (!mSitePickerMode.isReblogMode()) break; + switch (((NavigateToState) action).getNavigateState()) { + case TO_SITE_SELECTED: + mSitePickerMode = SitePickerMode.REBLOG_CONTINUE_MODE; + if (getAdapter().getIsInSearchMode()) { + disableSearchMode(); + } + + if (mReblogActionMode == null) { + startSupportActionMode(new ReblogActionModeCallback()); + } + + SiteRecord site = ((NavigateToState) action).getSiteForReblog(); + if (site != null) { + mReblogActionMode.setTitle(site.getBlogNameOrHomeURL()); + } + break; + case TO_NO_SITE_SELECTED: + mSitePickerMode = SitePickerMode.REBLOG_SELECT_MODE; + getAdapter().clearReblogSelection(); + break; + } + break; + case CONTINUE_REBLOG_TO: + if (!mSitePickerMode.isReblogMode()) break; + SiteRecord siteToReblog = ((ContinueReblogTo) action).getSiteForReblog(); + selectSiteAndFinish(siteToReblog); + break; + case ASK_FOR_SITE_SELECTION: + if (!mSitePickerMode.isReblogMode()) break; + if (BuildConfig.DEBUG) { + throw new IllegalStateException( + "SitePickerActivity > Selected site was null while attempting to reblog" + ); + } else { + AppLog.e( + AppLog.T.READER, + "SitePickerActivity > Selected site was null while attempting to reblog" + ); + ToastUtils.showToast(this, R.string.site_picker_ask_site_select); + } + break; + case SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY: + WPJetpackIndividualPluginFragment.show(getSupportFragmentManager()); + break; + } + return null; + })); // If the picker is already in editing mode from previous configuration, re-enable the editing mode. if (mIsInEditMode) { startEditingVisibility(); @@ -274,7 +282,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { AnalyticsTracker.track(Stat.SITE_SWITCHER_DISMISSED); - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } else if (itemId == R.id.menu_edit) { AnalyticsTracker.track(Stat.SITE_SWITCHER_TOGGLED_EDIT_TAPPED, @@ -477,6 +485,7 @@ public void onAfterLoad() { mRecycleView.scrollToPosition(scrollPos); } } + mViewModel.onSiteListLoaded(); } }, mSitePickerMode, 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 590379164055..32ab9c66cdfe 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 @@ -16,6 +16,7 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationManagerCompat; @@ -150,6 +151,7 @@ import org.wordpress.android.util.config.MySiteDashboardTodaysStatsCardFeatureConfig; import org.wordpress.android.util.config.OpenWebLinksWithJetpackFlowFeatureConfig; import org.wordpress.android.util.config.QRCodeAuthFlowFeatureConfig; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import org.wordpress.android.util.extensions.ViewExtensionsKt; import org.wordpress.android.viewmodel.main.WPMainActivityViewModel; import org.wordpress.android.viewmodel.main.WPMainActivityViewModel.FocusPointInfo; @@ -309,6 +311,7 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); + initBackPressHandler(); mConnectionBar = findViewById(R.id.connection_bar); mConnectionBar.setOnClickListener(v -> { @@ -328,7 +331,7 @@ public void onCreate(Bundle savedInstanceState) { boolean canShowAppRatingPrompt = savedInstanceState != null; mBottomNav = findViewById(R.id.bottom_navigation); - mBottomNav.init(getSupportFragmentManager(), this); + mBottomNav.init(getSupportFragmentManager(), this, mJetpackFeatureRemovalPhaseHelper); if (savedInstanceState == null) { if (!AppPrefs.isInstallationReferrerObtained()) { @@ -366,10 +369,10 @@ public void onCreate(Bundle savedInstanceState) { } } else if (openedFromShortcut) { initSelectedSite(); - mShortcutsNavigator.showTargetScreen(getIntent().getStringExtra( - ShortcutsNavigator.ACTION_OPEN_SHORTCUT), this, getSelectedSite()); - showJetpackOverlayIfNeeded(getIntent().getStringExtra( - ShortcutsNavigator.ACTION_OPEN_SHORTCUT)); + mShortcutsNavigator.showTargetScreen(getIntent().getStringExtra( + ShortcutsNavigator.ACTION_OPEN_SHORTCUT), this, getSelectedSite()); + showJetpackOverlayIfNeeded(getIntent().getStringExtra( + ShortcutsNavigator.ACTION_OPEN_SHORTCUT)); } else if (openRequestedPage) { handleOpenPageIntent(getIntent()); } else if (isQuickStartRequestedFromPush) { @@ -490,6 +493,30 @@ && getIntent().getExtras().getBoolean(ARG_CONTINUE_JETPACK_CONNECT, false)) { displayJetpackFeatureCollectionOverlayIfNeeded(); } + private void initBackPressHandler() { + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + // let the fragment handle the back button if it implements our OnParentBackPressedListener + if (mBottomNav != null) { + Fragment fragment = mBottomNav.getActiveFragment(); + if (fragment instanceof OnActivityBackPressedListener) { + boolean handled = ((OnActivityBackPressedListener) fragment).onActivityBackPressed(); + if (handled) { + return; + } + } + } + + if (isTaskRoot() && DeviceUtils.getInstance().isChromebook(WPMainActivity.this)) { + return; // don't close app in Main Activity + } + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + } + private void showJetpackOverlayIfNeeded(String action) { if (!mJetpackFeatureRemovalOverlayUtil.shouldHideJetpackFeatures()) { return; @@ -665,7 +692,7 @@ private void initViewModel() { break; case CREATE_NEW_PAGE: if (mMLPViewModel.canShowModalLayoutPicker() - && !mJetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()) { + && mJetpackFeatureRemovalPhaseHelper.shouldShowTemplateSelectionInPages()) { mMLPViewModel.createPageFlowTriggered(); } else { handleNewPageAction("", "", null, @@ -863,6 +890,10 @@ private void handleOpenPageIntent(Intent intent) { showJetpackFeatureOverlayAccessedInCorrectly(trackingProperties); break; } + if (mJetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { + ActivityLauncher.showJetpackStaticPoster(this); + break; + } if (intent.hasExtra(ARG_STATS_TIMEFRAME)) { ActivityLauncher.viewBlogStatsForTimeframe(this, getSelectedSite(), (StatsTimeframe) intent.getSerializableExtra(ARG_STATS_TIMEFRAME)); @@ -1107,25 +1138,6 @@ private void announceTitleForAccessibility(PageType pageType) { getWindow().getDecorView().announceForAccessibility(mBottomNav.getContentDescriptionForPageType(pageType)); } - @Override - public void onBackPressed() { - // let the fragment handle the back button if it implements our OnParentBackPressedListener - if (mBottomNav != null) { - Fragment fragment = mBottomNav.getActiveFragment(); - if (fragment instanceof OnActivityBackPressedListener) { - boolean handled = ((OnActivityBackPressedListener) fragment).onActivityBackPressed(); - if (handled) { - return; - } - } - } - - if (isTaskRoot() && DeviceUtils.getInstance().isChromebook(this)) { - return; // don't close app in Main Activity - } - super.onBackPressed(); - } - @Override public void onRequestShowBottomNavigation() { showBottomNav(true); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt index ea57c7111827..d2fbf6af2685 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainNavigationView.kt @@ -21,10 +21,13 @@ import com.google.android.material.navigation.NavigationBarView.OnItemReselected import com.google.android.material.navigation.NavigationBarView.OnItemSelectedListener import org.wordpress.android.BuildConfig import org.wordpress.android.R +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper import org.wordpress.android.ui.main.WPMainActivity.OnScrollToTopListener import org.wordpress.android.ui.main.WPMainNavigationView.PageType.MY_SITE import org.wordpress.android.ui.main.WPMainNavigationView.PageType.NOTIFS import org.wordpress.android.ui.main.WPMainNavigationView.PageType.READER +import org.wordpress.android.ui.main.jetpack.staticposter.JetpackStaticPosterFragment +import org.wordpress.android.ui.main.jetpack.staticposter.UiData import org.wordpress.android.ui.mysite.MySiteFragment import org.wordpress.android.ui.notifications.NotificationsListFragment import org.wordpress.android.ui.posts.PostUtils.EntryPoint @@ -49,6 +52,7 @@ class WPMainNavigationView @JvmOverloads constructor( private var fragmentManager: FragmentManager? = null private lateinit var pageListener: OnPageListener private var prevPosition = -1 + private lateinit var jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper private val unselectedButtonAlpha = ResourcesCompat.getFloat( resources, R.dimen.material_emphasis_disabled @@ -70,9 +74,10 @@ class WPMainNavigationView @JvmOverloads constructor( fun onNewPostButtonClicked(promptId: Int, origin: EntryPoint) } - fun init(fm: FragmentManager, listener: OnPageListener) { + fun init(fm: FragmentManager, listener: OnPageListener, helper: JetpackFeatureRemovalPhaseHelper) { fragmentManager = fm pageListener = listener + jetpackFeatureRemovalPhaseHelper = helper navAdapter = NavAdapter() assignNavigationListeners(true) @@ -102,7 +107,12 @@ class WPMainNavigationView @JvmOverloads constructor( itemView.addView(customView) } - currentPosition = AppPrefs.getMainPageIndex(numPages() - 1) + currentPosition = getMainPageIndex() + } + + private fun getMainPageIndex(): Int { + return if (jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()) 0 + else AppPrefs.getMainPageIndex(numPages() - 1) } private fun hideReaderTab() { @@ -164,7 +174,9 @@ class WPMainNavigationView @JvmOverloads constructor( setImageViewSelected(position, true) - AppPrefs.setMainPageIndex(position) + if(jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()) + AppPrefs.setMainPageIndex(0) + else AppPrefs.setMainPageIndex(position) // temporarily disable the nav listeners so they don't fire when we change the selected page assignNavigationListeners(false) @@ -291,11 +303,16 @@ class WPMainNavigationView @JvmOverloads constructor( } private inner class NavAdapter { - private fun createFragment(pageType: PageType): Fragment { + private fun createFragment(pageType: PageType, helper: JetpackFeatureRemovalPhaseHelper): Fragment { + val shouldUseStaticPostersFragment = helper.shouldShowStaticPage() val fragment = when (pageType) { MY_SITE -> MySiteFragment.newInstance() - READER -> ReaderFragment() - NOTIFS -> NotificationsListFragment.newInstance() + READER -> if (shouldUseStaticPostersFragment) + JetpackStaticPosterFragment.newInstance(UiData.READER) + else ReaderFragment() + NOTIFS -> if (shouldUseStaticPostersFragment) + JetpackStaticPosterFragment.newInstance(UiData.NOTIFICATIONS) + else NotificationsListFragment.newInstance() } fragmentManager?.beginTransaction() ?.add(R.id.fragment_container, fragment, getTagForPageType(pageType)) @@ -303,9 +320,35 @@ class WPMainNavigationView @JvmOverloads constructor( return fragment } + internal fun getFragment(position: Int): Fragment? { return pages().getOrNull(position)?.let { pageType -> - fragmentManager?.findFragmentByTag(getTagForPageType(pageType)) ?: createFragment(pageType) + val currentFragment = fragmentManager?.findFragmentByTag(getTagForPageType(pageType)) + return currentFragment?.let { + when (it) { + is ReaderFragment, is NotificationsListFragment -> checkAndCreateForStaticPage(it, pageType) + is JetpackStaticPosterFragment -> checkAndCreateForNonStaticPage(it, pageType) + else -> it + } + } ?: createFragment(pageType, jetpackFeatureRemovalPhaseHelper) + } + } + + private fun checkAndCreateForStaticPage(fragment: Fragment, pageType: PageType): Fragment { + return if (jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { + fragmentManager?.beginTransaction()?.remove(fragment)?.commitNow() + createFragment(pageType, jetpackFeatureRemovalPhaseHelper) + } else { + fragment + } + } + + private fun checkAndCreateForNonStaticPage(fragment: Fragment, pageType: PageType): Fragment { + return if (!jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { + fragmentManager?.beginTransaction()?.remove(fragment)?.commitNow() + createFragment(pageType, jetpackFeatureRemovalPhaseHelper) + } else { + fragment } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationActivity.kt index 109109bac25a..f8604ff640bb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationActivity.kt @@ -8,6 +8,7 @@ import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.databinding.ActivityJetpackMigrationBinding import org.wordpress.android.ui.utils.PreMigrationDeepLinkData +import org.wordpress.android.util.extensions.getParcelableExtraCompat @AndroidEntryPoint class JetpackMigrationActivity : AppCompatActivity() { @@ -18,7 +19,7 @@ class JetpackMigrationActivity : AppCompatActivity() { setContentView(root) if (savedInstanceState == null) { val showDeleteWpState = intent.getBooleanExtra(KEY_SHOW_DELETE_WP_STATE, false) - val deepLinkData = intent.getParcelableExtra(KEY_DEEP_LINK_DATA) + val deepLinkData = intent.getParcelableExtraCompat(KEY_DEEP_LINK_DATA) val fragment = JetpackMigrationFragment.newInstance(showDeleteWpState, deepLinkData) supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, fragment) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationFragment.kt index 607636233623..91f9e78ca712 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/migration/JetpackMigrationFragment.kt @@ -4,7 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable @@ -44,6 +44,7 @@ import org.wordpress.android.ui.utils.PreMigrationDeepLinkData import org.wordpress.android.util.AppThemeUtils import org.wordpress.android.util.LocaleManager import org.wordpress.android.util.UriWrapper +import org.wordpress.android.util.extensions.getParcelableCompat import javax.inject.Inject @AndroidEntryPoint @@ -77,7 +78,7 @@ class JetpackMigrationFragment : Fragment() { observeViewModelEvents() observeRefreshAppThemeEvents() val showDeleteWpState = arguments?.getBoolean(KEY_SHOW_DELETE_WP_STATE, false) ?: false - val deepLinkData = arguments?.getParcelable(KEY_DEEP_LINK_DATA) + val deepLinkData = arguments?.getParcelableCompat(KEY_DEEP_LINK_DATA) initBackPressHandler(showDeleteWpState) viewModel.start( showDeleteWpState, @@ -127,15 +128,7 @@ class JetpackMigrationFragment : Fragment() { private fun initBackPressHandler(showDeleteWpState: Boolean) { if (showDeleteWpState) return - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback( - true - ) { - override fun handleOnBackPressed() { - viewModel.logoutAndFallbackToLogin() - } - }) + requireActivity().onBackPressedDispatcher.addCallback(this) { viewModel.logoutAndFallbackToLogin() } } companion object { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/staticposter/JetpackStaticPosterFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/staticposter/JetpackStaticPosterFragment.kt new file mode 100644 index 000000000000..41500c8366b4 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/staticposter/JetpackStaticPosterFragment.kt @@ -0,0 +1,80 @@ +package org.wordpress.android.ui.main.jetpack.staticposter + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.ui.ActivityLauncherWrapper +import org.wordpress.android.ui.ActivityLauncherWrapper.Companion.JETPACK_PACKAGE_NAME +import org.wordpress.android.ui.WPWebViewActivity +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.main.jetpack.staticposter.compose.JetpackStaticPoster +import org.wordpress.android.util.UrlUtils +import org.wordpress.android.util.extensions.getParcelableCompat +import javax.inject.Inject + +@AndroidEntryPoint +class JetpackStaticPosterFragment : Fragment() { + private val viewModel: JetpackStaticPosterViewModel by viewModels() + + @Inject + lateinit var activityLauncher: ActivityLauncherWrapper + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = ComposeView(requireContext()).apply { + setContent { + AppTheme { + val uiState by viewModel.uiState.collectAsState() + when (val state = uiState) { + is UiState.Content -> JetpackStaticPoster( + uiState = state, + onPrimaryClick = viewModel::onPrimaryClick, + onSecondaryClick = viewModel::onSecondaryClick, + onBackClick = requireActivity().onBackPressedDispatcher::onBackPressed, + ) + is UiState.Loading -> CircularProgressIndicator() + } + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + observeEvents() + viewModel.start(requireNotNull(requireArguments().getParcelableCompat(ARG_PARCEL))) + } + + private fun observeEvents() { + viewModel.events.observe(viewLifecycleOwner) { event -> + when (event) { + is Event.PrimaryButtonClick -> activityLauncher.openPlayStoreLink( + requireActivity(), + JETPACK_PACKAGE_NAME + ) + is Event.SecondaryButtonClick -> event.url?.let { + WPWebViewActivity.openURL(requireContext(), UrlUtils.addUrlSchemeIfNeeded(it, true)) + } + } + } + } + + companion object { + private const val ARG_PARCEL = "ARG_PARCEL" + + fun newInstance(parcel: UiData) = JetpackStaticPosterFragment().apply { + arguments = Bundle().apply { + putParcelable(ARG_PARCEL, parcel) + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/staticposter/JetpackStaticPosterViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/staticposter/JetpackStaticPosterViewModel.kt new file mode 100644 index 000000000000..077c206ad820 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/staticposter/JetpackStaticPosterViewModel.kt @@ -0,0 +1,92 @@ +package org.wordpress.android.ui.main.jetpack.staticposter + +import androidx.lifecycle.LiveData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.models.JetpackPoweredScreen +import org.wordpress.android.modules.UI_THREAD +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.util.config.PhaseThreeBlogPostLinkConfig +import org.wordpress.android.viewmodel.ScopedViewModel +import org.wordpress.android.viewmodel.SingleLiveEvent +import javax.inject.Inject +import javax.inject.Named + +private const val KEY_SOURCE = "source" + +@HiltViewModel +class JetpackStaticPosterViewModel @Inject constructor( + @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, + private val phaseThreeBlogPostLinkConfig: PhaseThreeBlogPostLinkConfig, +) : ScopedViewModel(mainDispatcher) { + private var isStarted = false + + private val _uiState = MutableStateFlow(UiState.Loading) + val uiState = _uiState.asStateFlow() + + private val _events = SingleLiveEvent() + val events = _events as LiveData + + private lateinit var data: UiData + + fun start(uiData: UiData) { + if (!isStarted || uiData != data) trackStart(uiData.screen.trackingName) + if (isStarted) return else isStarted = true + data = uiData + _uiState.value = data.toContentUiState() + } + + fun onPrimaryClick() { + trackPrimaryClick() + launch { _events.value = Event.PrimaryButtonClick } + } + + fun onSecondaryClick() { + trackSecondaryClick() + launch { _events.value = Event.SecondaryButtonClick(phaseThreeBlogPostLinkConfig.getValue()) } + } + + private fun trackStart(source: String) = analyticsTrackerWrapper.track( + AnalyticsTracker.Stat.REMOVE_STATIC_POSTER_DISPLAYED, + mapOf(KEY_SOURCE to source) + ) + + private fun trackPrimaryClick() = analyticsTrackerWrapper.track( + AnalyticsTracker.Stat.REMOVE_STATIC_POSTER_GET_JETPACK_TAPPED, + mapOf(KEY_SOURCE to data.screen.trackingName) + ) + + private fun trackSecondaryClick() = analyticsTrackerWrapper.track( + AnalyticsTracker.Stat.REMOVE_STATIC_POSTER_LINK_TAPPED, + mapOf(KEY_SOURCE to data.screen.trackingName) + ) +} + +sealed class UiState { + object Loading : UiState() + data class Content( + val showTopBar: Boolean, + val featureName: UiString, + val showPluralTitle: Boolean, + val animResLtrToRtl: Pair, + ) : UiState() +} + +sealed class Event { + object PrimaryButtonClick : Event() + data class SecondaryButtonClick(val url: String?) : Event() +} + +typealias UiData = JetpackPoweredScreen.WithStaticPoster + +fun UiData.toContentUiState() = UiState.Content( + showTopBar = this == UiData.STATS, + featureName = screen.featureName, + showPluralTitle = screen.isPlural, + animResLtrToRtl = animResLtr to animResRtl, +) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/staticposter/compose/JetpackStaticPoster.kt b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/staticposter/compose/JetpackStaticPoster.kt new file mode 100644 index 000000000000..4fa78a33c53f --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/jetpack/staticposter/compose/JetpackStaticPoster.kt @@ -0,0 +1,151 @@ +package org.wordpress.android.ui.main.jetpack.staticposter.compose + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.rememberLottieComposition +import org.wordpress.android.R +import org.wordpress.android.ui.compose.components.MainTopAppBar +import org.wordpress.android.ui.compose.components.NavigationIcons +import org.wordpress.android.ui.compose.components.buttons.PrimaryButton +import org.wordpress.android.ui.compose.components.buttons.SecondaryButton +import org.wordpress.android.ui.compose.theme.AppColor +import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.theme.JpColorPalette +import org.wordpress.android.ui.compose.utils.uiStringText +import org.wordpress.android.ui.main.jetpack.staticposter.UiData +import org.wordpress.android.ui.main.jetpack.staticposter.UiState +import org.wordpress.android.ui.main.jetpack.staticposter.toContentUiState +import org.wordpress.android.util.extensions.isRtl + +@Composable +fun JetpackStaticPoster( + uiState: UiState.Content, + onPrimaryClick: () -> Unit = {}, + onSecondaryClick: () -> Unit = {}, + onBackClick: () -> Unit = {}, +) = with(uiState) { + Scaffold( + topBar = { + if (showTopBar) { + MainTopAppBar( + title = null, + navigationIcon = NavigationIcons.BackIcon, + onNavigationIconClick = onBackClick, + ) + } + }, + ) { + val orientation = LocalConfiguration.current.orientation + val verticalPadding = remember(orientation) { + if (orientation == Configuration.ORIENTATION_LANDSCAPE) 60.dp else 30.dp + } + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 30.dp, vertical = verticalPadding) + ) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier + .padding(bottom = 20.dp) + .fillMaxWidth(), + ) { + val animRes = if (LocalContext.current.isRtl()) animResLtrToRtl.second else animResLtrToRtl.first + val lottieComposition by rememberLottieComposition(LottieCompositionSpec.RawRes(animRes)) + LottieAnimation(lottieComposition) + Text( + stringResource( + if (showPluralTitle) + R.string.wp_jp_static_poster_title_plural else R.string.wp_jp_static_poster_title, + uiStringText(featureName) + ), + style = MaterialTheme.typography.h1.copy(fontSize = 34.sp, fontWeight = FontWeight.Bold), + ) + Text( + stringResource(R.string.wp_jp_static_poster_message), + style = MaterialTheme.typography.body1.copy(fontSize = 17.sp), + ) + Text( + stringResource(R.string.wp_jp_static_poster_footnote), + style = MaterialTheme.typography.body1.copy(colorResource(R.color.gray_50), 17.sp), + ) + } + PrimaryButton( + stringResource(R.string.wp_jp_static_poster_button_primary), + onPrimaryClick, + colors = ButtonDefaults.buttonColors( + backgroundColor = JpColorPalette().primary, + contentColor = AppColor.White, + ), + padding = PaddingValues(bottom = 15.dp), + textStyle = MaterialTheme.typography.body1.copy(fontSize = 17.sp, fontWeight = FontWeight.SemiBold), + ) + SecondaryButton( + stringResource(R.string.wp_jp_static_poster_button_secondary), + onSecondaryClick, + colors = ButtonDefaults.buttonColors( + backgroundColor = Color.Transparent, + contentColor = JpColorPalette().primary, + ), + padding = PaddingValues(0.dp), + textStyle = MaterialTheme.typography.body1.copy(fontSize = 17.sp), + ) { + Spacer(modifier = Modifier.width(10.dp)) + Icon( + painterResource(R.drawable.ic_external_v2), + stringResource(R.string.icon_desc), + tint = colorResource(R.color.jetpack_green_40) + ) + } + } + } +} + +@Preview(showBackground = true, device = Devices.PIXEL_4_XL) +@Composable +private fun PreviewJetpackStaticPoster() { + AppTheme { + Box { + val uiState = UiData.STATS.toContentUiState() + JetpackStaticPoster(uiState) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java index 80502a557e6a..a4123e8df05d 100755 --- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaBrowserActivity.java @@ -10,6 +10,7 @@ import android.content.ServiceConnection; import android.net.ConnectivityManager; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; @@ -639,7 +640,7 @@ public boolean onCreateOptionsMenu(Menu menu) { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; case R.id.menu_new_media: // Do Nothing (handled in action view click listener) @@ -1002,13 +1003,13 @@ private void doAddMediaItemClicked(@NonNull AddMenuItem item) { // stock photos item requires no permission, all other items do if (item != AddMenuItem.ITEM_CHOOSE_STOCK_MEDIA) { - String[] permissions; + String[] permissions = null; if (item == AddMenuItem.ITEM_CAPTURE_PHOTO || item == AddMenuItem.ITEM_CAPTURE_VIDEO) { - permissions = new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}; - } else { + permissions = PermissionUtils.getCameraAndStoragePermissions(); + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; } - if (!PermissionUtils.checkAndRequestPermissions( + if (permissions != null && !PermissionUtils.checkAndRequestPermissions( this, WPPermissionUtils.MEDIA_BROWSER_PERMISSION_REQUEST_CODE, permissions)) { return; } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaPreviewActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaPreviewActivity.java index 2cb4b8a8fe39..5f3a70a4f110 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaPreviewActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaPreviewActivity.java @@ -222,7 +222,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java index 8836a8d76e66..350b96532dd8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/media/MediaSettingsActivity.java @@ -14,6 +14,7 @@ import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; @@ -34,6 +35,7 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.DrawableRes; import androidx.annotation.IntegerRes; import androidx.annotation.NonNull; @@ -74,7 +76,6 @@ import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.ColorUtils; -import org.wordpress.android.util.extensions.ContextExtensionsKt; import org.wordpress.android.util.DateTimeUtils; import org.wordpress.android.util.DisplayUtils; import org.wordpress.android.util.EditTextUtils; @@ -86,9 +87,11 @@ import org.wordpress.android.util.SiteUtils; import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.ToastUtils; -import org.wordpress.android.util.extensions.ViewExtensionsKt; import org.wordpress.android.util.WPMediaUtils; import org.wordpress.android.util.WPPermissionUtils; +import org.wordpress.android.util.extensions.CompatExtensionsKt; +import org.wordpress.android.util.extensions.ContextExtensionsKt; +import org.wordpress.android.util.extensions.ViewExtensionsKt; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.util.image.ImageManager.RequestListener; import org.wordpress.android.util.image.ImageType; @@ -215,6 +218,15 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { setContentView(R.layout.media_settings_activity); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + saveChanges(); + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + setSupportActionBar(findViewById(R.id.toolbar)); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { @@ -507,12 +519,6 @@ private void showProgress(boolean show) { findViewById(R.id.progress).setVisibility(show ? View.VISIBLE : View.GONE); } - @Override - public void onBackPressed() { - saveChanges(); - super.onBackPressed(); - } - @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.media_settings, menu); @@ -545,7 +551,7 @@ public boolean onPrepareOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } else if (item.getItemId() == R.id.menu_save) { saveMediaToDevice(); @@ -982,14 +988,14 @@ private void updateImageSizeParameters() { * saves the media to the local device using the Android DownloadManager */ private void saveMediaToDevice() { - // must request permissions even though they're already defined in the manifest - String[] permissionList = { - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - }; - if (!PermissionUtils.checkAndRequestPermissions(this, WPPermissionUtils.MEDIA_PREVIEW_PERMISSION_REQUEST_CODE, - permissionList)) { - return; + // must request the permission even though it's already defined in the manifest + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + String[] permissionList = {Manifest.permission.WRITE_EXTERNAL_STORAGE}; + if (!PermissionUtils.checkAndRequestPermissions(this, + WPPermissionUtils.MEDIA_PREVIEW_PERMISSION_REQUEST_CODE, + permissionList)) { + return; + } } if (!NetworkUtils.checkConnection(this)) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaItem.kt index 2987ced93581..878b9abb1b7a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaItem.kt @@ -10,6 +10,7 @@ import org.wordpress.android.ui.mediapicker.MediaItem.IdentifierType.LOCAL_URI import org.wordpress.android.ui.mediapicker.MediaItem.IdentifierType.REMOTE_ID import org.wordpress.android.ui.mediapicker.MediaItem.IdentifierType.STOCK_MEDIA_IDENTIFIER import org.wordpress.android.util.UriWrapper +import org.wordpress.android.util.extensions.readParcelableCompat data class MediaItem( val identifier: Identifier, @@ -82,7 +83,7 @@ data class MediaItem( return when (type) { LOCAL_URI -> { LocalUri( - UriWrapper(requireNotNull(parcel.readParcelable(Uri::class.java.classLoader))), + UriWrapper(requireNotNull(parcel.readParcelableCompat(Uri::class.java.classLoader))), parcel.readInt() != 0 ) } @@ -97,7 +98,7 @@ data class MediaItem( } GIF_MEDIA_IDENTIFIER -> { GifMediaIdentifier( - UriWrapper(requireNotNull(parcel.readParcelable(Uri::class.java.classLoader))), + UriWrapper(requireNotNull(parcel.readParcelableCompat(Uri::class.java.classLoader))), parcel.readString() ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActionModeCallback.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActionModeCallback.kt index b0d467bc7972..20b76d875dcf 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActionModeCallback.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActionModeCallback.kt @@ -12,7 +12,6 @@ import androidx.lifecycle.Lifecycle.Event.ON_START import androidx.lifecycle.Lifecycle.Event.ON_STOP import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry -import androidx.lifecycle.Observer import org.wordpress.android.R import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ActionModeUiModel import org.wordpress.android.ui.utils.UiString.UiStringRes @@ -29,7 +28,7 @@ class MediaPickerActionModeCallback(private val viewModel: MediaPickerViewModel) lifecycleRegistry.handleLifecycleEvent(ON_START) val inflater = actionMode.menuInflater inflater.inflate(R.menu.photo_picker_action_mode, menu) - viewModel.uiState.observe(this, Observer { uiState -> + viewModel.uiState.observe(this) { uiState -> when (val uiModel = uiState.actionModeUiModel) { is ActionModeUiModel.Hidden -> { actionMode.finish() @@ -42,19 +41,20 @@ class MediaPickerActionModeCallback(private val viewModel: MediaPickerViewModel) if (editItemUiModel.isVisible) { editItem.isVisible = true - editItem.actionView.let { actionView -> + editItem.actionView?.let { actionView -> actionView.setOnClickListener { onActionItemClicked(actionMode, editItem) } TooltipCompat.setTooltipText(actionView, editItem.title) } - val editItemBadge = editItem.actionView.findViewById(R.id.customize_icon_count) - if (editItemUiModel.isCounterBadgeVisible) { - editItemBadge.visibility = View.VISIBLE - editItemBadge.text = editItemUiModel.counterBadgeValue.toString() - } else { - editItemBadge.visibility = View.GONE + editItem.actionView?.findViewById(R.id.customize_icon_count)?.let { editItemBadge -> + if (editItemUiModel.isCounterBadgeVisible) { + editItemBadge.visibility = View.VISIBLE + editItemBadge.text = editItemUiModel.counterBadgeValue.toString() + } else { + editItemBadge.visibility = View.GONE + } } } else { editItem.isVisible = false @@ -67,7 +67,7 @@ class MediaPickerActionModeCallback(private val viewModel: MediaPickerViewModel) } } } - }) + } return true } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt index 5e25e003eab9..8adde6b7060a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerActivity.kt @@ -50,6 +50,8 @@ import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T.MEDIA import org.wordpress.android.util.WPMediaUtils +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import java.io.File import javax.inject.Inject @@ -118,11 +120,11 @@ class MediaPickerActivity : LocaleAwareActivity(), MediaPickerListener { } if (savedInstanceState == null) { mediaPickerSetup = MediaPickerSetup.fromIntent(intent) - site = intent.getSerializableExtra(WordPress.SITE) as? SiteModel + site = intent.getSerializableExtraCompat(WordPress.SITE) localPostId = intent.getIntExtra(LOCAL_POST_ID, EMPTY_LOCAL_POST_ID) } else { mediaPickerSetup = MediaPickerSetup.fromBundle(savedInstanceState) - site = savedInstanceState.getSerializable(WordPress.SITE) as? SiteModel + site = savedInstanceState.getSerializableCompat(WordPress.SITE) localPostId = savedInstanceState.getInt(LOCAL_POST_ID, EMPTY_LOCAL_POST_ID) } var fragment = pickerFragment diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt index 949150a3d3e2..56b649baade0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerFragment.kt @@ -5,6 +5,7 @@ import android.app.Activity import android.content.Intent.ACTION_GET_CONTENT import android.content.Intent.ACTION_OPEN_DOCUMENT import android.net.Uri +import android.os.Build import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater @@ -52,8 +53,6 @@ import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.BrowseMenuUiMod import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.BrowseMenuUiModel.BrowseAction.SYSTEM_PICKER import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.BrowseMenuUiModel.BrowseAction.WP_MEDIA_LIBRARY import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.FabUiModel -import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PermissionsRequested.CAMERA -import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PermissionsRequested.STORAGE import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.PhotoListUiModel import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ProgressDialogUiModel import org.wordpress.android.ui.mediapicker.MediaPickerViewModel.ProgressDialogUiModel.Visible @@ -65,6 +64,7 @@ import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.util.AccessibilityUtils import org.wordpress.android.util.AniUtils import org.wordpress.android.util.AniUtils.Duration.MEDIUM +import org.wordpress.android.util.PermissionUtils import org.wordpress.android.util.SnackbarItem import org.wordpress.android.util.SnackbarItem.Action import org.wordpress.android.util.SnackbarItem.Info @@ -74,6 +74,9 @@ import org.wordpress.android.util.WPLinkMovementMethod import org.wordpress.android.util.WPMediaUtils import org.wordpress.android.util.WPPermissionUtils import org.wordpress.android.util.WPSwipeToRefreshHelper +import org.wordpress.android.util.extensions.getParcelableArrayListCompat +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -199,6 +202,7 @@ class MediaPickerFragment : Fragment(), MenuProvider { lateinit var uiHelpers: UiHelpers private lateinit var viewModel: MediaPickerViewModel private var binding: MediaPickerFragmentBinding? = null + private lateinit var mediaPickerSetup: MediaPickerSetup override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -223,14 +227,14 @@ class MediaPickerFragment : Fragment(), MenuProvider { super.onViewCreated(view, savedInstanceState) requireActivity().addMenuProvider(this, viewLifecycleOwner) - val mediaPickerSetup = MediaPickerSetup.fromBundle(requireArguments()) - val site = requireArguments().getSerializable(WordPress.SITE) as? SiteModel + mediaPickerSetup = MediaPickerSetup.fromBundle(requireArguments()) + val site = requireArguments().getSerializableCompat(WordPress.SITE) var selectedIds: List? = null var lastTappedIcon: MediaPickerIcon? = null if (savedInstanceState != null) { lastTappedIcon = MediaPickerIcon.fromBundle(savedInstanceState) if (savedInstanceState.containsKey(KEY_SELECTED_IDS)) { - selectedIds = savedInstanceState.getParcelableArrayList(KEY_SELECTED_IDS)?.map { it } + selectedIds = savedInstanceState.getParcelableArrayListCompat(KEY_SELECTED_IDS)?.map { it } } } @@ -239,7 +243,7 @@ class MediaPickerFragment : Fragment(), MenuProvider { NUM_COLUMNS ) - savedInstanceState?.getParcelable(KEY_LIST_STATE)?.let { + savedInstanceState?.getParcelableCompat(KEY_LIST_STATE)?.let { layoutManager.onRestoreInstanceState(it) } with(MediaPickerFragmentBinding.bind(view)) { @@ -273,12 +277,7 @@ class MediaPickerFragment : Fragment(), MenuProvider { navigateEvent(navigationEvent) } - viewModel.onPermissionsRequested.observeEvent(viewLifecycleOwner) { - when (it) { - CAMERA -> requestCameraPermission() - STORAGE -> requestStoragePermission() - } - } + viewModel.onCameraPermissionsRequested.observeEvent(viewLifecycleOwner) { requestCameraPermission() } viewModel.onSnackbarMessage.observeEvent(viewLifecycleOwner) { messageHolder -> showSnackbar(messageHolder) } @@ -416,13 +415,13 @@ class MediaPickerFragment : Fragment(), MenuProvider { private fun initializeSearchView(actionMenuItem: MenuItem) { var isExpanding = false actionMenuItem.setOnActionExpandListener(object : OnActionExpandListener { - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { viewModel.onSearchExpanded() isExpanding = true return true } - override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { viewModel.onSearchCollapsed() return true } @@ -453,7 +452,7 @@ class MediaPickerFragment : Fragment(), MenuProvider { if (uiModel.isAlwaysDenied) { WPPermissionUtils.showAppSettings(requireActivity()) } else { - requestStoragePermission() + requestMediaPermission() } } @@ -618,44 +617,69 @@ class MediaPickerFragment : Fragment(), MenuProvider { override fun onResume() { super.onResume() - checkStoragePermission() + checkMediaPermission() } fun setMediaPickerListener(listener: MediaPickerListener?) { this.listener = listener } - private val isStoragePermissionAlwaysDenied: Boolean - get() = WPPermissionUtils.isPermissionAlwaysDenied( - requireActivity(), permission.WRITE_EXTERNAL_STORAGE - ) - /* * load the photos if we have the necessary permission, otherwise show the "soft ask" view * which asks the user to allow the permission */ - private fun checkStoragePermission() { + private fun checkMediaPermission() { if (!isAdded) { return } - viewModel.checkStoragePermission(isStoragePermissionAlwaysDenied) + + // Storage permission is available only for API lower than 33 + val isStoragePermissionAlwaysDenied = WPPermissionUtils.isPermissionAlwaysDenied( + requireActivity(), + permission.WRITE_EXTERNAL_STORAGE + ) + + val isPhotosVideosPermissionAlwaysDenied = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + WPPermissionUtils.isPermissionAlwaysDenied(requireActivity(), permission.READ_MEDIA_IMAGES) || + WPPermissionUtils.isPermissionAlwaysDenied(requireActivity(), permission.READ_MEDIA_VIDEO) + } else { + // For devices lower than API 33, storage permission is the equivalent of Photos and Videos permission + isStoragePermissionAlwaysDenied + } + val isMusicAudioPermissionAlwaysDenied = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + WPPermissionUtils.isPermissionAlwaysDenied( + requireActivity(), + permission.READ_MEDIA_AUDIO + ) + } else { + // For devices lower than API 33, storage permission is the equivalent of Music and Audio permission + isStoragePermissionAlwaysDenied + } + viewModel.checkMediaPermissions(isPhotosVideosPermissionAlwaysDenied, isMusicAudioPermissionAlwaysDenied) } @Suppress("DEPRECATION") - private fun requestStoragePermission() { - val permissions = arrayOf(permission.WRITE_EXTERNAL_STORAGE, permission.READ_EXTERNAL_STORAGE) - requestPermissions( - permissions, WPPermissionUtils.PHOTO_PICKER_STORAGE_PERMISSION_REQUEST_CODE - ) + private fun requestMediaPermission() { + val permissions = arrayListOf() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (mediaPickerSetup.requiresPhotosVideosPermissions) { + permissions.add(permission.READ_MEDIA_IMAGES) + permissions.add(permission.READ_MEDIA_VIDEO) + } + if (mediaPickerSetup.requiresMusicAudioPermissions) { + permissions.add(permission.READ_MEDIA_AUDIO) + } + } else { + // READ_EXTERNAL_STORAGE is the equivalent of READ_MEDIA_IMAGES, READ_MEDIA_VIDEO and READ_MEDIA_AUDIO on + // devices lower than API 33. + permissions.add(permission.READ_EXTERNAL_STORAGE) + } + requestPermissions(permissions.toTypedArray(), WPPermissionUtils.PHOTO_PICKER_MEDIA_PERMISSION_REQUEST_CODE) } @Suppress("DEPRECATION") private fun requestCameraPermission() { - // in addition to CAMERA permission we also need a storage permission, to store media from the camera - val permissions = arrayOf( - permission.CAMERA, - permission.WRITE_EXTERNAL_STORAGE - ) + val permissions = PermissionUtils.getCameraAndStoragePermissions() requestPermissions(permissions, WPPermissionUtils.PHOTO_PICKER_CAMERA_PERMISSION_REQUEST_CODE) } @@ -670,7 +694,7 @@ class MediaPickerFragment : Fragment(), MenuProvider { requireActivity(), requestCode, permissions, grantResults, checkForAlwaysDenied ) when (requestCode) { - WPPermissionUtils.PHOTO_PICKER_STORAGE_PERMISSION_REQUEST_CODE -> checkStoragePermission() + WPPermissionUtils.PHOTO_PICKER_MEDIA_PERMISSION_REQUEST_CODE -> checkMediaPermission() WPPermissionUtils.PHOTO_PICKER_CAMERA_PERMISSION_REQUEST_CODE -> if (allGranted) { viewModel.clickOnLastTappedIcon() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt index 9a210d777428..5cb2ebf4c0ca 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerSetup.kt @@ -9,7 +9,8 @@ data class MediaPickerSetup( val primaryDataSource: DataSource, val availableDataSources: Set, val canMultiselect: Boolean, - val requiresStoragePermissions: Boolean, + val requiresPhotosVideosPermissions: Boolean, + val requiresMusicAudioPermissions: Boolean, val allowedTypes: Set, val cameraSetup: CameraSetup, val systemPickerEnabled: Boolean, @@ -31,7 +32,8 @@ data class MediaPickerSetup( bundle.putIntegerArrayList(KEY_AVAILABLE_DATA_SOURCES, ArrayList(availableDataSources.map { it.ordinal })) bundle.putIntegerArrayList(KEY_ALLOWED_TYPES, ArrayList(allowedTypes.map { it.ordinal })) bundle.putBoolean(KEY_CAN_MULTISELECT, canMultiselect) - bundle.putBoolean(KEY_REQUIRES_STORAGE_PERMISSIONS, requiresStoragePermissions) + bundle.putBoolean(KEY_REQUIRES_PHOTOS_VIDEOS_PERMISSIONS, requiresPhotosVideosPermissions) + bundle.putBoolean(KEY_REQUIRES_MUSIC_AUDIO_PERMISSIONS, requiresMusicAudioPermissions) bundle.putInt(KEY_CAMERA_SETUP, cameraSetup.ordinal) bundle.putBoolean(KEY_SYSTEM_PICKER_ENABLED, systemPickerEnabled) bundle.putBoolean(KEY_EDITING_ENABLED, editingEnabled) @@ -45,7 +47,8 @@ data class MediaPickerSetup( intent.putIntegerArrayListExtra(KEY_AVAILABLE_DATA_SOURCES, ArrayList(availableDataSources.map { it.ordinal })) intent.putIntegerArrayListExtra(KEY_ALLOWED_TYPES, ArrayList(allowedTypes.map { it.ordinal })) intent.putExtra(KEY_CAN_MULTISELECT, canMultiselect) - intent.putExtra(KEY_REQUIRES_STORAGE_PERMISSIONS, requiresStoragePermissions) + intent.putExtra(KEY_REQUIRES_PHOTOS_VIDEOS_PERMISSIONS, requiresPhotosVideosPermissions) + intent.putExtra(KEY_REQUIRES_MUSIC_AUDIO_PERMISSIONS, requiresMusicAudioPermissions) intent.putExtra(KEY_CAMERA_SETUP, cameraSetup.ordinal) intent.putExtra(KEY_SYSTEM_PICKER_ENABLED, systemPickerEnabled) intent.putExtra(KEY_EDITING_ENABLED, editingEnabled) @@ -58,7 +61,8 @@ data class MediaPickerSetup( private const val KEY_PRIMARY_DATA_SOURCE = "key_primary_data_source" private const val KEY_AVAILABLE_DATA_SOURCES = "key_available_data_sources" private const val KEY_CAN_MULTISELECT = "key_can_multiselect" - private const val KEY_REQUIRES_STORAGE_PERMISSIONS = "key_requires_storage_permissions" + private const val KEY_REQUIRES_PHOTOS_VIDEOS_PERMISSIONS = "key_requires_photos_videos_permissions" + private const val KEY_REQUIRES_MUSIC_AUDIO_PERMISSIONS = "key_requires_music_audio_permissions" private const val KEY_ALLOWED_TYPES = "key_allowed_types" private const val KEY_CAMERA_SETUP = "key_camera_setup" private const val KEY_SYSTEM_PICKER_ENABLED = "key_system_picker_enabled" @@ -77,7 +81,8 @@ data class MediaPickerSetup( }.toSet() val multipleSelectionAllowed = bundle.getBoolean(KEY_CAN_MULTISELECT) val cameraSetup = CameraSetup.values()[bundle.getInt(KEY_CAMERA_SETUP)] - val requiresStoragePermissions = bundle.getBoolean(KEY_REQUIRES_STORAGE_PERMISSIONS) + val requiresPhotosVideosPermissions = bundle.getBoolean(KEY_REQUIRES_PHOTOS_VIDEOS_PERMISSIONS) + val requiresMusicAudioPermissions = bundle.getBoolean(KEY_REQUIRES_MUSIC_AUDIO_PERMISSIONS) val systemPickerEnabled = bundle.getBoolean(KEY_SYSTEM_PICKER_ENABLED) val editingEnabled = bundle.getBoolean(KEY_EDITING_ENABLED) val queueResults = bundle.getBoolean(KEY_QUEUE_RESULTS) @@ -87,7 +92,8 @@ data class MediaPickerSetup( dataSource, availableDataSources, multipleSelectionAllowed, - requiresStoragePermissions, + requiresPhotosVideosPermissions, + requiresMusicAudioPermissions, allowedTypes, cameraSetup, systemPickerEnabled, @@ -109,7 +115,8 @@ data class MediaPickerSetup( }.toSet() val multipleSelectionAllowed = intent.getBooleanExtra(KEY_CAN_MULTISELECT, false) val cameraSetup = CameraSetup.values()[intent.getIntExtra(KEY_CAMERA_SETUP, -1)] - val requiresStoragePermissions = intent.getBooleanExtra(KEY_REQUIRES_STORAGE_PERMISSIONS, false) + val requiresPhotosVideosPermissions = intent.getBooleanExtra(KEY_REQUIRES_PHOTOS_VIDEOS_PERMISSIONS, false) + val requiresMusicAudioPermissions = intent.getBooleanExtra(KEY_REQUIRES_MUSIC_AUDIO_PERMISSIONS, false) val systemPickerEnabled = intent.getBooleanExtra(KEY_SYSTEM_PICKER_ENABLED, false) val editingEnabled = intent.getBooleanExtra(KEY_SYSTEM_PICKER_ENABLED, false) val queueResults = intent.getBooleanExtra(KEY_QUEUE_RESULTS, false) @@ -119,7 +126,8 @@ data class MediaPickerSetup( dataSource, availableDataSources, multipleSelectionAllowed, - requiresStoragePermissions, + requiresPhotosVideosPermissions, + requiresMusicAudioPermissions, allowedTypes, cameraSetup, systemPickerEnabled, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt index 243f084145ce..ae18dc31d0b4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModel.kt @@ -1,6 +1,6 @@ package org.wordpress.android.ui.mediapicker -import android.Manifest.permission +import android.os.Build import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineDispatcher @@ -64,7 +64,6 @@ import org.wordpress.android.ui.utils.UiString.UiStringText import org.wordpress.android.util.LocaleManagerWrapper import org.wordpress.android.util.MediaUtilsWrapper import org.wordpress.android.util.UriWrapper -import org.wordpress.android.util.WPPermissionUtils import org.wordpress.android.util.distinct import org.wordpress.android.util.merge import org.wordpress.android.viewmodel.Event @@ -91,7 +90,7 @@ class MediaPickerViewModel @Inject constructor( private var searchJob: Job? = null private val _domainModel = MutableLiveData() private val _selectedIds = MutableLiveData?>() - private val _onPermissionsRequested = MutableLiveData>() + private val _onCameraPermissionsRequested = MutableLiveData>() private val _softAskRequest = MutableLiveData() private val _searchExpanded = MutableLiveData() private val _showProgressDialog = MutableLiveData() @@ -101,7 +100,7 @@ class MediaPickerViewModel @Inject constructor( val onSnackbarMessage: LiveData> = _onSnackbarMessage val onNavigate = _onNavigate as LiveData> - val onPermissionsRequested: LiveData> = _onPermissionsRequested + val onCameraPermissionsRequested: LiveData> = _onCameraPermissionsRequested val uiState: LiveData = merge( _domainModel.distinct(), @@ -297,14 +296,12 @@ class MediaPickerViewModel @Inject constructor( UiStringText(String.format(resourceProvider.getString(R.string.cab_selected), numSelected)) } else -> { - val isImagePicker = mediaPickerSetup.allowedTypes.contains(IMAGE) - val isVideoPicker = mediaPickerSetup.allowedTypes.contains(VIDEO) - val isAudioPicker = mediaPickerSetup.allowedTypes.contains(AUDIO) - if (isImagePicker && isVideoPicker) { + if (mediaPickerSetup.allowedTypes.size > 1) { + // "image + video" picker, or "image + video + audio" picker UiStringRes(R.string.photo_picker_use_media) - } else if (isVideoPicker) { + } else if (isVideoPicker()) { UiStringRes(R.string.photo_picker_use_video) - } else if (isAudioPicker) { + } else if (isAudioPicker()) { UiStringRes(R.string.photo_picker_use_audio) } else { UiStringRes(R.string.photo_picker_use_photo) @@ -329,11 +326,10 @@ class MediaPickerViewModel @Inject constructor( } fun refreshData(forceReload: Boolean) { - if (!permissionsHandler.hasStoragePermission()) { - return - } - launch(bgDispatcher) { - loadActions.send(LoadAction.Refresh(forceReload)) + if (!needPhotosVideoPermission() && !needMusicAudioPermission()) { + launch(bgDispatcher) { + loadActions.send(LoadAction.Refresh(forceReload)) + } } } @@ -369,7 +365,7 @@ class MediaPickerViewModel @Inject constructor( _domainModel.value = domainModel } } - if (!mediaPickerSetup.requiresStoragePermissions || permissionsHandler.hasStoragePermission()) { + if (!needPhotosVideoPermission() && !needMusicAudioPermission()) { launch(bgDispatcher) { loadActions.send(LoadAction.Start()) } @@ -498,8 +494,8 @@ class MediaPickerViewModel @Inject constructor( private fun clickIcon(icon: MediaPickerIcon) { mediaPickerTracker.trackIconClick(icon, mediaPickerSetup) if (icon is WpStoriesCapture || icon is CapturePhoto) { - if (!permissionsHandler.hasPermissionsToAccessPhotos()) { - _onPermissionsRequested.value = Event(PermissionsRequested.CAMERA) + if (!permissionsHandler.hasPermissionsToTakePhoto()) { + _onCameraPermissionsRequested.value = Event(Unit) lastTappedIcon = icon return } @@ -566,11 +562,14 @@ class MediaPickerViewModel @Inject constructor( return IconClickEvent(action) } - fun checkStoragePermission(isAlwaysDenied: Boolean) { - if (!mediaPickerSetup.requiresStoragePermissions) { + fun checkMediaPermissions(isPhotosVideosAlwaysDenied: Boolean, isMusicAudioAlwaysDenied: Boolean) { + if (!mediaPickerSetup.requiresPhotosVideosPermissions && !mediaPickerSetup.requiresMusicAudioPermissions) { + // No permission is required, so there is no need to check permissions. return } - if (permissionsHandler.hasStoragePermission()) { + val isAlwaysDenied = (mediaPickerSetup.requiresPhotosVideosPermissions && isPhotosVideosAlwaysDenied) || + (mediaPickerSetup.requiresMusicAudioPermissions && isMusicAudioAlwaysDenied) + if (!needPhotosVideoPermission() && !needMusicAudioPermission()) { _softAskRequest.value = SoftAskRequest(show = false, isAlwaysDenied = isAlwaysDenied) if (_domainModel.value?.domainItems.isNullOrEmpty()) { refreshData(false) @@ -591,34 +590,50 @@ class MediaPickerViewModel @Inject constructor( clickIcon(icon) } + private fun getRequiredPermissionsNames(): String { + val permissionName = when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU -> R.string.permission_storage + needPhotosVideoPermission() && needMusicAudioPermission() -> R.string.permission_images_video_audio + needPhotosVideoPermission() -> R.string.permission_images + needMusicAudioPermission() -> R.string.permission_audio + else -> R.string.unknown + } + return resourceProvider.getString(permissionName) + } + + private fun isImagePicker() = mediaPickerSetup.allowedTypes.contains(IMAGE) + private fun isVideoPicker() = mediaPickerSetup.allowedTypes.contains(VIDEO) + private fun isAudioPicker() = mediaPickerSetup.allowedTypes.contains(AUDIO) + + private fun needPhotosVideoPermission() = + mediaPickerSetup.requiresPhotosVideosPermissions && !permissionsHandler.hasPhotosVideosPermission() + + private fun needMusicAudioPermission() = + mediaPickerSetup.requiresMusicAudioPermissions && !permissionsHandler.hasMusicAudioPermission() + private fun buildSoftAskView(softAskRequest: SoftAskRequest?): SoftAskViewUiModel { if (softAskRequest != null && softAskRequest.show) { mediaPickerTracker.trackShowPermissionsScreen(mediaPickerSetup, softAskRequest.isAlwaysDenied) val appName = "${resourceProvider.getString(R.string.app_name)}" val label = if (softAskRequest.isAlwaysDenied) { - val writePermission = ("${ - WPPermissionUtils.getPermissionName( - resourceProvider, - permission.WRITE_EXTERNAL_STORAGE - ) - }") - val readPermission = ("${ - WPPermissionUtils.getPermissionName( - resourceProvider, - permission.READ_EXTERNAL_STORAGE - ) - }") + val permission = ("${getRequiredPermissionsNames()}") String.format( - resourceProvider.getString(R.string.media_picker_soft_ask_permissions_denied), + resourceProvider.getString(R.string.media_picker_soft_ask_media_permissions_denied), appName, - writePermission, - readPermission + permission ) } else { - String.format( - resourceProvider.getString(R.string.photo_picker_soft_ask_label), - appName - ) + val description = when { + isImagePicker() && isVideoPicker() && isAudioPicker() -> { + R.string.photo_picker_soft_ask_photos_videos_audio_label + } + isImagePicker() && isVideoPicker() -> R.string.photo_picker_soft_ask_photos_videos_label + isImagePicker() -> R.string.photo_picker_soft_ask_photos_label + isVideoPicker() -> R.string.photo_picker_soft_ask_videos_label + isAudioPicker() -> R.string.photo_picker_soft_ask_audios_label + else -> R.string.unknown + } + String.format(resourceProvider.getString(description), appName) } val allowId = if (softAskRequest.isAlwaysDenied) { R.string.button_edit_permissions @@ -733,10 +748,6 @@ class MediaPickerViewModel @Inject constructor( } } - enum class PermissionsRequested { - CAMERA, STORAGE - } - data class SoftAskRequest(val show: Boolean, val isAlwaysDenied: Boolean) data class EditActionUiModel( 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 0599b8702842..34c4c6a613fc 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 @@ -81,8 +81,7 @@ class MySiteAdapter( MySiteCardAndItem.Type.JETPACK_FEATURE_CARD.ordinal -> JetpackFeatureCardViewHolder(parent, uiHelpers) MySiteCardAndItem.Type.JETPACK_SWITCH_CARD.ordinal -> SwitchToJetpackMenuCardViewHolder(parent) MySiteCardAndItem.Type.JETPACK_INSTALL_FULL_PLUGIN_CARD.ordinal -> JetpackInstallFullPluginCardViewHolder( - parent, - uiHelpers + parent ) else -> throw IllegalArgumentException("Unexpected view type") } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt index 7c3c67bd8ba3..913d93b92861 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItem.kt @@ -45,7 +45,7 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte SINGLE_ACTION_CARD, JETPACK_FEATURE_CARD, JETPACK_SWITCH_CARD, - JETPACK_INSTALL_FULL_PLUGIN_CARD + JETPACK_INSTALL_FULL_PLUGIN_CARD, } enum class DashboardCardType { @@ -57,7 +57,11 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte POST_CARD_WITHOUT_POST_ITEMS, POST_CARD_WITH_POST_ITEMS, BLOGGING_PROMPT_CARD, - PROMOTE_WITH_BLAZE_CARD + PROMOTE_WITH_BLAZE_CARD, + DASHBOARD_DOMAIN_CARD, + PAGES_CARD_ERROR, + PAGES_CARD, + ACTIVITY_CARD, } data class SiteInfoHeaderCard( @@ -199,6 +203,35 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte } } + sealed class PagesCard( + override val dashboardCardType: DashboardCardType, + ): DashboardCard(dashboardCardType) { + data class Error( + override val title: UiString + ) : PagesCard(dashboardCardType = DashboardCardType.PAGES_CARD_ERROR), ErrorWithinCard + + data class PagesCardWithData( + val title: UiString, + val pages: List, + val footerLink: CreatNewPageItem + ) : PagesCard(dashboardCardType = DashboardCardType.PAGES_CARD) { + data class PageContentItem( + val title: UiString, + @DrawableRes val statusIcon: Int, + val status: UiString, + val lastEditedOrScheduledTime: UiString, + val onCardClick: () -> Unit + ) + + data class CreatNewPageItem( + val label: UiString, + val description: UiString? = null, + @DrawableRes val imageRes: Int? = null, + val onClick: () -> Unit + ) + } + } + sealed class PostCard( override val dashboardCardType: DashboardCardType, open val footerLink: FooterLink? = null @@ -243,6 +276,34 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte ) } + sealed class ActivityCard( + override val dashboardCardType: DashboardCardType, + open val footerLink: FooterLink? = null + ) : DashboardCard(dashboardCardType) { + data class ActivityCardWithItems( + val title: UiString, + val activityItems: List, + override val footerLink: FooterLink + ) : ActivityCard( + dashboardCardType = DashboardCardType.ACTIVITY_CARD, + footerLink = footerLink + ) { + data class ActivityItem( + val label: UiString, + val subLabel: String?, + val displayDate: String, + @DrawableRes val icon: Int, + @DrawableRes val iconBackgroundColor: Int, + val onClick: ListItemInteraction + ) + } + + data class FooterLink( + val label: UiString, + val onClick: () -> Unit + ) + } + sealed class BloggingPromptCard( override val dashboardCardType: DashboardCardType ) : DashboardCard(dashboardCardType) { @@ -271,6 +332,14 @@ sealed class MySiteCardAndItem(open val type: Type, open val activeQuickStartIte val onHideMenuItemClick: ListItemInteraction, val onMoreMenuClick: ListItemInteraction, ): DashboardCard(dashboardCardType = DashboardCardType.PROMOTE_WITH_BLAZE_CARD) + + data class DashboardDomainCard( + val title: UiString?, + val subtitle: UiString?, + val onClick: ListItemInteraction, + val onHideMenuItemClick: ListItemInteraction, + val onMoreMenuClick: ListItemInteraction, + ): DashboardCard(dashboardCardType = DashboardCardType.DASHBOARD_DOMAIN_CARD) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItemBuilderParams.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItemBuilderParams.kt index 168245dbac19..ca094dfe8fa6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItemBuilderParams.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteCardAndItemBuilderParams.kt @@ -4,10 +4,13 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.bloggingprompts.BloggingPromptModel +import org.wordpress.android.fluxc.model.dashboard.CardModel +import org.wordpress.android.fluxc.model.dashboard.CardModel.PagesCardModel import org.wordpress.android.fluxc.model.dashboard.CardModel.PostsCardModel import org.wordpress.android.fluxc.model.dashboard.CardModel.TodaysStatsCardModel import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType +import org.wordpress.android.ui.mysite.cards.dashboard.pages.PagesCardContentType import org.wordpress.android.ui.mysite.cards.dashboard.posts.PostCardType import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository.QuickStartCategory import org.wordpress.android.ui.mysite.items.listitem.ListItemAction @@ -62,7 +65,10 @@ sealed class MySiteCardAndItemBuilderParams { val todaysStatsCardBuilderParams: TodaysStatsCardBuilderParams, val postCardBuilderParams: PostCardBuilderParams, val bloggingPromptCardBuilderParams: BloggingPromptCardBuilderParams, - val promoteWithBlazeCardBuilderParams: PromoteWithBlazeCardBuilderParams + val promoteWithBlazeCardBuilderParams: PromoteWithBlazeCardBuilderParams, + val dashboardCardDomainBuilderParams: DashboardCardDomainBuilderParams, + val pagesCardBuilderParams: PagesCardBuilderParams, + val activityCardBuilderParams: ActivityCardBuilderParams ) : MySiteCardAndItemBuilderParams() data class TodaysStatsCardBuilderParams( @@ -83,6 +89,29 @@ sealed class MySiteCardAndItemBuilderParams { ) } + data class PagesCardBuilderParams( + val pageCard: PagesCardModel?, + val onPagesItemClick: (params: PagesItemClickParams) -> Unit, + val onFooterLinkClick: () -> Unit + ) : MySiteCardAndItemBuilderParams() { + data class PagesItemClickParams( + val pagesCardType: PagesCardContentType, + val pageId: Int + ) + } + + data class ActivityCardBuilderParams( + val site: SiteModel, + val activityCardModel: CardModel.ActivityCardModel?, + val onActivityItemClick: (activityCardItemClickParams: ActivityCardItemClickParams) -> Unit, + val onFooterLinkClick: () -> Unit + ) : MySiteCardAndItemBuilderParams() { + data class ActivityCardItemClickParams( + val activityId: String, + val isRewindable: Boolean + ) + } + data class SiteItemsBuilderParams( val site: SiteModel, val activeTask: QuickStartTask? = null, @@ -115,6 +144,13 @@ sealed class MySiteCardAndItemBuilderParams { val onMoreMenuClick: () -> Unit ) : MySiteCardAndItemBuilderParams() + data class DashboardCardDomainBuilderParams( + val isEligible: Boolean = false, + val onClick: () -> Unit, + val onHideMenuItemClick: () -> Unit, + val onMoreMenuClick: () -> Unit + ) : MySiteCardAndItemBuilderParams() + data class SingleActionCardParams( @StringRes val textResource: Int, @DrawableRes val imageResource: Int, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt index 9f50ca206e34..0f1cd14db71e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt @@ -22,6 +22,7 @@ import org.wordpress.android.WordPress import org.wordpress.android.databinding.MySiteFragmentBinding import org.wordpress.android.databinding.MySiteInfoHeaderCardBinding import org.wordpress.android.ui.ActivityLauncher +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginFragment import org.wordpress.android.ui.main.SitePickerActivity import org.wordpress.android.ui.main.utils.MeGravatarLoader import org.wordpress.android.ui.mysite.MySiteCardAndItem.SiteInfoHeaderCard @@ -100,7 +101,7 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment), toolbarMain.let { toolbar -> toolbar.inflateMenu(R.menu.my_site_menu) toolbar.menu.findItem(R.id.me_item)?.let { meMenu -> - meMenu.actionView.let { actionView -> + meMenu.actionView?.let { actionView -> actionView.contentDescription = meMenu.title actionView.setOnClickListener { viewModel.onAvatarPressed() } TooltipCompat.setTooltipText(actionView, meMenu.title) @@ -187,6 +188,9 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment), viewModel.selectTab.observeEvent(viewLifecycleOwner) { navTarget -> viewPager.setCurrentItem(navTarget.position, navTarget.smoothAnimation) } + viewModel.onShowJetpackIndividualPluginOverlay.observeEvent(viewLifecycleOwner) { + WPJetpackIndividualPluginFragment.show(requireActivity().supportFragmentManager) + } } private fun MySiteFragmentBinding.loadGravatar(avatarUrl: String) = @@ -204,9 +208,9 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment), private fun MySiteFragmentBinding.loadData(state: State.SiteSelected) { tabLayout.setVisible(state.tabsUiState.showTabs) updateTabs(state.tabsUiState) - actionableEmptyView.setVisible(false) - viewModel.setActionableEmptyViewGone(actionableEmptyView.isVisible) { + if (actionableEmptyView.isVisible) { actionableEmptyView.setVisible(false) + viewModel.onActionableEmptyViewGone() } if (state.siteInfoHeaderState.hasUpdates || !header.isVisible) { siteInfo.loadMySiteDetails(state.siteInfoHeaderState.siteInfoHeader) @@ -259,11 +263,11 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment), private fun MySiteFragmentBinding.loadEmptyView(state: State.NoSites) { tabLayout.setVisible(state.tabsUiState.showTabs) - viewModel.setActionableEmptyViewVisible(actionableEmptyView.isVisible) { + if (!actionableEmptyView.isVisible) { actionableEmptyView.setVisible(true) actionableEmptyView.image.setVisible(state.shouldShowImage) + viewModel.onActionableEmptyViewVisible() } - actionableEmptyView.image.setVisible(state.shouldShowImage) siteTitle = getString(R.string.my_site_section_screen_title) updateSiteInfoToolbarView(state.siteInfoToolbarViewParams) appbarMain.setExpanded(false, true) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteSourceManager.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteSourceManager.kt index 1e950bd10c36..64cc6ef006b5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteSourceManager.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteSourceManager.kt @@ -9,6 +9,7 @@ import org.wordpress.android.ui.mysite.MySiteUiState.PartialState import org.wordpress.android.ui.mysite.cards.blaze.PromoteWithBlazeCardSource import org.wordpress.android.ui.mysite.cards.dashboard.CardsSource import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptCardSource +import org.wordpress.android.ui.mysite.cards.dashboard.domain.DashboardCardDomainSource import org.wordpress.android.ui.mysite.cards.domainregistration.DomainRegistrationSource import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartCardSource import org.wordpress.android.ui.mysite.dynamiccards.DynamicCardMenuViewModel.DynamicCardMenuInteraction @@ -31,7 +32,8 @@ class MySiteSourceManager @Inject constructor( siteIconProgressSource: SiteIconProgressSource, private val bloggingPromptCardSource: BloggingPromptCardSource, promoteWithBlazeCardSource: PromoteWithBlazeCardSource, - private val selectedSiteRepository: SelectedSiteRepository + private val selectedSiteRepository: SelectedSiteRepository, + private val dashboardCardDomainSource: DashboardCardDomainSource ) { private val mySiteSources: List> = listOf( selectedSiteSource, @@ -43,7 +45,8 @@ class MySiteSourceManager @Inject constructor( dynamicCardsSource, cardsSource, bloggingPromptCardSource, - promoteWithBlazeCardSource + promoteWithBlazeCardSource, + dashboardCardDomainSource ) private val showDashboardCards: Boolean diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteUiState.kt index 720d9e247f6b..1c8b12d0ae7a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteUiState.kt @@ -30,13 +30,16 @@ data class MySiteUiState( val visibleDynamicCards: List = listOf(), val cardsUpdate: CardsUpdate? = null, val bloggingPromptsUpdate: BloggingPromptUpdate? = null, - val promoteWithBlazeUpdate: PartialState.PromoteWithBlazeUpdate? = null + val promoteWithBlazeUpdate: PartialState.PromoteWithBlazeUpdate? = null, + val hasSiteCustomDomains: Boolean = false ) { sealed class PartialState { data class CurrentAvatarUrl(val url: String) : PartialState() data class SelectedSite(val site: SiteModel?) : PartialState() data class ShowSiteIconProgressBar(val showSiteIconProgressBar: Boolean) : PartialState() data class DomainCreditAvailable(val isDomainCreditAvailable: Boolean) : PartialState() + data class CustomDomainsAvailable(val hasSiteCustomDomains: Boolean) : PartialState() + data class JetpackCapabilities(val scanAvailable: Boolean, val backupAvailable: Boolean) : PartialState() data class QuickStartUpdate( val activeTask: QuickStartTask? = null, @@ -72,6 +75,9 @@ data class MySiteUiState( is SelectedSite -> uiState.copy(site = partialState.site) is ShowSiteIconProgressBar -> uiState.copy(showSiteIconProgressBar = partialState.showSiteIconProgressBar) is DomainCreditAvailable -> uiState.copy(isDomainCreditAvailable = partialState.isDomainCreditAvailable) + is PartialState.CustomDomainsAvailable -> uiState.copy( + hasSiteCustomDomains = partialState.hasSiteCustomDomains + ) is JetpackCapabilities -> uiState.copy( scanAvailable = partialState.scanAvailable, backupAvailable = partialState.backupAvailable 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 d4c53359bc05..21a7da8b0126 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 @@ -15,6 +15,7 @@ import androidx.lifecycle.switchMap import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode.MAIN import org.wordpress.android.R @@ -23,6 +24,8 @@ import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.model.DynamicCardType import org.wordpress.android.fluxc.model.MediaModel import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.dashboard.CardModel.ActivityCardModel +import org.wordpress.android.fluxc.model.dashboard.CardModel.PagesCardModel import org.wordpress.android.fluxc.model.dashboard.CardModel.PostsCardModel import org.wordpress.android.fluxc.model.dashboard.CardModel.TodaysStatsCardModel import org.wordpress.android.fluxc.store.AccountStore @@ -39,13 +42,15 @@ import org.wordpress.android.models.ReaderTag 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.blaze.BlazeFlowSource import org.wordpress.android.ui.blaze.BlazeFeatureUtils +import org.wordpress.android.ui.blaze.BlazeFlowSource import org.wordpress.android.ui.bloggingprompts.BloggingPromptsPostTagProvider import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureCollectionOverlaySource.FEATURE_CARD -import org.wordpress.android.ui.jpfullplugininstall.GetShowJetpackFullPluginInstallOnboardingUseCase +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginHelper +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.GetShowJetpackFullPluginInstallOnboardingUseCase import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DomainRegistrationCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.JetpackFeatureCard @@ -56,11 +61,15 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Item.SingleActionCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.JetpackBadge import org.wordpress.android.ui.mysite.MySiteCardAndItem.SiteInfoHeaderCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Type +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams.ActivityCardItemClickParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BloggingPromptCardBuilderParams +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardDomainBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardsBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DomainRegistrationCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.InfoItemBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.JetpackInstallFullPluginCardBuilderParams +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.PagesCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.PostCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.PostCardBuilderParams.PostItemClickParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.PromoteWithBlazeCardBuilderParams @@ -84,6 +93,7 @@ import org.wordpress.android.ui.mysite.cards.CardsBuilder import org.wordpress.android.ui.mysite.cards.DomainRegistrationCardShownTracker import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptsCardAnalyticsTracker +import org.wordpress.android.ui.mysite.cards.dashboard.domain.DashboardCardDomainUtils import org.wordpress.android.ui.mysite.cards.dashboard.posts.PostCardType import org.wordpress.android.ui.mysite.cards.dashboard.todaysstats.TodaysStatsCardBuilder.Companion.URL_GET_MORE_VIEWS_AND_TRAFFIC import org.wordpress.android.ui.mysite.cards.jetpackfeature.JetpackFeatureCardHelper @@ -201,7 +211,10 @@ class MySiteViewModel @Inject constructor( private val bloggingPromptsCardTrackHelper: BloggingPromptsCardTrackHelper, private val getShowJetpackFullPluginInstallOnboardingUseCase: GetShowJetpackFullPluginInstallOnboardingUseCase, private val jetpackInstallFullPluginShownTracker: JetpackInstallFullPluginShownTracker, - private val blazeFeatureUtils: BlazeFeatureUtils + private val blazeFeatureUtils: BlazeFeatureUtils, + private val dashboardCardDomainUtils: DashboardCardDomainUtils, + private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper, + private val wpJetpackIndividualPluginHelper: WPJetpackIndividualPluginHelper, ) : ScopedViewModel(mainDispatcher) { private var isDefaultTabSet: Boolean = false private val _onSnackbarMessage = MutableLiveData>() @@ -220,6 +233,7 @@ class MySiteViewModel @Inject constructor( private val _onBloggingPromptsViewMore = SingleLiveEvent>() private val _onBloggingPromptsRemoved = SingleLiveEvent>() private val _onOpenJetpackInstallFullPluginOnboarding = SingleLiveEvent>() + private val _onShowJetpackIndividualPluginOverlay = SingleLiveEvent>() private val tabsUiState: LiveData = quickStartRepository.onQuickStartTabStep .switchMap { quickStartSiteMenuStep -> @@ -246,7 +260,7 @@ class MySiteViewModel @Inject constructor( val isMySiteTabsEnabled: Boolean get() = isMySiteDashboardTabsEnabled && buildConfigWrapper.isMySiteTabsEnabled && - !jetpackFeatureRemovalUtils.shouldHideJetpackFeatures() && + jetpackFeatureRemovalPhaseHelper.shouldShowDashboard() && selectedSiteRepository.getSelectedSite()?.isUsingWpComRestApi ?: true val orderedTabTypes: List @@ -294,6 +308,7 @@ class MySiteViewModel @Inject constructor( val onBloggingPromptsViewMore = _onBloggingPromptsViewMore as LiveData> val onBloggingPromptsRemoved = _onBloggingPromptsRemoved as LiveData> val onOpenJetpackInstallFullPluginOnboarding = _onOpenJetpackInstallFullPluginOnboarding as LiveData> + val onShowJetpackIndividualPluginOverlay = _onShowJetpackIndividualPluginOverlay as LiveData> val onTrackWithTabSource = _onTrackWithTabSource as LiveData> val selectTab: LiveData> = _selectTab private var shouldMarkUpdateSiteTitleTaskComplete = false @@ -332,7 +347,8 @@ class MySiteViewModel @Inject constructor( scanAvailable, cardsUpdate, bloggingPromptsUpdate, - promoteWithBlazeUpdate + promoteWithBlazeUpdate, + hasSiteCustomDomains ) selectDefaultTabIfNeeded() trackCardsAndItemsShownIfNeeded(state) @@ -349,6 +365,8 @@ class MySiteViewModel @Inject constructor( bloggingPromptsCardTrackHelper.onSiteChanged(site?.id) + dashboardCardDomainUtils.onSiteChanged(site?.id, state as? SiteSelected) + UiModel(currentAvatarUrl.orEmpty(), state) } } @@ -378,7 +396,8 @@ class MySiteViewModel @Inject constructor( scanAvailable: Boolean, cardsUpdate: CardsUpdate?, bloggingPromptUpdate: BloggingPromptUpdate?, - promoteWithBlazeUpdate: PromoteWithBlazeUpdate? + promoteWithBlazeUpdate: PromoteWithBlazeUpdate?, + hasSiteCustomDomains: Boolean ): SiteSelected { val siteItems = buildSiteSelectedState( site, @@ -391,7 +410,8 @@ class MySiteViewModel @Inject constructor( scanAvailable, cardsUpdate, bloggingPromptUpdate, - promoteWithBlazeUpdate + promoteWithBlazeUpdate, + hasSiteCustomDomains ) val siteInfoCardBuilderParams = SiteInfoCardBuilderParams( @@ -480,7 +500,8 @@ class MySiteViewModel @Inject constructor( scanAvailable: Boolean, cardsUpdate: CardsUpdate?, bloggingPromptUpdate: BloggingPromptUpdate?, - promoteWithBlazeUpdate: PromoteWithBlazeUpdate? + promoteWithBlazeUpdate: PromoteWithBlazeUpdate?, + hasSiteCustomDomains: Boolean ): Map> { val infoItem = siteItemsBuilder.build( InfoItemBuilderParams( @@ -510,7 +531,7 @@ class MySiteViewModel @Inject constructor( val migrationSuccessCard = SingleActionCard( textResource = R.string.jp_migration_success_card_message, - imageResource = R.drawable.ic_wordpress_blue_32dp, + imageResource = R.drawable.ic_wordpress_jetpack_appicon, onActionClick = ::onPleaseDeleteWordPressAppCardClick ).takeIf { val isJetpackApp = buildConfigWrapper.isJetpackApp @@ -526,7 +547,7 @@ class MySiteViewModel @Inject constructor( ) val jetpackInstallFullPluginCard = jetpackInstallFullPluginCardBuilder.build(jetpackInstallFullPluginCardParams) - val cardsResult = if (jetpackFeatureRemovalUtils.shouldHideJetpackFeatures()) emptyList() + val cardsResult = if (!jetpackFeatureRemovalPhaseHelper.shouldShowDashboard()) emptyList() else cardsBuilder.build( QuickActionsCardBuilderParams( siteModel = site, @@ -574,14 +595,35 @@ class MySiteViewModel @Inject constructor( onRemoveClick = this::onBloggingPromptRemoveClick ), promoteWithBlazeCardBuilderParams = PromoteWithBlazeCardBuilderParams( - isEligible = blazeFeatureUtils.shouldShowBlazeEntryPoint( + isEligible = blazeFeatureUtils.shouldShowBlazeCardEntryPoint( promoteWithBlazeUpdate?.blazeStatusModel, site.siteId ), onClick = this::onPromoteWithBlazeCardClick, onHideMenuItemClick = this::onPromoteWithBlazeCardHideMenuItemClick, onMoreMenuClick = this::onPromoteWithBlazeCardMoreMenuClick - ) + ), + dashboardCardDomainBuilderParams = DashboardCardDomainBuilderParams( + isEligible = dashboardCardDomainUtils.shouldShowCard( + site, isDomainCreditAvailable, hasSiteCustomDomains + ), + onClick = this::onDashboardCardDomainClick, + onHideMenuItemClick = this::onDashboardCardDomainHideMenuItemClick, + onMoreMenuClick = this::onDashboardCardDomainMoreMenuClick + ), + pagesCardBuilderParams = PagesCardBuilderParams( + pageCard = cardsUpdate?.cards?.firstOrNull { it is PagesCardModel } as? PagesCardModel, + onPagesItemClick = this::onPagesItemClick, + onFooterLinkClick = this::onPagesCardFooterLinkClick + ), + activityCardBuilderParams = ActivityCardBuilderParams( + site = site, + activityCardModel = cardsUpdate?.cards?.firstOrNull { + it is ActivityCardModel + } as? ActivityCardModel, + onActivityItemClick = this::onActivityCardItemClick, + onFooterLinkClick = this::onActivityCardFooterLinkClick + ), ), QuickLinkRibbonBuilderParams( siteModel = site, @@ -614,7 +656,7 @@ class MySiteViewModel @Inject constructor( enableMediaFocusPoint = shouldEnableSiteItemsFocusPoints(), onClick = this::onItemClick, isBlazeEligible = - blazeFeatureUtils.shouldShowBlazeEntryPoint(promoteWithBlazeUpdate?.blazeStatusModel, site.siteId) + blazeFeatureUtils.shouldShowBlazeMenuEntryPoint(promoteWithBlazeUpdate?.blazeStatusModel) ) ) @@ -665,6 +707,34 @@ class MySiteViewModel @Inject constructor( ) } + @Suppress("UNUSED_PARAMETER") + private fun onPagesItemClick(params: PagesCardBuilderParams.PagesItemClickParams) { + // implement navigation logic for pages + } + + @Suppress("UNUSED_PARAMETER") + private fun onPagesCardFooterLinkClick() { + // implement navigation logic for create page + } + + private fun onActivityCardItemClick(activityCardItemClickParams: ActivityCardItemClickParams) { + cardsTracker.trackActivityCardItemClicked() + _onNavigation.value = + Event( + SiteNavigationAction.OpenActivityLogDetail( + requireNotNull(selectedSiteRepository.getSelectedSite()), + activityCardItemClickParams.activityId, + activityCardItemClickParams.isRewindable + ) + ) + } + + private fun onActivityCardFooterLinkClick() { + cardsTracker.trackActivityCardFooterClicked() + _onNavigation.value = + Event(SiteNavigationAction.OpenActivityLog(requireNotNull(selectedSiteRepository.getSelectedSite()))) + } + private fun buildJetpackBadgeIfEnabled(): JetpackBadge? { val screen = JetpackPoweredScreen.WithStaticText.HOME return JetpackBadge( @@ -675,8 +745,7 @@ class MySiteViewModel @Inject constructor( null } ).takeIf { - jetpackBrandingUtils.shouldShowJetpackBranding() && - !jetpackFeatureRemovalUtils.shouldHideJetpackFeatures() + jetpackBrandingUtils.shouldShowJetpackBrandingInDashboard() } } @@ -703,6 +772,7 @@ class MySiteViewModel @Inject constructor( add(Type.QUICK_LINK_RIBBON) add(Type.JETPACK_INSTALL_FULL_PLUGIN_CARD) } + MySiteTabType.DASHBOARD -> mutableListOf().apply { if (defaultTab == MySiteTabType.SITE_MENU) { add(Type.QUICK_START_CARD) @@ -710,6 +780,7 @@ class MySiteViewModel @Inject constructor( add(Type.DOMAIN_REGISTRATION_CARD) add(Type.QUICK_ACTIONS_CARD) } + MySiteTabType.ALL -> emptyList() } @@ -722,9 +793,13 @@ class MySiteViewModel @Inject constructor( @Suppress("EmptyFunctionBlock") private fun onGetMoreViewsClick() { cardsTracker.trackTodaysStatsCardGetMoreViewsNudgeClicked() - _onNavigation.value = Event( - SiteNavigationAction.OpenTodaysStatsGetMoreViewsExternalUrl(URL_GET_MORE_VIEWS_AND_TRAFFIC) - ) + if (jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { + _onNavigation.value = Event(SiteNavigationAction.ShowJetpackRemovalStaticPostersView) + } else { + _onNavigation.value = Event( + SiteNavigationAction.OpenTodaysStatsGetMoreViewsExternalUrl(URL_GET_MORE_VIEWS_AND_TRAFFIC) + ) + } } private fun onTodaysStatsCardFooterLinkClick() { @@ -739,7 +814,11 @@ class MySiteViewModel @Inject constructor( private fun navigateToTodaysStats() { val selectedSite = requireNotNull(selectedSiteRepository.getSelectedSite()) - _onNavigation.value = Event(SiteNavigationAction.OpenStatsInsights(selectedSite)) + if (jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { + _onNavigation.value = Event(SiteNavigationAction.ShowJetpackRemovalStaticPostersView) + } else { + _onNavigation.value = Event(SiteNavigationAction.OpenStatsInsights(selectedSite)) + } } private fun buildNoSiteState(): NoSites { @@ -816,6 +895,7 @@ class MySiteViewModel @Inject constructor( QuickStartNewSiteTask.UPDATE_SITE_TITLE, QuickStartNewSiteTask.UPLOAD_SITE_ICON, quickStartRepository.quickStartType.getTaskFromString(QUICK_START_VIEW_SITE_LABEL) -> true + else -> false } } @@ -845,17 +925,20 @@ class MySiteViewModel @Inject constructor( ListItemAction.PLAN -> { SiteNavigationAction.OpenPlan(selectedSite) } + ListItemAction.POSTS -> SiteNavigationAction.OpenPosts(selectedSite) ListItemAction.PAGES -> { quickStartRepository.completeTask(QuickStartNewSiteTask.REVIEW_PAGES) SiteNavigationAction.OpenPages(selectedSite) } + ListItemAction.ADMIN -> SiteNavigationAction.OpenAdmin(selectedSite) ListItemAction.PEOPLE -> SiteNavigationAction.OpenPeople(selectedSite) ListItemAction.SHARING -> { quickStartRepository.requestNextStepOfTask(QuickStartNewSiteTask.ENABLE_POST_SHARING) SiteNavigationAction.OpenSharing(selectedSite) } + ListItemAction.DOMAINS -> SiteNavigationAction.OpenDomains(selectedSite) ListItemAction.SITE_SETTINGS -> SiteNavigationAction.OpenSiteSettings(selectedSite) ListItemAction.THEMES -> SiteNavigationAction.OpenThemes(selectedSite) @@ -866,16 +949,19 @@ class MySiteViewModel @Inject constructor( ) getStatsNavigationActionForSite(selectedSite) } + ListItemAction.MEDIA -> { quickStartRepository.requestNextStepOfTask( quickStartRepository.quickStartType.getTaskFromString(QUICK_START_UPLOAD_MEDIA_LABEL) ) SiteNavigationAction.OpenMedia(selectedSite) } + ListItemAction.COMMENTS -> SiteNavigationAction.OpenUnifiedComments(selectedSite) ListItemAction.VIEW_SITE -> { SiteNavigationAction.OpenSite(selectedSite) } + ListItemAction.JETPACK_SETTINGS -> SiteNavigationAction.OpenJetpackSettings(selectedSite) ListItemAction.BLAZE -> { blazeFeatureUtils.trackEntryPointTapped(BlazeFlowSource.MENU_ITEM) @@ -951,9 +1037,11 @@ class MySiteViewModel @Inject constructor( !selectedSite.isUsingWpComRestApi -> { R.string.my_site_icon_dialog_change_requires_jetpack_message } + hasIcon -> { R.string.my_site_icon_dialog_change_requires_permission_message } + else -> { R.string.my_site_icon_dialog_add_requires_permission_message } @@ -1055,6 +1143,7 @@ class MySiteViewModel @Inject constructor( checkAndShowJetpackFullPluginInstallOnboarding() checkAndShowQuickStartNotice() bloggingPromptsCardTrackHelper.onResume(currentTab) + dashboardCardDomainUtils.onResume(currentTab, uiModel.value?.state as? SiteSelected) } private fun checkAndShowJetpackFullPluginInstallOnboarding() { @@ -1107,21 +1196,26 @@ class MySiteViewModel @Inject constructor( ) ) } + TAG_REMOVE_NEXT_STEPS_DIALOG -> onRemoveNextStepsDialogPositiveButtonClicked() } + is Negative -> when (interaction.tag) { TAG_ADD_SITE_ICON_DIALOG -> { quickStartRepository.completeTask(QuickStartNewSiteTask.UPLOAD_SITE_ICON) quickStartRepository.checkAndShowQuickStartNotice() } + TAG_CHANGE_SITE_ICON_DIALOG -> { analyticsTrackerWrapper.track(Stat.MY_SITE_ICON_REMOVED) quickStartRepository.completeTask(QuickStartNewSiteTask.UPLOAD_SITE_ICON) quickStartRepository.checkAndShowQuickStartNotice() selectedSiteRepository.updateSiteIconMediaId(0, true) } + TAG_REMOVE_NEXT_STEPS_DIALOG -> onRemoveNextStepsDialogNegativeButtonClicked() } + is Dismissed -> when (interaction.tag) { TAG_ADD_SITE_ICON_DIALOG, TAG_CHANGE_SITE_ICON_DIALOG -> { quickStartRepository.completeTask(QuickStartNewSiteTask.UPLOAD_SITE_ICON) @@ -1224,7 +1318,11 @@ class MySiteViewModel @Inject constructor( return fluxCUtilsWrapper.mediaModelFromLocalUri(uri, mimeType, site.id) } - private fun getStatsNavigationActionForSite(site: SiteModel) = when { + private fun getStatsNavigationActionForSite(site: SiteModel): SiteNavigationAction = when { + // if we are in static posters phase - we don't want to show any connection/login messages + jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage() -> + SiteNavigationAction.ShowJetpackRemovalStaticPostersView + // If the user is not logged in and the site is already connected to Jetpack, ask to login. !accountStore.hasAccessToken() && site.isJetpackConnected -> SiteNavigationAction.StartWPComLoginForJetpackStats @@ -1308,6 +1406,7 @@ class MySiteViewModel @Inject constructor( isSiteTitleTaskCompleted: Boolean, isNewSite: Boolean ) { + if (!jetpackFeatureRemovalPhaseHelper.shouldShowQuickStart()) return quickStartRepository.checkAndSetQuickStartType(isNewSite = isNewSite) if (quickStartDynamicCardsFeatureConfig.isEnabled()) { startQuickStart(siteLocalId, isSiteTitleTaskCompleted) @@ -1368,8 +1467,10 @@ class MySiteViewModel @Inject constructor( when (params.postCardType) { PostCardType.CREATE_FIRST, PostCardType.CREATE_NEXT -> _onNavigation.value = Event(SiteNavigationAction.OpenEditorToCreateNewPost(site)) + PostCardType.DRAFT -> _onNavigation.value = Event(SiteNavigationAction.EditDraftPost(site, params.postId)) + PostCardType.SCHEDULED -> _onNavigation.value = Event(SiteNavigationAction.EditScheduledPost(site, params.postId)) } @@ -1386,6 +1487,7 @@ class MySiteViewModel @Inject constructor( _onNavigation.value = when (postCardType) { PostCardType.CREATE_FIRST, PostCardType.CREATE_NEXT -> Event(SiteNavigationAction.OpenEditorToCreateNewPost(site)) + PostCardType.DRAFT -> Event(SiteNavigationAction.OpenDraftsPosts(site)) PostCardType.SCHEDULED -> Event(SiteNavigationAction.OpenScheduledPosts(site)) } @@ -1525,16 +1627,46 @@ class MySiteViewModel @Inject constructor( refresh() } + private fun onDashboardCardDomainMoreMenuClick() { + dashboardCardDomainUtils.trackDashboardCardDomainMoreMenuTapped(uiModel.value?.state as? SiteSelected) + } + + private fun onDashboardCardDomainClick() { + val selectedSite = requireNotNull(selectedSiteRepository.getSelectedSite()) + dashboardCardDomainUtils.trackDashboardCardDomainTapped(uiModel.value?.state as? SiteSelected) + _onNavigation.value = Event(SiteNavigationAction.OpenPaidDomainSearch(selectedSite)) + } + + private fun onDashboardCardDomainHideMenuItemClick() { + dashboardCardDomainUtils.trackDashboardCardDomainHiddenByUser(uiModel.value?.state as? SiteSelected) + selectedSiteRepository.getSelectedSite()?.let { + dashboardCardDomainUtils.hideCard(it.siteId) + } + refresh() + } + fun isRefreshing() = mySiteSourceManager.isRefreshing() - fun setActionableEmptyViewGone(isVisible: Boolean, setGone: () -> Unit) { - if (isVisible) analyticsTrackerWrapper.track(Stat.MY_SITE_NO_SITES_VIEW_HIDDEN) - setGone() + fun onActionableEmptyViewGone() { + analyticsTrackerWrapper.track(Stat.MY_SITE_NO_SITES_VIEW_HIDDEN) + } + + fun onActionableEmptyViewVisible() { + analyticsTrackerWrapper.track(Stat.MY_SITE_NO_SITES_VIEW_DISPLAYED) + checkJetpackIndividualPluginOverlayShouldShow() } - fun setActionableEmptyViewVisible(isVisible: Boolean, setVisible: () -> Unit) { - if (!isVisible) analyticsTrackerWrapper.track(Stat.MY_SITE_NO_SITES_VIEW_DISPLAYED) - setVisible() + private fun checkJetpackIndividualPluginOverlayShouldShow() { + // don't check if already shown + if (_onShowJetpackIndividualPluginOverlay.value?.peekContent() == Unit) return + + viewModelScope.launch { + val showOverlay = wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay() + if (showOverlay) { + delay(DELAY_BEFORE_SHOWING_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY) + _onShowJetpackIndividualPluginOverlay.value = Event(Unit) + } + } } fun trackWithTabSource(event: MySiteTrackWithTabSource) { @@ -1592,6 +1724,7 @@ class MySiteViewModel @Inject constructor( .forEach { jetpackFeatureCardShownTracker.trackShown(it.type) } siteSelected.cardAndItems.filterIsInstance() .forEach { jetpackInstallFullPluginShownTracker.trackShown(it.type, quickStartRepository.currentTab) } + dashboardCardDomainUtils.trackDashboardCardDomainShown(viewModelScope, siteSelected) } private fun resetShownTrackers() { @@ -1750,5 +1883,6 @@ class MySiteViewModel @Inject constructor( const val LIST_SCROLL_DELAY_MS = 500L const val MY_SITE_TAB = "tab" const val TAB_SOURCE = "tab_source" + private const val DELAY_BEFORE_SHOWING_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY = 500L } } 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 index 42c4d4c92fe7..9c9a34c1f9f2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt @@ -60,6 +60,7 @@ sealed class SiteNavigationAction { ) : SiteNavigationAction() data class OpenDomainRegistration(val site: SiteModel) : SiteNavigationAction() + data class OpenPaidDomainSearch(val site: SiteModel) : SiteNavigationAction() data class AddNewSite(val hasAccessToken: Boolean, val source: SiteCreationSource) : SiteNavigationAction() data class ShowQuickStartDialog( @StringRes val title: Int, @@ -84,4 +85,7 @@ sealed class SiteNavigationAction { object OpenJetpackMigrationDeleteWP : SiteNavigationAction() data class OpenJetpackFeatureOverlay(val source: JetpackFeatureCollectionOverlaySource) : SiteNavigationAction() data class OpenPromoteWithBlazeOverlay(val source: BlazeFlowSource) : SiteNavigationAction() + object ShowJetpackRemovalStaticPostersView : SiteNavigationAction() + data class OpenActivityLogDetail(val site: SiteModel, val activityId: String, val isRewindable: Boolean) : + SiteNavigationAction() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsAdapter.kt index d18ba656e0b2..0841b25dccb4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsAdapter.kt @@ -5,6 +5,8 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil.Callback import androidx.recyclerview.widget.RecyclerView.Adapter import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ActivityCard +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.DashboardDomainCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PromoteWithBlazeCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.BloggingPromptCard.BloggingPromptCardWithData import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ErrorCard @@ -13,14 +15,18 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.Das import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PostCard.PostCardWithPostItems import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PostCard.PostCardWithoutPostItems import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.TodaysStatsCard.TodaysStatsCardWithData +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PagesCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.DashboardCardType import org.wordpress.android.ui.mysite.cards.blaze.PromoteWithBlazeCardViewHolder +import org.wordpress.android.ui.mysite.cards.dashboard.activity.ActivityCardViewHolder import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptCardViewHolder import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptsCardAnalyticsTracker import org.wordpress.android.ui.mysite.cards.dashboard.error.ErrorCardViewHolder import org.wordpress.android.ui.mysite.cards.dashboard.error.ErrorWithinCardViewHolder +import org.wordpress.android.ui.mysite.cards.dashboard.pages.PagesCardViewHolder import org.wordpress.android.ui.mysite.cards.dashboard.posts.PostCardViewHolder import org.wordpress.android.ui.mysite.cards.dashboard.todaysstats.TodaysStatsCardViewHolder +import org.wordpress.android.ui.mysite.cards.dashboard.domain.DashboardDomainCardViewHolder import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.HtmlCompatWrapper import org.wordpress.android.util.image.ImageManager @@ -52,6 +58,9 @@ class CardsAdapter( learnMoreClicked ) DashboardCardType.PROMOTE_WITH_BLAZE_CARD.ordinal -> PromoteWithBlazeCardViewHolder(parent, uiHelpers) + DashboardCardType.DASHBOARD_DOMAIN_CARD.ordinal -> DashboardDomainCardViewHolder(parent, uiHelpers) + DashboardCardType.PAGES_CARD.ordinal -> PagesCardViewHolder(parent, uiHelpers) + DashboardCardType.ACTIVITY_CARD.ordinal -> ActivityCardViewHolder(parent, uiHelpers) else -> throw IllegalArgumentException("Unexpected view type") } } @@ -66,6 +75,9 @@ class CardsAdapter( is PostCardViewHolder<*> -> holder.bind(items[position] as PostCard) is BloggingPromptCardViewHolder -> holder.bind(items[position] as BloggingPromptCardWithData) is PromoteWithBlazeCardViewHolder -> holder.bind(items[position] as PromoteWithBlazeCard) + is DashboardDomainCardViewHolder -> holder.bind(items[position] as DashboardDomainCard) + is PagesCardViewHolder -> holder.bind(items[position] as PagesCard) + is ActivityCardViewHolder -> holder.bind(items[position] as ActivityCard) } } @@ -95,6 +107,9 @@ class CardsAdapter( oldItem is PostCardWithoutPostItems && newItem is PostCardWithoutPostItems -> true oldItem is BloggingPromptCardWithData && newItem is BloggingPromptCardWithData -> true oldItem is PromoteWithBlazeCard && newItem is PromoteWithBlazeCard -> true + oldItem is DashboardDomainCard && newItem is DashboardDomainCard -> true + oldItem is PagesCard && newItem is PagesCard -> true + oldItem is ActivityCard && newItem is ActivityCard -> true else -> throw UnsupportedOperationException("Diff not implemented yet") } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsBuilder.kt index 05628bafb1ed..3c232d868758 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsBuilder.kt @@ -6,9 +6,12 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.Das import org.wordpress.android.ui.mysite.MySiteCardAndItem.DashboardCardType.POST_CARD_WITHOUT_POST_ITEMS import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardsBuilderParams import org.wordpress.android.ui.mysite.cards.blaze.PromoteWithBlazeCardBuilder +import org.wordpress.android.ui.mysite.cards.dashboard.activity.ActivityCardBuilder import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptCardBuilder +import org.wordpress.android.ui.mysite.cards.dashboard.pages.PagesCardBuilder import org.wordpress.android.ui.mysite.cards.dashboard.posts.PostCardBuilder import org.wordpress.android.ui.mysite.cards.dashboard.todaysstats.TodaysStatsCardBuilder +import org.wordpress.android.ui.mysite.cards.dashboard.domain.DashboardDomainCardBuilder import org.wordpress.android.ui.utils.ListItemInteraction import javax.inject.Inject @@ -16,7 +19,10 @@ class CardsBuilder @Inject constructor( private val todaysStatsCardBuilder: TodaysStatsCardBuilder, private val postCardBuilder: PostCardBuilder, private val bloggingPromptCardBuilder: BloggingPromptCardBuilder, - private val promoteWithBlazeCardBuilder: PromoteWithBlazeCardBuilder + private val promoteWithBlazeCardBuilder: PromoteWithBlazeCardBuilder, + private val dashboardDomainCardBuilder: DashboardDomainCardBuilder, + private val pagesCardBuilder: PagesCardBuilder, + private val activityCardBuilder: ActivityCardBuilder ) { fun build( dashboardCardsBuilderParams: DashboardCardsBuilderParams @@ -36,6 +42,10 @@ class CardsBuilder @Inject constructor( add(it) } + dashboardDomainCardBuilder.build(dashboardCardsBuilderParams.dashboardCardDomainBuilderParams)?.let { + add(it) + } + todaysStatsCardBuilder.build(dashboardCardsBuilderParams.todaysStatsCardBuilderParams) ?.let { add(it) } @@ -50,6 +60,10 @@ class CardsBuilder @Inject constructor( if (showPostCards) { addAll(postCards) } + + pagesCardBuilder.build(dashboardCardsBuilderParams.pagesCardBuilderParams)?.let { add(it) } + + activityCardBuilder.build(dashboardCardsBuilderParams.activityCardBuilderParams)?.let { add(it) } } }.toList() ) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsShownTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsShownTracker.kt index 1d21e898c01c..6154bb9b2a07 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsShownTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsShownTracker.kt @@ -4,6 +4,7 @@ import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.ui.blaze.BlazeFlowSource import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.DashboardDomainCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.BloggingPromptCard.BloggingPromptCardWithData import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ErrorCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PostCard @@ -34,6 +35,7 @@ class CardsShownTracker @Inject constructor( } } + @Suppress("LongMethod") private fun trackCardShown(card: DashboardCard) = when (card) { is ErrorCard -> trackCardShown( Pair( @@ -83,6 +85,24 @@ class CardsShownTracker @Inject constructor( Type.PROMOTE_WITH_BLAZE.label ) ) + is DashboardDomainCard -> trackCardShown( + Pair( + card.dashboardCardType.toTypeValue().label, + Type.DASHBOARD_CARD_DOMAIN.label + ) + ) + is DashboardCard.PagesCard -> trackCardShown( + Pair( + card.dashboardCardType.toTypeValue().label, + Type.PAGES.label + ) + ) + is DashboardCard.ActivityCard.ActivityCardWithItems -> trackCardShown( + Pair( + card.dashboardCardType.toTypeValue().label, + Type.ACTIVITY.label + ) + ) } fun trackQuickStartCardShown(quickStartType: QuickStartType) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt index cfcd85e10c6b..4dd949973f7c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSource.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.dashboard.CardModel.Type @@ -14,6 +15,8 @@ import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.mysite.MySiteSource.MySiteRefreshSource import org.wordpress.android.ui.mysite.MySiteUiState.PartialState.CardsUpdate import org.wordpress.android.ui.mysite.SelectedSiteRepository +import org.wordpress.android.util.config.DashboardCardActivityLogConfig +import org.wordpress.android.util.config.DashboardCardPagesConfig import org.wordpress.android.util.config.MySiteDashboardTodaysStatsCardFeatureConfig import javax.inject.Inject import javax.inject.Named @@ -24,9 +27,16 @@ class CardsSource @Inject constructor( private val selectedSiteRepository: SelectedSiteRepository, private val cardsStore: CardsStore, todaysStatsCardFeatureConfig: MySiteDashboardTodaysStatsCardFeatureConfig, + dashboardCardPagesConfig: DashboardCardPagesConfig, + dashboardCardActivityLogConfig: DashboardCardActivityLogConfig, @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher ) : MySiteRefreshSource { private val isTodaysStatsCardFeatureConfigEnabled = todaysStatsCardFeatureConfig.isEnabled() + + private val isDashboardCardPagesConfigEnabled = dashboardCardPagesConfig.isEnabled() + + private val isDashboardCardActivityLogConfigEnabled = dashboardCardActivityLogConfig.isEnabled() + override val refresh = MutableLiveData(false) override fun build(coroutineScope: CoroutineScope, siteLocalId: Int): LiveData { @@ -44,9 +54,11 @@ class CardsSource @Inject constructor( val selectedSite = selectedSiteRepository.getSelectedSite() if (selectedSite != null && selectedSite.id == siteLocalId) { coroutineScope.launch(bgDispatcher) { - cardsStore.getCards(selectedSite, getCardTypes()).collect { result -> - postValue(CardsUpdate(result.model)) - } + cardsStore.getCards(selectedSite, getCardTypes()) + .map { it.model } + .collect { result -> + postValue(CardsUpdate(result)) + } } } else { postErrorState() @@ -95,6 +107,8 @@ class CardsSource @Inject constructor( private fun getCardTypes() = mutableListOf().apply { if (isTodaysStatsCardFeatureConfigEnabled) add(Type.TODAYS_STATS) + if (isDashboardCardPagesConfigEnabled) add(Type.PAGES) + if (isDashboardCardActivityLogConfigEnabled) add(Type.ACTIVITY) add(Type.POSTS) }.toList() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTracker.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTracker.kt index 975107aa0625..78a64233f026 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTracker.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTracker.kt @@ -24,7 +24,10 @@ class CardsTracker @Inject constructor( STATS("stats"), POST("post"), BLOGGING_PROMPT("blogging_prompt"), - PROMOTE_WITH_BLAZE("promote_with_blaze") + PROMOTE_WITH_BLAZE("promote_with_blaze"), + PAGES("pages"), + ACTIVITY("activity_log"), + DASHBOARD_CARD_DOMAIN("dashboard_card_domain"), } enum class QuickStartSubtype(val label: String) { @@ -46,6 +49,10 @@ class CardsTracker @Inject constructor( SCHEDULED("scheduled") } + enum class ActivityLogSubtype(val label: String) { + ACTIVITY_LOG("activity_log"), + } + fun trackQuickStartCardItemClicked(quickStartTaskType: QuickStartTaskType) { trackCardItemClicked(Type.QUICK_START.label, quickStartTaskType.toSubtypeValue().label) } @@ -70,6 +77,14 @@ class CardsTracker @Inject constructor( trackCardItemClicked(Type.POST.label, postCardType.toSubtypeValue().label) } + fun trackActivityCardItemClicked() { + trackCardItemClicked(Type.ACTIVITY.label, ActivityLogSubtype.ACTIVITY_LOG.label) + } + + fun trackActivityCardFooterClicked() { + trackCardFooterLinkClicked(Type.ACTIVITY.label, ActivityLogSubtype.ACTIVITY_LOG.label) + } + private fun trackCardFooterLinkClicked(type: String, subtype: String) { analyticsTrackerWrapper.track( Stat.MY_SITE_DASHBOARD_CARD_FOOTER_ACTION_TAPPED, @@ -119,6 +134,10 @@ fun DashboardCardType.toTypeValue(): Type { DashboardCardType.POST_CARD_WITH_POST_ITEMS -> Type.POST DashboardCardType.BLOGGING_PROMPT_CARD -> Type.BLOGGING_PROMPT DashboardCardType.PROMOTE_WITH_BLAZE_CARD -> Type.PROMOTE_WITH_BLAZE + DashboardCardType.DASHBOARD_DOMAIN_CARD -> Type.DASHBOARD_CARD_DOMAIN + DashboardCardType.PAGES_CARD -> Type.PAGES + DashboardCardType.PAGES_CARD_ERROR -> Type.ERROR + DashboardCardType.ACTIVITY_CARD -> Type.ACTIVITY } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityCardBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityCardBuilder.kt new file mode 100644 index 000000000000..d2c84da61617 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityCardBuilder.kt @@ -0,0 +1,74 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.activity + +import org.wordpress.android.R +import org.wordpress.android.fluxc.model.activity.ActivityLogModel +import org.wordpress.android.ui.activitylog.list.ActivityLogListItem +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ActivityCard +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ActivityCard.ActivityCardWithItems +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ActivityCard.ActivityCardWithItems.ActivityItem +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams.ActivityCardItemClickParams +import org.wordpress.android.ui.utils.ListItemInteraction +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.util.DateTimeUtilsWrapper +import org.wordpress.android.util.SiteUtilsWrapper +import org.wordpress.android.util.config.DashboardCardActivityLogConfig +import java.util.Date +import javax.inject.Inject + +private const val MAX_ITEMS_IN_CARD: Int = 3 + +class ActivityCardBuilder @Inject constructor( + private val dashboardCardActivityLogConfig: DashboardCardActivityLogConfig, + private val dateTimeUtilsWrapper: DateTimeUtilsWrapper, + private val siteUtilsWrapper: SiteUtilsWrapper, +) { + fun build(params: ActivityCardBuilderParams): ActivityCard? { + return shouldBuildActivityCard(params).takeIf { it }?.let { buildActivityCard(params) } + } + + private fun buildActivityCard(params: ActivityCardBuilderParams): ActivityCardWithItems { + val activities = params.activityCardModel?.activities + val content = activities?.take(MAX_ITEMS_IN_CARD)?.mapToActivityItems(params.onActivityItemClick) ?: emptyList() + return ActivityCardWithItems( + title = UiStringRes(R.string.dashboard_activity_card_title), + activityItems = content, + footerLink = ActivityCard.FooterLink( + label = UiStringRes(R.string.dashboard_activity_card_footer_link), + onClick = params.onFooterLinkClick + ) + ) + } + + private fun List.mapToActivityItems( + onClick: (activityCardItemClickParams: ActivityCardItemClickParams) -> Unit + ) = map { + ActivityItem( + label = UiString.UiStringText(it.content?.text ?: ""), + subLabel = it.summary, + displayDate = buildDateLine(it.published), + icon = ActivityLogListItem.Icon.fromValue(it.gridicon).drawable, + iconBackgroundColor = ActivityLogListItem.Status.fromValue(it.status).color, + onClick = ListItemInteraction.create( + ActivityCardItemClickParams(it.activityID, it.rewindable ?: false), + onClick + ) + ) + } + + private fun buildDateLine(published: Date) = dateTimeUtilsWrapper.javaDateToTimeSpan(published) + + private fun shouldBuildActivityCard(params: ActivityCardBuilderParams) : Boolean { + if (!dashboardCardActivityLogConfig.isEnabled() || + params.activityCardModel == null || + params.activityCardModel.activities.isEmpty()) { + return false + } + + val isWpComOrJetpack = siteUtilsWrapper.isAccessedViaWPComRest( + params.site + ) || params.site.isJetpackConnected + return params.site.hasCapabilityManageOptions && isWpComOrJetpack && !params.site.isWpForTeamsSite + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityCardViewHolder.kt new file mode 100644 index 000000000000..2ee470f57f26 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityCardViewHolder.kt @@ -0,0 +1,40 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.activity + +import android.view.ViewGroup +import org.wordpress.android.databinding.MySiteActivityCardWithActivityItemsBinding +import org.wordpress.android.databinding.MySiteCardFooterLinkBinding +import org.wordpress.android.databinding.MySiteCardToolbarBinding +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.extensions.viewBinding +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ActivityCard +import org.wordpress.android.ui.mysite.cards.dashboard.CardViewHolder +import org.wordpress.android.ui.utils.UiString + +class ActivityCardViewHolder( + parent: ViewGroup, + private val uiHelpers: UiHelpers +) : CardViewHolder( + parent.viewBinding(MySiteActivityCardWithActivityItemsBinding::inflate) +) { + init { + binding.activityItems.adapter = ActivityItemsAdapter(uiHelpers) + } + + fun bind(card: ActivityCard) = with(binding) { + val activityCard = card as ActivityCard.ActivityCardWithItems + (activityItems.adapter as ActivityItemsAdapter).update(activityCard.activityItems) + mySiteToolbar.update(activityCard.title) + mySiteCardFooterLink.update(activityCard.footerLink) + } + + private fun MySiteCardToolbarBinding.update(title: UiString?) { + uiHelpers.setTextOrHide(mySiteCardToolbarTitle, title) + } + + private fun MySiteCardFooterLinkBinding.update(footerLink: ActivityCard.FooterLink) { + uiHelpers.setTextOrHide(linkLabel, footerLink.label) + linkLabel.setOnClickListener { + footerLink.onClick.invoke() + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityItemViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityItemViewHolder.kt new file mode 100644 index 000000000000..55c2ea8794b9 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityItemViewHolder.kt @@ -0,0 +1,23 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.activity + +import android.view.ViewGroup +import org.wordpress.android.databinding.MySiteActivityCardItemBinding +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ActivityCard.ActivityCardWithItems.ActivityItem +import org.wordpress.android.ui.mysite.MySiteCardAndItemViewHolder +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.extensions.viewBinding + +class ActivityItemViewHolder(parent: ViewGroup, + private val uiHelpers: UiHelpers +) : MySiteCardAndItemViewHolder( + parent.viewBinding(MySiteActivityCardItemBinding::inflate) +) { + fun bind(item: ActivityItem) = with(binding) { + activityContentContainer.setOnClickListener { item.onClick.click() } + uiHelpers.setTextOrHide(activityCardItemLabel, item.label) + uiHelpers.setTextOrHide(activityCardItemSubLabel, item.subLabel) + uiHelpers.setTextOrHide(activityCardItemDisplayDate, item.displayDate) + icon.setImageResource(item.icon) + icon.setBackgroundResource(item.iconBackgroundColor) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityItemsAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityItemsAdapter.kt new file mode 100644 index 000000000000..0a747de24224 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityItemsAdapter.kt @@ -0,0 +1,53 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.activity + +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import org.wordpress.android.ui.utils.UiHelpers +import androidx.recyclerview.widget.RecyclerView.Adapter +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ActivityCard.ActivityCardWithItems.ActivityItem + +class ActivityItemsAdapter( + private val uiHelpers: UiHelpers +) : Adapter() { + private val items = mutableListOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ActivityItemViewHolder( + parent, + uiHelpers + ) + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: ActivityItemViewHolder, position: Int) { + holder.bind(items[position]) + } + + fun update(newItems: List) { + val diffResult = DiffUtil.calculateDiff(ActivityItemDiffUtil(items, newItems)) + items.clear() + items.addAll(newItems) + diffResult.dispatchUpdatesTo(this) + } + + class ActivityItemDiffUtil( + private val oldList: List, + private val newList: List + ) : DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val newItem = newList[newItemPosition] + val oldItem = oldList[oldItemPosition] + + return (oldItem == newItem) + } + + override fun getOldListSize(): Int = oldList.size + + override fun getNewListSize(): Int = newList.size + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean = oldList[oldItemPosition] == newList[newItemPosition] + } +} + diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardCardDomainSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardCardDomainSource.kt new file mode 100644 index 000000000000..70241091f238 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardCardDomainSource.kt @@ -0,0 +1,91 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.domain + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.fluxc.utils.AppLogWrapper +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.ui.mysite.MySiteSource +import org.wordpress.android.ui.mysite.MySiteUiState +import org.wordpress.android.ui.mysite.SelectedSiteRepository +import org.wordpress.android.util.AppLog +import javax.inject.Inject +import javax.inject.Named + +class DashboardCardDomainSource @Inject constructor( + @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, + private val selectedSiteRepository: SelectedSiteRepository, + private val siteStore: SiteStore, + private val domainUtils: DashboardCardDomainUtils, + private val appLogWrapper: AppLogWrapper +) : MySiteSource.MySiteRefreshSource { + override val refresh = MutableLiveData(false) + + override fun build( + coroutineScope: CoroutineScope, + siteLocalId: Int + ): LiveData { + val data = MediatorLiveData() + data.addSource(refresh) { data.refreshData(coroutineScope, siteLocalId, refresh.value) } + refresh() + return data + } + + private fun shouldFetchDomains(selectedSite: SiteModel): Boolean { + // By assuming that "isDomainCreditAvailable" and "hasSiteDomain" are false, we are checking other cases for + // "shouldShowCard". If "shouldShowCard" still returns false, then we do not need to fetch domains. + val isDomainCreditAvailable = false + val hasSiteDomain = false + + return domainUtils.shouldShowCard(selectedSite, isDomainCreditAvailable, hasSiteDomain) + } + + private fun MediatorLiveData.refreshData( + coroutineScope: CoroutineScope, + siteLocalId: Int, + isRefresh: Boolean? = null + ) { + val selectedSite = selectedSiteRepository.getSelectedSite() + when (isRefresh) { + null, true -> refreshData(coroutineScope, siteLocalId, selectedSite) + false -> Unit // Do nothing + } + } + + private fun MediatorLiveData.refreshData( + coroutineScope: CoroutineScope, + siteLocalId: Int, + selectedSite: SiteModel? + ) { + if (selectedSite == null || selectedSite.id != siteLocalId || !shouldFetchDomains(selectedSite)) { + postState(MySiteUiState.PartialState.CustomDomainsAvailable(false)) + } else { + fetchDomainsAndRefreshData(coroutineScope, selectedSite) + } + } + + private fun MediatorLiveData.fetchDomainsAndRefreshData( + coroutineScope: CoroutineScope, + selectedSite: SiteModel + ) { + coroutineScope.launch(bgDispatcher) { + val result = siteStore.fetchSiteDomains(selectedSite) + val domains = result.domains + val error = result.error + + if (result.isError) { + appLogWrapper.e( + AppLog.T.DOMAIN_REGISTRATION, + "An error occurred while fetching domains: ${error.message}" + ) + } + + postState(MySiteUiState.PartialState.CustomDomainsAvailable(domainUtils.hasCustomDomain(domains))) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardCardDomainUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardCardDomainUtils.kt new file mode 100644 index 000000000000..33a08a69c01e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardCardDomainUtils.kt @@ -0,0 +1,159 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.domain + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.rest.wpcom.site.Domain +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.DashboardDomainCard +import org.wordpress.android.ui.mysite.MySiteViewModel.State.SiteSelected +import org.wordpress.android.ui.mysite.tabs.MySiteTabType +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.util.BuildConfigWrapper +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.util.config.DashboardCardDomainFeatureConfig +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject +import javax.inject.Named + +class DashboardCardDomainUtils @Inject constructor( + private val appPrefsWrapper: AppPrefsWrapper, + private val dashboardCardDomainFeatureConfig: DashboardCardDomainFeatureConfig, + private val buildConfigWrapper: BuildConfigWrapper, + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, + @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher +) { + private var dashboardUpdateDebounceJob: Job? = null + + private val domainCardVisible = AtomicReference(null) + private val waitingToTrack = AtomicBoolean(false) + private val currentSite = AtomicReference(null) + + fun shouldShowCard(siteModel: SiteModel, isDomainCreditAvailable: Boolean, hasSiteCustomDomains: Boolean): Boolean { + return isDashboardCardDomainEnabled() && + !isDashboardCardDomainHiddenByUser(siteModel.siteId) && + (siteModel.isWPCom || siteModel.isWPComAtomic) && + siteModel.isAdmin && + !siteModel.isWpForTeamsSite && + !hasSiteCustomDomains && + !isDomainCreditAvailable + } + + fun hideCard(siteId: Long) { + appPrefsWrapper.setShouldHideDashboardDomainCard(siteId, true) + } + + fun trackDashboardCardDomainShown(scope: CoroutineScope, siteSelected: SiteSelected?) { + // cancel any existing job (debouncing mechanism) + dashboardUpdateDebounceJob?.cancel() + + dashboardUpdateDebounceJob = scope.launch(bgDispatcher) { + val isVisible = siteSelected + ?.dashboardCardsAndItems + ?.filterIsInstance() + ?.firstOrNull() + ?.cards + ?.any { + card -> card is DashboardDomainCard + } ?: false + + // add a delay (debouncing mechanism) + delay(CARD_VISIBLE_DEBOUNCE) + + domainCardVisible.set(isVisible) + if (isVisible && waitingToTrack.getAndSet(false)) { + trackDomainCardShown(positionIndex(siteSelected)) + } + }.also { + it.invokeOnCompletion { cause -> + // only set the job to null if it wasn't cancelled since cancellation is part of debouncing + if (cause == null) dashboardUpdateDebounceJob = null + } + } + } + + fun onResume(currentTab: MySiteTabType, siteSelected: SiteSelected?) { + if (currentTab == MySiteTabType.DASHBOARD) { + onDashboardRefreshed(siteSelected) + } else { + // moved away from dashboard, no longer waiting to track + waitingToTrack.set(false) + } + } + + fun onSiteChanged(siteId: Int?, siteSelected: SiteSelected?) { + if (currentSite.getAndSet(siteId) != siteId) { + domainCardVisible.set(null) + onDashboardRefreshed(siteSelected) + } + } + + fun trackDashboardCardDomainTapped(siteSelected: SiteSelected?) { + analyticsTrackerWrapper.track( + AnalyticsTracker.Stat.DASHBOARD_CARD_DOMAIN_TAPPED, + mapOf(POSITION_INDEX to positionIndex(siteSelected)) + ) + } + + fun trackDashboardCardDomainMoreMenuTapped(siteSelected: SiteSelected?) { + analyticsTrackerWrapper.track( + AnalyticsTracker.Stat.DASHBOARD_CARD_DOMAIN_MORE_MENU_TAPPED, + mapOf(POSITION_INDEX to positionIndex(siteSelected)) + ) + } + fun trackDashboardCardDomainHiddenByUser(siteSelected: SiteSelected?) { + analyticsTrackerWrapper.track( + AnalyticsTracker.Stat.DASHBOARD_CARD_DOMAIN_HIDDEN, + mapOf(POSITION_INDEX to positionIndex(siteSelected)) + ) + } + + private fun trackDomainCardShown(positionIndex: Int) { + analyticsTrackerWrapper.track( + AnalyticsTracker.Stat.DASHBOARD_CARD_DOMAIN_SHOWN, + mapOf(POSITION_INDEX to positionIndex) + ) + } + + private fun isDashboardCardDomainHiddenByUser(siteId: Long): Boolean { + return appPrefsWrapper.getShouldHideDashboardDomainCard(siteId) + } + + private fun isDashboardCardDomainEnabled(): Boolean { + return buildConfigWrapper.isJetpackApp && + dashboardCardDomainFeatureConfig.isEnabled() + } + + private fun positionIndex(siteSelected: SiteSelected?): Int { + return siteSelected + ?.dashboardCardsAndItems + ?.filterIsInstance() + ?.firstOrNull() + ?.cards + ?.indexOfFirst { + it is DashboardDomainCard + } ?: -1 + } + + private fun onDashboardRefreshed(siteSelected: SiteSelected?) { + domainCardVisible.get()?.let { isVisible -> + if (isVisible) trackDomainCardShown(positionIndex(siteSelected)) + waitingToTrack.set(false) + } ?: run { + waitingToTrack.set(true) + } + } + + fun hasCustomDomain(domains: List?) = domains?.any { !it.wpcomDomain } == true + + companion object { + const val POSITION_INDEX = "position_index" + private const val CARD_VISIBLE_DEBOUNCE = 500L + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardDomainCardBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardDomainCardBuilder.kt new file mode 100644 index 000000000000..4e85aeb5d0d2 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardDomainCardBuilder.kt @@ -0,0 +1,24 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.domain + +import org.wordpress.android.R +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.DashboardDomainCard +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardDomainBuilderParams +import org.wordpress.android.ui.utils.ListItemInteraction +import org.wordpress.android.ui.utils.UiString +import javax.inject.Inject + +class DashboardDomainCardBuilder @Inject constructor() { + fun build(params: DashboardCardDomainBuilderParams):DashboardDomainCard? { + return if (params.isEligible) { + DashboardDomainCard( + title = UiString.UiStringRes(R.string.dashboard_card_domain_title), + subtitle = UiString.UiStringRes(R.string.dashboard_card_domain_sub_title), + onClick = ListItemInteraction.create(params.onClick), + onHideMenuItemClick = ListItemInteraction.create(params.onHideMenuItemClick), + onMoreMenuClick = ListItemInteraction.create(params.onMoreMenuClick) + ) + } else { + null + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardDomainCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardDomainCardViewHolder.kt new file mode 100644 index 000000000000..74b569a12033 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardDomainCardViewHolder.kt @@ -0,0 +1,51 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.domain + +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu +import org.wordpress.android.R +import org.wordpress.android.databinding.DashboardCardDomainBinding +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.DashboardDomainCard +import org.wordpress.android.ui.mysite.cards.dashboard.CardViewHolder +import org.wordpress.android.ui.utils.ListItemInteraction +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.extensions.viewBinding + +class DashboardDomainCardViewHolder( + parent: ViewGroup, + private val uiHelpers: UiHelpers +) : CardViewHolder( + parent.viewBinding(DashboardCardDomainBinding::inflate) +) { + fun bind(card: DashboardDomainCard) = with(binding) { + uiHelpers.setTextOrHide(dashboardCardDomainTitle, card.title) + dashboardCardDomainCta.setOnClickListener { card.onClick.click() } + dashboardDomainCardMore.setOnClickListener { + showMoreMenu( + card.onHideMenuItemClick, + card.onMoreMenuClick, + dashboardDomainCardMore, + ) + } + } + + private fun showMoreMenu( + onHideMenuItemClick: ListItemInteraction, + onMoreMenuClick: ListItemInteraction, + anchor: View + ) { + onMoreMenuClick.click() + val popupMenu = PopupMenu(itemView.context, anchor) + popupMenu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.dashboard_card_domain_menu_item_hide_this -> { + onHideMenuItemClick.click() + return@setOnMenuItemClickListener true + } + else -> return@setOnMenuItemClickListener true + } + } + popupMenu.inflate(R.menu.dashboard_card_domain_menu) + popupMenu.show() + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesCardBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesCardBuilder.kt new file mode 100644 index 000000000000..8313b545eb9d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesCardBuilder.kt @@ -0,0 +1,76 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.pages + +import org.wordpress.android.R +import org.wordpress.android.fluxc.model.dashboard.CardModel.PagesCardModel +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PagesCard +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PagesCard.PagesCardWithData.CreatNewPageItem +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PagesCard.PagesCardWithData.PageContentItem +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.PagesCardBuilderParams +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.util.config.DashboardCardPagesConfig +import javax.inject.Inject + +private const val REQUIRED_PAGES_IN_CARD: Int = 3 + +class PagesCardBuilder @Inject constructor( + private val dashboardCardPagesConfig: DashboardCardPagesConfig +) { + fun build(params: PagesCardBuilderParams): PagesCard? { + if (!dashboardCardPagesConfig.isEnabled()) { + return null + } + return convertToPagesItems(params) + } + + private fun convertToPagesItems(params: PagesCardBuilderParams): PagesCard.PagesCardWithData { + val pages = params.pageCard?.pages + val content = pages?.let { getPagesContentItems(pages) } ?: emptyList() + val createPageCard = getCreatePageCard(params) + return PagesCard.PagesCardWithData( + title = UiString.UiStringRes(R.string.dashboard_pages_card_title), + pages = content, + footerLink = createPageCard + ) + } + + @Suppress("UNUSED_PARAMETER") + private fun getPagesContentItems(pages: List): List { + return emptyList() + } + + private fun getCreatePageCard(params: PagesCardBuilderParams): CreatNewPageItem { + // Create new page button is shown with image if there is + // less than three pages for a user + val pages = params.pageCard?.pages ?: emptyList() + return if (pages.isEmpty()) { + createNewPageCardWithAddPageMessage(params) + } else if (pages.size < REQUIRED_PAGES_IN_CARD) { + createNewPageCardWithAddAnotherPageMessage(params) + } else { + createNewPageCardWithOnlyButton(params) + } + } + + private fun createNewPageCardWithAddPageMessage(params: PagesCardBuilderParams): CreatNewPageItem { + return CreatNewPageItem( + label = UiString.UiStringRes(R.string.dashboard_pages_card_create_another_page_button), + description = UiString.UiStringRes(R.string.dashboard_pages_card_create_another_page_description), + onClick = params.onFooterLinkClick + ) + } + + private fun createNewPageCardWithAddAnotherPageMessage(params: PagesCardBuilderParams): CreatNewPageItem { + return CreatNewPageItem( + label = UiString.UiStringRes(R.string.dashboard_pages_card_create_another_page_button), + description = UiString.UiStringRes(R.string.dashboard_pages_card_create_another_page_description), + onClick = params.onFooterLinkClick + ) + } + + private fun createNewPageCardWithOnlyButton(params: PagesCardBuilderParams): CreatNewPageItem { + return CreatNewPageItem( + label = UiString.UiStringRes(R.string.dashboard_pages_card_no_pages_create_page_button), + onClick = params.onFooterLinkClick + ) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesCardContentType.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesCardContentType.kt new file mode 100644 index 000000000000..301f0f394f7c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesCardContentType.kt @@ -0,0 +1,8 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.pages + +// this class represents the type of pages card that will be displayed +enum class PagesCardContentType(val status: String) { + DRAFT("draft"), + PUBLISHED("published"), + SCHEDULED("scheduled") +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesCardViewHolder.kt new file mode 100644 index 000000000000..0c102d7b1fc2 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesCardViewHolder.kt @@ -0,0 +1,32 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.pages + +import android.view.ViewGroup +import org.wordpress.android.databinding.MySiteCardToolbarBinding +import org.wordpress.android.databinding.MySitePagesCardWithPageItemsBinding +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PagesCard +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PagesCard.PagesCardWithData +import org.wordpress.android.ui.mysite.cards.dashboard.CardViewHolder +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.util.extensions.viewBinding + +class PagesCardViewHolder( + parent: ViewGroup, + private val uiHelpers: UiHelpers +) : CardViewHolder( + parent.viewBinding(MySitePagesCardWithPageItemsBinding::inflate) +) { + init { + binding.pagesItems.adapter = PagesItemsAdapter(uiHelpers) + } + + fun bind(card: PagesCard) = with(binding) { + val pagesCard = card as PagesCardWithData + (pagesItems.adapter as PagesItemsAdapter).update(pagesCard.pages) + mySiteToolbar.update(pagesCard.title) + } + + private fun MySiteCardToolbarBinding.update(title: UiString?) { + uiHelpers.setTextOrHide(mySiteCardToolbarTitle, title) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesItemViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesItemViewHolder.kt new file mode 100644 index 000000000000..620cf0431647 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesItemViewHolder.kt @@ -0,0 +1,19 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.pages + +import android.view.ViewGroup +import org.wordpress.android.databinding.PagesItemBinding +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PagesCard.PagesCardWithData.PageContentItem +import org.wordpress.android.ui.mysite.MySiteCardAndItemViewHolder +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.extensions.viewBinding + +class PagesItemViewHolder( + parent: ViewGroup, + private val uiHelpers: UiHelpers +) : MySiteCardAndItemViewHolder( + parent.viewBinding(PagesItemBinding::inflate) +) { + fun bind(item: PageContentItem) = with(binding) { + uiHelpers.setTextOrHide(title, item.title) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesItemsAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesItemsAdapter.kt new file mode 100644 index 000000000000..bd84412a4a90 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/pages/PagesItemsAdapter.kt @@ -0,0 +1,52 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.pages + +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.DiffUtil.Callback +import androidx.recyclerview.widget.RecyclerView.Adapter +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PagesCard.PagesCardWithData.PageContentItem + +class PagesItemsAdapter( + private val uiHelpers: UiHelpers +) : Adapter() { + private val items = mutableListOf() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = PagesItemViewHolder( + parent, + uiHelpers + ) + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: PagesItemViewHolder, position: Int) { + holder.bind(items[position]) + } + + fun update(newItems: List) { + val diffResult = DiffUtil.calculateDiff(PagesItemDiffUtil(items, newItems)) + items.clear() + items.addAll(newItems) + diffResult.dispatchUpdatesTo(this) + } + + class PagesItemDiffUtil( + private val oldList: List, + private val newList: List + ) : Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val newItem = newList[newItemPosition] + val oldItem = oldList[oldItemPosition] + + return (oldItem == newItem) + } + + override fun getOldListSize(): Int = oldList.size + + override fun getNewListSize(): Int = newList.size + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean = oldList[oldItemPosition] == newList[newItemPosition] + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/jpfullplugininstall/JetpackInstallFullPluginCardBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/jpfullplugininstall/JetpackInstallFullPluginCardBuilder.kt index 5f3ce262096d..48c77fcfc985 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/jpfullplugininstall/JetpackInstallFullPluginCardBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/jpfullplugininstall/JetpackInstallFullPluginCardBuilder.kt @@ -6,8 +6,8 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.JetpackIns import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.utils.ListItemInteraction import org.wordpress.android.util.config.JetpackInstallFullPluginFeatureConfig -import org.wordpress.android.util.extensions.activeJetpackConnectionPluginNames -import org.wordpress.android.util.extensions.isJetpackConnectedWithoutFullPlugin +import org.wordpress.android.util.extensions.activeIndividualJetpackPluginNames +import org.wordpress.android.util.extensions.isJetpackIndividualPluginConnectedWithoutFullPlugin import javax.inject.Inject class JetpackInstallFullPluginCardBuilder @Inject constructor( @@ -19,7 +19,7 @@ class JetpackInstallFullPluginCardBuilder @Inject constructor( ): JetpackInstallFullPluginCard? = if (shouldShowCard(params.site)) { JetpackInstallFullPluginCard( siteName = params.site.name, - pluginNames = params.site.activeJetpackConnectionPluginNames().orEmpty(), + pluginNames = params.site.activeIndividualJetpackPluginNames().orEmpty(), onLearnMoreClick = ListItemInteraction.create(params.onLearnMoreClick), onHideMenuItemClick = ListItemInteraction.create(params.onHideMenuItemClick), ) @@ -29,6 +29,6 @@ class JetpackInstallFullPluginCardBuilder @Inject constructor( return site.id != 0 && installFullPluginFeatureConfig.isEnabled() && !appPrefsWrapper.getShouldHideJetpackInstallFullPluginCard(site.id) && - site.isJetpackConnectedWithoutFullPlugin() + site.isJetpackIndividualPluginConnectedWithoutFullPlugin() } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/jpfullplugininstall/JetpackInstallFullPluginCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/jpfullplugininstall/JetpackInstallFullPluginCardViewHolder.kt index de38eac74dd6..316542831f69 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/jpfullplugininstall/JetpackInstallFullPluginCardViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/jpfullplugininstall/JetpackInstallFullPluginCardViewHolder.kt @@ -3,27 +3,29 @@ package org.wordpress.android.ui.mysite.cards.jpfullplugininstall import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.PopupMenu +import androidx.compose.ui.res.stringResource import org.wordpress.android.R import org.wordpress.android.databinding.JpInstallFullPluginCardBinding import org.wordpress.android.ui.compose.theme.AppTheme -import org.wordpress.android.ui.jpfullplugininstall.onboarding.compose.component.PluginDescription +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.compose.component.PluginDescription import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.JetpackInstallFullPluginCard import org.wordpress.android.ui.mysite.MySiteCardAndItemViewHolder import org.wordpress.android.ui.utils.ListItemInteraction -import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.extensions.viewBinding class JetpackInstallFullPluginCardViewHolder( parent: ViewGroup, - private val uiHelpers: UiHelpers, ) : MySiteCardAndItemViewHolder( parent.viewBinding(JpInstallFullPluginCardBinding::inflate) ) { fun bind(card: JetpackInstallFullPluginCard) = with(binding) { jpInstallFullPluginCardContentComposable.setContent { AppTheme { - uiHelpers::class.java - PluginDescription(siteName = card.siteName, pluginNames = card.pluginNames) + PluginDescription( + siteString = stringResource(R.string.jetpack_full_plugin_install_onboarding_description_this_site), + pluginNames = card.pluginNames, + useConciseText = true, + ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/quicklinksribbon/QuickLinkRibbonViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/quicklinksribbon/QuickLinkRibbonViewHolder.kt index 0bd36bd70177..2f899c5a1260 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/quicklinksribbon/QuickLinkRibbonViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/quicklinksribbon/QuickLinkRibbonViewHolder.kt @@ -66,14 +66,14 @@ class QuickLinkRibbonViewHolder( * We need to do this immediately, because if we don't, then the next move event could potentially * trigger the viewPager to switch tabs */ - override fun onDown(e: MotionEvent?): Boolean = with(binding) { + override fun onDown(e: MotionEvent): Boolean = with(binding) { quickLinkRibbonItemList.parent.requestDisallowInterceptTouchEvent(true) return super.onDown(e) } override fun onScroll( - e1: MotionEvent?, - e2: MotionEvent?, + e1: MotionEvent, + e2: MotionEvent, distanceX: Float, distanceY: Float ): Boolean = with(binding) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/dynamiccards/quickstart/QuickStartDynamicCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/dynamiccards/quickstart/QuickStartDynamicCardViewHolder.kt index 8be75b0c0ba0..c78f9b01b5c5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/dynamiccards/quickstart/QuickStartDynamicCardViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/dynamiccards/quickstart/QuickStartDynamicCardViewHolder.kt @@ -22,6 +22,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.DynamicCard.QuickStartD import org.wordpress.android.ui.mysite.MySiteCardAndItemViewHolder import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.ColorUtils +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.extensions.viewBinding private const val Y_BUFFER = 10 @@ -90,7 +91,7 @@ class QuickStartDynamicCardViewHolder( private fun restoreScrollState(recyclerView: RecyclerView, key: String) { recyclerView.layoutManager?.apply { - val scrollState = nestedScrollStates.getParcelable(key) + val scrollState = nestedScrollStates.getParcelableCompat(key) if (scrollState != null) { onRestoreInstanceState(scrollState) } else { @@ -127,14 +128,14 @@ class QuickStartDynamicCardViewHolder( * We need to do this immediately, because if we don't, then the next move event could potentially * trigger the viewPager to switch tabs */ - override fun onDown(e: MotionEvent?): Boolean = with(binding) { + override fun onDown(e: MotionEvent): Boolean = with(binding) { quickStartCardRecyclerView.parent.requestDisallowInterceptTouchEvent(true) return super.onDown(e) } override fun onScroll( - e1: MotionEvent?, - e2: MotionEvent?, + e1: MotionEvent, + e2: MotionEvent, distanceX: Float, distanceY: Float ): Boolean = with(binding) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/jetpackbadge/JetpackPoweredBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/jetpackbadge/JetpackPoweredBottomSheetFragment.kt index 6945317669b4..40e52ccb47b2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/jetpackbadge/JetpackPoweredBottomSheetFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/jetpackbadge/JetpackPoweredBottomSheetFragment.kt @@ -14,8 +14,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R -import org.wordpress.android.R.raw -import org.wordpress.android.R.string import org.wordpress.android.databinding.JetpackPoweredBottomSheetBinding import org.wordpress.android.databinding.JetpackPoweredExpandedBottomSheetBinding import org.wordpress.android.ui.ActivityLauncherWrapper @@ -27,6 +25,7 @@ import org.wordpress.android.ui.main.WPMainNavigationView.PageType.READER import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredDialogAction.DismissDialog import org.wordpress.android.ui.mysite.jetpackbadge.JetpackPoweredDialogAction.OpenPlayStore import org.wordpress.android.util.extensions.exhaustive +import org.wordpress.android.util.extensions.getSerializableCompat import javax.inject.Inject @AndroidEntryPoint @@ -85,27 +84,27 @@ class JetpackPoweredBottomSheetFragment : BottomSheetDialogFragment() { private fun setupFullScreenViews(view: View) { with(JetpackPoweredExpandedBottomSheetBinding.bind(view)) { - when (arguments?.getSerializable(KEY_SITE_SCREEN) as? PageType ?: MY_SITE) { + when (arguments?.getSerializableCompat(KEY_SITE_SCREEN) ?: MY_SITE) { MY_SITE -> { - val animRes = if (rtlLayout(view)) raw.jp_stats_rtl else raw.jp_stats_left + val animRes = if (rtlLayout(view)) R.raw.jp_stats_rtl else R.raw.jp_stats_left illustrationView.setAnimation(animRes) - title.text = getString(string.wp_jetpack_powered_stats_powered_by_jetpack) - caption.text = getString(string.wp_jetpack_powered_stats_powered_by_jetpack_caption) - secondaryButton.text = getString(string.wp_jetpack_continue_to_stats) + title.text = getString(R.string.wp_jetpack_powered_stats_powered_by_jetpack) + caption.text = getString(R.string.wp_jetpack_powered_stats_powered_by_jetpack_caption) + secondaryButton.text = getString(R.string.wp_jetpack_continue_to_stats) } READER -> { - val animRes = if (rtlLayout(view)) raw.jp_reader_rtl else raw.jp_reader_left + val animRes = if (rtlLayout(view)) R.raw.jp_reader_rtl else R.raw.jp_reader_left illustrationView.setAnimation(animRes) - title.text = getString(string.wp_jetpack_powered_reader_powered_by_jetpack) - caption.text = getString(string.wp_jetpack_powered_reader_powered_by_jetpack_caption) - secondaryButton.text = getString(string.wp_jetpack_continue_to_reader) + title.text = getString(R.string.wp_jetpack_powered_reader_powered_by_jetpack) + caption.text = getString(R.string.wp_jetpack_powered_reader_powered_by_jetpack_caption) + secondaryButton.text = getString(R.string.wp_jetpack_continue_to_reader) } NOTIFS -> { - val animRes = if (rtlLayout(view)) raw.jp_notifications_rtl else raw.jp_notifications_left + val animRes = if (rtlLayout(view)) R.raw.jp_notifications_rtl else R.raw.jp_notifications_left illustrationView.setAnimation(animRes) - title.text = getString(string.wp_jetpack_powered_notifications_powered_by_jetpack) - caption.text = getString(string.wp_jetpack_powered_notifications_powered_by_jetpack_caption) - secondaryButton.text = getString(string.wp_jetpack_continue_to_notifications) + title.text = getString(R.string.wp_jetpack_powered_notifications_powered_by_jetpack) + caption.text = getString(R.string.wp_jetpack_powered_notifications_powered_by_jetpack_caption) + secondaryButton.text = getString(R.string.wp_jetpack_continue_to_notifications) } } primaryButton.setOnClickListener { viewModel.openJetpackAppDownloadLink() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/tabs/MySiteTabFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/tabs/MySiteTabFragment.kt index 3bfa3ba7352f..3870005d8ac9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/tabs/MySiteTabFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/tabs/MySiteTabFragment.kt @@ -34,9 +34,10 @@ import org.wordpress.android.ui.TextInputDialogFragment import org.wordpress.android.ui.accounts.LoginEpilogueActivity import org.wordpress.android.ui.domains.DomainRegistrationActivity.Companion.RESULT_REGISTERED_DOMAIN_EMAIL import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistrationPurpose.CTA_DOMAIN_CREDIT_REDEMPTION +import org.wordpress.android.ui.domains.DomainRegistrationActivity.DomainRegistrationPurpose.DOMAIN_PURCHASE import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayFragment import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureCollectionOverlaySource -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingDialogFragment +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingDialogFragment import org.wordpress.android.ui.main.SitePickerActivity import org.wordpress.android.ui.main.WPMainActivity import org.wordpress.android.ui.main.jetpack.migration.JetpackMigrationActivity @@ -87,6 +88,7 @@ import org.wordpress.android.util.SnackbarSequencer import org.wordpress.android.util.UriWrapper import org.wordpress.android.util.WPSwipeToRefreshHelper.buildSwipeToRefreshHelper import org.wordpress.android.util.extensions.getColorFromAttribute +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.extensions.setVisible import org.wordpress.android.util.helpers.SwipeToRefreshHelper import org.wordpress.android.util.image.ImageManager @@ -398,6 +400,12 @@ class MySiteTabFragment : Fragment(R.layout.my_site_tab_fragment), action.site, CTA_DOMAIN_CREDIT_REDEMPTION ) + is SiteNavigationAction.OpenPaidDomainSearch -> ActivityLauncher.viewDomainRegistrationActivityForResult( + this, + action.site, + DOMAIN_PURCHASE + ) + is SiteNavigationAction.AddNewSite -> SitePickerActivity.addSite(activity, action.hasAccessToken, action.source) is SiteNavigationAction.ShowQuickStartDialog -> showQuickStartDialog( action.title, @@ -437,6 +445,15 @@ class MySiteTabFragment : Fragment(R.layout.my_site_tab_fragment), null, action.source ) + is SiteNavigationAction.ShowJetpackRemovalStaticPostersView -> { + ActivityLauncher.showJetpackStaticPoster(requireActivity()) + } + is SiteNavigationAction.OpenActivityLogDetail -> ActivityLauncher.viewActivityLogDetailFromDashboardCard( + activity, + action.site, + action.activityId, + action.isRewindable + ) } private fun showJetpackPoweredBottomSheet() { @@ -737,7 +754,7 @@ class MySiteTabFragment : Fragment(R.layout.my_site_tab_fragment), } override fun onConfirm(result: Bundle?) { - val task = result?.getSerializable(QuickStartFullScreenDialogFragment.RESULT_TASK) as? QuickStartTask + val task = result?.getSerializableCompat(QuickStartFullScreenDialogFragment.RESULT_TASK) as? QuickStartTask task?.let { viewModel.onQuickStartTaskCardClick(it) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/DismissNotificationReceiver.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/DismissNotificationReceiver.kt index 88d6f30af644..2a1125c4fabc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/DismissNotificationReceiver.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/DismissNotificationReceiver.kt @@ -7,6 +7,7 @@ import androidx.core.app.NotificationManagerCompat import org.wordpress.android.WordPress import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.util.extensions.getSerializableExtraCompat import javax.inject.Inject class DismissNotificationReceiver : BroadcastReceiver() { @@ -21,7 +22,7 @@ class DismissNotificationReceiver : BroadcastReceiver() { } private fun trackAnalyticsEvent(intent: Intent) { - val stat = intent.getSerializableExtra(EXTRA_STAT_TO_TRACK) as Stat? + val stat = intent.getSerializableExtraCompat(EXTRA_STAT_TO_TRACK) if (stat != null) { analyticsTrackerWrapper.track(stat) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java index 5d305470d934..faee35b34051 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailActivity.java @@ -9,6 +9,7 @@ import android.view.WindowManager; import android.widget.ProgressBar; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -57,13 +58,14 @@ import org.wordpress.android.ui.reader.comments.ThreadedCommentsActionSource; import org.wordpress.android.ui.reader.tracker.ReaderTracker; import org.wordpress.android.ui.stats.StatsViewType; -import org.wordpress.android.util.extensions.AppBarLayoutExtensionsKt; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; import org.wordpress.android.util.analytics.AnalyticsUtils.AnalyticsCommentActionSource; import org.wordpress.android.util.config.LikesEnhancementsFeatureConfig; +import org.wordpress.android.util.extensions.AppBarLayoutExtensionsKt; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import org.wordpress.android.widgets.WPSwipeSnackbar; import org.wordpress.android.widgets.WPViewPager; import org.wordpress.android.widgets.WPViewPagerTransformer; @@ -74,14 +76,14 @@ import javax.inject.Inject; -import dagger.hilt.android.AndroidEntryPoint; - import static org.wordpress.android.models.Note.NOTE_COMMENT_LIKE_TYPE; import static org.wordpress.android.models.Note.NOTE_COMMENT_TYPE; import static org.wordpress.android.models.Note.NOTE_FOLLOW_TYPE; import static org.wordpress.android.models.Note.NOTE_LIKE_TYPE; import static org.wordpress.android.ui.notifications.services.NotificationsUpdateServiceStarter.IS_TAPPED_ON_NOTIFICATION; +import dagger.hilt.android.AndroidEntryPoint; + @AndroidEntryPoint public class NotificationsDetailActivity extends LocaleAwareActivity implements CommentActions.OnNoteCommentActionListener, @@ -105,18 +107,6 @@ public class NotificationsDetailActivity extends LocaleAwareActivity implements private AppBarLayout mAppBarLayout; private Toolbar mToolbar; - @Override - public void onBackPressed() { - CollapseFullScreenDialogFragment fragment = (CollapseFullScreenDialogFragment) - getSupportFragmentManager().findFragmentByTag(CollapseFullScreenDialogFragment.TAG); - - if (fragment != null) { - fragment.onBackPressed(); - } else { - super.onBackPressed(); - } - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -125,6 +115,21 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.notifications_detail_activity); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + CollapseFullScreenDialogFragment fragment = (CollapseFullScreenDialogFragment) + getSupportFragmentManager().findFragmentByTag(CollapseFullScreenDialogFragment.TAG); + + if (fragment != null) { + fragment.collapse(); + } else { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + mToolbar = findViewById(R.id.toolbar_main); setSupportActionBar(mToolbar); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.kt index 4fa1055660b9..15a310182506 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListFragment.kt @@ -2,8 +2,10 @@ package org.wordpress.android.ui.notifications +import android.Manifest import android.app.Activity import android.content.Intent +import android.os.Build import android.os.Bundle import android.text.TextUtils import android.view.Menu @@ -13,6 +15,7 @@ import android.view.View import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.core.text.HtmlCompat +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.recyclerview.widget.RecyclerView @@ -30,6 +33,7 @@ import org.wordpress.android.analytics.AnalyticsTracker.NOTIFICATIONS_SELECTED_F import org.wordpress.android.analytics.AnalyticsTracker.Stat.NOTIFICATION_TAPPED_SEGMENTED_CONTROL import org.wordpress.android.databinding.NotificationsListFragmentBinding import org.wordpress.android.fluxc.store.AccountStore +import org.wordpress.android.models.JetpackPoweredScreen import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.JetpackConnectionSource.NOTIFICATIONS import org.wordpress.android.ui.JetpackConnectionWebViewActivity @@ -54,8 +58,10 @@ import org.wordpress.android.ui.notifications.services.NotificationsUpdateServic import org.wordpress.android.ui.stats.StatsConnectJetpackActivity import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.JetpackBrandingUtils -import org.wordpress.android.models.JetpackPoweredScreen import org.wordpress.android.util.NetworkUtils +import org.wordpress.android.util.PermissionUtils +import org.wordpress.android.util.WPPermissionUtils +import org.wordpress.android.util.WPPermissionUtils.NOTIFICATIONS_PERMISSION_REQUEST_CODE import org.wordpress.android.util.WPUrlUtils import org.wordpress.android.util.extensions.setLiftOnScrollTargetViewIdAndRequestLayout import org.wordpress.android.viewmodel.observeEvent @@ -174,6 +180,7 @@ class NotificationsListFragment : Fragment(R.layout.notifications_list_fragment) } } setSelectedTab(lastTabPosition) + setNotificationPermissionWarning() } viewModel.onResume() } @@ -202,6 +209,57 @@ class NotificationsListFragment : Fragment(R.layout.notifications_list_fragment) tabLayout.getTabAt(lastTabPosition)?.select() } + private fun NotificationsListFragmentBinding.setNotificationPermissionWarning() { + val hasPermission = PermissionUtils.checkNotificationsPermission(activity) + if (hasPermission) { + // If the permissions is granted, we should reset the state of the warning. Because the permission may be + // disabled later, then we should be able to show the warning again to inform the user. + viewModel.resetNotificationsPermissionWarningDismissState() + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || hasPermission || + viewModel.isNotificationsPermissionsWarningDismissed + ) { + // If the user dismissed the warning, don't show it again. + notificationPermissionWarning.isVisible = false + } else { + notificationPermissionWarning.isVisible = true + notificationPermissionWarning.setOnClickListener { + val isAlwaysDenied = WPPermissionUtils.isPermissionAlwaysDenied( + requireActivity(), + Manifest.permission.POST_NOTIFICATIONS + ) + if (isAlwaysDenied) { + NotificationsPermissionBottomSheetFragment().show( + parentFragmentManager, + NotificationsPermissionBottomSheetFragment.TAG + ) + } else { + requestPermissions( + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + NOTIFICATIONS_PERMISSION_REQUEST_CODE + ) + } + } + permissionDismissButton.setOnClickListener { + notificationPermissionWarning.isVisible = false + viewModel.onNotificationsPermissionWarningDismissed() + } + } + } + + @Suppress("OVERRIDE_DEPRECATION") + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + WPPermissionUtils.setPermissionListAsked( + requireActivity(), + requestCode, + permissions, + grantResults, + false + ) + viewModel.resetNotificationsPermissionWarningDismissState() + } + private fun NotificationsListFragmentBinding.showConnectJetpackView() { clearToolbarScrollFlags() jetpackSetup.setOnClickListener { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt index 12d853754376..f9f8b14113a9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsListViewModel.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.CoroutineDispatcher import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil import org.wordpress.android.ui.jetpackoverlay.JetpackOverlayConnectedFeature.NOTIFICATIONS +import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ScopedViewModel @@ -16,6 +17,7 @@ import javax.inject.Named @HiltViewModel class NotificationsListViewModel @Inject constructor( @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, + private val appPrefsWrapper: AppPrefsWrapper, private val jetpackBrandingUtils: JetpackBrandingUtils, private val jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil @@ -26,6 +28,9 @@ class NotificationsListViewModel @Inject constructor( private val _showJetpackOverlay = MutableLiveData>() val showJetpackOverlay: LiveData> = _showJetpackOverlay + val isNotificationsPermissionsWarningDismissed + get() = appPrefsWrapper.notificationPermissionsWarningDismissed + init { if (jetpackBrandingUtils.shouldShowJetpackPoweredBottomSheet()) showJetpackPoweredBottomSheet() } @@ -42,4 +47,12 @@ class NotificationsListViewModel @Inject constructor( private fun showJetpackOverlay() { _showJetpackOverlay.value = Event(true) } + + fun onNotificationsPermissionWarningDismissed() { + appPrefsWrapper.notificationPermissionsWarningDismissed = true + } + + fun resetNotificationsPermissionWarningDismissState() { + appPrefsWrapper.notificationPermissionsWarningDismissed = false + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsPermissionBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsPermissionBottomSheetFragment.kt new file mode 100644 index 000000000000..9ba6f79f6cf2 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsPermissionBottomSheetFragment.kt @@ -0,0 +1,39 @@ +package org.wordpress.android.ui.notifications + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.wordpress.android.R +import org.wordpress.android.databinding.NotificationsPermissionBottomSheetBinding +import org.wordpress.android.util.WPPermissionUtils + +class NotificationsPermissionBottomSheetFragment : BottomSheetDialogFragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.notifications_permission_bottom_sheet, container) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + (dialog as? BottomSheetDialog)?.behavior?.state = BottomSheetBehavior.STATE_EXPANDED + with(NotificationsPermissionBottomSheetBinding.bind(view)) { + val appName = getString(R.string.app_name) + description.text = getString(R.string.notifications_permission_bottom_sheet_description_2, appName) + + primaryButton.setOnClickListener { + WPPermissionUtils.showNotificationsSettings(requireActivity()) + dismiss() + } + } + } + + companion object { + const val TAG = "NOTIFICATIONS_PERMISSION_BOTTOM_SHEET_FRAGMENT" + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageListFragment.kt index 98709e268b61..3f1c488ce4b7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageListFragment.kt @@ -17,6 +17,8 @@ import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.DisplayUtils import org.wordpress.android.util.QuickStartUtilsWrapper import org.wordpress.android.util.SnackbarSequencer +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.pages.PageListViewModel import org.wordpress.android.viewmodel.pages.PageListViewModel.PageListType @@ -88,11 +90,13 @@ class PageListFragment : ViewPagerFragment(R.layout.pages_list_fragment) { } private fun PagesListFragmentBinding.initializeViewModels(activity: FragmentActivity) { - val pagesViewModel = ViewModelProvider(activity, viewModelFactory).get(PagesViewModel::class.java) + val pagesViewModel = ViewModelProvider(activity, viewModelFactory)[PagesViewModel::class.java] - val listType = arguments?.getSerializable(typeKey) as PageListType - viewModel = ViewModelProvider(this@PageListFragment, viewModelFactory) - .get(listType.name, PageListViewModel::class.java) + val listType = requireNotNull(arguments?.getSerializableCompat(typeKey)) + viewModel = ViewModelProvider( + this@PageListFragment, + viewModelFactory + )[listType.name, PageListViewModel::class.java] viewModel.start(listType, pagesViewModel) @@ -101,7 +105,7 @@ class PageListFragment : ViewPagerFragment(R.layout.pages_list_fragment) { private fun PagesListFragmentBinding.initializeViews(savedInstanceState: Bundle?) { val layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) - savedInstanceState?.getParcelable(listStateKey)?.let { + savedInstanceState?.getParcelableCompat(listStateKey)?.let { layoutManager.onRestoreInstanceState(it) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageParentFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageParentFragment.kt index cdeb85212eab..02628c31f887 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageParentFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageParentFragment.kt @@ -25,6 +25,8 @@ import org.wordpress.android.WordPress import org.wordpress.android.databinding.PageParentFragmentBinding import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.util.DisplayUtils +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.viewmodel.pages.PageParentViewModel import org.wordpress.android.widgets.RecyclerItemDecoration import javax.inject.Inject @@ -60,7 +62,7 @@ class PageParentFragment : Fragment(R.layout.page_parent_fragment), MenuProvider override fun onMenuItemSelected(menuItem: MenuItem) = when (menuItem.itemId) { android.R.id.home -> { - activity?.onBackPressed() + activity?.onBackPressedDispatcher?.onBackPressed() true } R.id.save_parent -> { @@ -87,17 +89,17 @@ class PageParentFragment : Fragment(R.layout.page_parent_fragment), MenuProvider result.putExtra(EXTRA_PAGE_REMOTE_ID_KEY, pageId) result.putExtra(EXTRA_PAGE_PARENT_ID_KEY, viewModel.currentParent.id) activity?.setResult(Activity.RESULT_OK, result) - activity?.onBackPressed() + activity?.onBackPressedDispatcher?.onBackPressed() } private fun PageParentFragmentBinding.initializeSearchView() { searchAction.setOnActionExpandListener(object : OnActionExpandListener { - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { viewModel.onSearchExpanded(restorePreviousSearch) return true } - override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { viewModel.onSearchCollapsed() return true } @@ -121,8 +123,8 @@ class PageParentFragment : Fragment(R.layout.page_parent_fragment), MenuProvider } }) - val searchEditFrame = searchAction.actionView.findViewById(R.id.search_edit_frame) - (searchEditFrame.layoutParams as LinearLayout.LayoutParams) + val searchEditFrame = searchAction.actionView?.findViewById(R.id.search_edit_frame) + (searchEditFrame?.layoutParams as LinearLayout.LayoutParams) .apply { this.leftMargin = DisplayUtils.dpToPx(activity, SEARCH_ACTION_LEFT_MARGIN_DP) } viewModel.isSearchExpanded.observe(this@PageParentFragment) { @@ -158,7 +160,7 @@ class PageParentFragment : Fragment(R.layout.page_parent_fragment), MenuProvider private fun PageParentFragmentBinding.initializeViews(activity: FragmentActivity, savedInstanceState: Bundle?) { val layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) - savedInstanceState?.getParcelable(listStateKey)?.let { + savedInstanceState?.getParcelableCompat(listStateKey)?.let { layoutManager.onRestoreInstanceState(it) } @@ -178,15 +180,13 @@ class PageParentFragment : Fragment(R.layout.page_parent_fragment), MenuProvider pageId: Long, isFirstStart: Boolean ) { - viewModel = ViewModelProvider(activity, viewModelFactory) - .get(PageParentViewModel::class.java) + viewModel = ViewModelProvider(activity, viewModelFactory)[PageParentViewModel::class.java] setupObservers() if (isFirstStart) { - val site = activity.intent?.getSerializableExtra(WordPress.SITE) as SiteModel? - val nonNullSite = checkNotNull(site) - viewModel.start(nonNullSite, pageId) + val site = requireNotNull(activity.intent?.getSerializableExtraCompat(WordPress.SITE)) + viewModel.start(site, pageId) } else { restorePreviousSearch = true } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageParentSearchFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageParentSearchFragment.kt index 6dd9857a8ee6..43fad8ba2caa 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/PageParentSearchFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PageParentSearchFragment.kt @@ -16,6 +16,7 @@ import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.databinding.PagesListFragmentBinding import org.wordpress.android.util.DisplayUtils +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.viewmodel.pages.PageParentSearchViewModel import org.wordpress.android.viewmodel.pages.PageParentViewModel import org.wordpress.android.widgets.RecyclerItemDecoration @@ -71,7 +72,7 @@ class PageParentSearchFragment : Fragment(R.layout.pages_list_fragment), Corouti private fun PagesListFragmentBinding.initializeViews(savedInstanceState: Bundle?) { val layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) - savedInstanceState?.getParcelable(listStateKey)?.let { + savedInstanceState?.getParcelableCompat(listStateKey)?.let { layoutManager.onRestoreInstanceState(it) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesActivity.kt index 70caf8d0bbb8..f7a52adb5ece 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesActivity.kt @@ -12,6 +12,7 @@ import org.wordpress.android.ui.LocaleAwareActivity import org.wordpress.android.ui.notifications.SystemNotificationsTracker import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogNegativeClickInterface import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogPositiveClickInterface +import org.wordpress.android.util.extensions.getSerializableExtraCompat import javax.inject.Inject const val EXTRA_PAGE_REMOTE_ID_KEY = "extra_page_remote_id_key" @@ -39,8 +40,9 @@ class PagesActivity : LocaleAwareActivity(), private fun handleIntent(intent: Intent) { if (intent.hasExtra(ARG_NOTIFICATION_TYPE)) { - val notificationType: NotificationType = - intent.getSerializableExtra(ARG_NOTIFICATION_TYPE) as NotificationType + val notificationType = requireNotNull( + intent.getSerializableExtraCompat(ARG_NOTIFICATION_TYPE) + ) systemNotificationTracker.trackTappedNotification(notificationType) } @@ -54,7 +56,7 @@ class PagesActivity : LocaleAwareActivity(), override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt index 017f7b4b732e..fe8b382d9342 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt @@ -56,6 +56,8 @@ import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.DisplayUtils import org.wordpress.android.util.ToastUtils.Duration import org.wordpress.android.util.WPSwipeToRefreshHelper +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.util.extensions.redirectContextClickToLongPressListener import org.wordpress.android.util.extensions.setLiftOnScrollTargetViewIdAndRequestLayout import org.wordpress.android.util.helpers.SwipeToRefreshHelper @@ -265,12 +267,12 @@ class PagesFragment : Fragment(R.layout.pages_fragment), ScrollableViewInitializ private fun PagesFragmentBinding.initializeSearchView() { actionMenuItem.setOnActionExpandListener(object : OnActionExpandListener { - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { viewModel.onSearchExpanded(restorePreviousSearch) return true } - override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { viewModel.onSearchCollapsed() return true } @@ -295,17 +297,17 @@ class PagesFragment : Fragment(R.layout.pages_fragment), ScrollableViewInitializ }) // fix the search view margins to match the action bar - val searchEditFrame = actionMenuItem.actionView.findViewById(R.id.search_edit_frame) - (searchEditFrame.layoutParams as LinearLayout.LayoutParams) + val searchEditFrame = actionMenuItem.actionView?.findViewById(R.id.search_edit_frame) + (searchEditFrame?.layoutParams as LinearLayout.LayoutParams) .apply { this.leftMargin = DisplayUtils.dpToPx(activity, -8) } - viewModel.isSearchExpanded.observe(this@PagesFragment, Observer { + viewModel.isSearchExpanded.observe(this@PagesFragment) { if (it == true) { showSearchList(actionMenuItem) } else { hideSearchList(actionMenuItem) } - }) + } } private fun PagesFragmentBinding.initializeViewModelObservers( @@ -316,13 +318,15 @@ class PagesFragment : Fragment(R.layout.pages_fragment), ScrollableViewInitializ setupActions(activity) setupMlpObservers(activity) - val site = if (savedInstanceState == null) { - val nonNullIntent = checkNotNull(activity.intent) - nonNullIntent.getSerializableExtra(WordPress.SITE) as SiteModel - } else { - restorePreviousSearch = true - savedInstanceState.getSerializable(WordPress.SITE) as SiteModel - } + val site = requireNotNull( + if (savedInstanceState == null) { + val nonNullIntent = checkNotNull(activity.intent) + nonNullIntent.getSerializableExtraCompat(WordPress.SITE) + } else { + restorePreviousSearch = true + savedInstanceState.getSerializableCompat(WordPress.SITE) + } + ) viewModel.authorUIState.observe(activity, Observer { state -> state?.let { @@ -377,7 +381,7 @@ class PagesFragment : Fragment(R.layout.pages_fragment), ScrollableViewInitializ viewModel.createNewPage.observe(viewLifecycleOwner, { if (mlpViewModel.canShowModalLayoutPicker() - && !jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()) { + && jetpackFeatureRemovalPhaseHelper.shouldShowTemplateSelectionInPages()) { mlpViewModel.createPageFlowTriggered() } else { createNewPage() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/SearchListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/SearchListFragment.kt index d5ce0ee83b76..a22325336b8f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/SearchListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/SearchListFragment.kt @@ -14,6 +14,7 @@ import org.wordpress.android.WordPress import org.wordpress.android.databinding.PagesListFragmentBinding import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.DisplayUtils +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.viewmodel.pages.PagesViewModel import org.wordpress.android.viewmodel.pages.SearchListViewModel import org.wordpress.android.widgets.RecyclerItemDecoration @@ -66,7 +67,7 @@ class SearchListFragment : Fragment(R.layout.pages_list_fragment) { private fun PagesListFragmentBinding.initializeViews(savedInstanceState: Bundle?) { val layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) - savedInstanceState?.getParcelable(listStateKey)?.let { + savedInstanceState?.getParcelableCompat(listStateKey)?.let { layoutManager.onRestoreInstanceState(it) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleInviteDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleInviteDialogFragment.kt index c286635c31ba..1c55fbda758f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleInviteDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleInviteDialogFragment.kt @@ -11,6 +11,7 @@ import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.ui.people.PeopleInviteDialogFragment.DialogMode.DISABLE_INVITE_LINKS_CONFIRMATION import org.wordpress.android.ui.people.PeopleInviteDialogFragment.DialogMode.INVITE_LINKS_ROLE_SELECTION +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.viewmodel.ContextProvider import javax.inject.Inject @@ -42,10 +43,11 @@ class PeopleInviteDialogFragment : DialogFragment() { @Suppress("DEPRECATION") override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { viewModel = ViewModelProvider( - targetFragment as ViewModelStoreOwner, viewModelFactory - ).get(PeopleInviteViewModel::class.java) + targetFragment as ViewModelStoreOwner, + viewModelFactory + )[PeopleInviteViewModel::class.java] - val dialogMode = arguments?.getSerializable(ARG_DIALOG_MODE) as? DialogMode + val dialogMode = arguments?.getSerializableCompat(ARG_DIALOG_MODE) val roles = arguments?.getStringArray(ARG_ROLES) val builder = MaterialAlertDialogBuilder(requireActivity()) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleManagementActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleManagementActivity.java index 0f0f8c69a615..4d2214c3a7ca 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleManagementActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/people/PeopleManagementActivity.java @@ -4,6 +4,7 @@ import android.os.Bundle; import android.view.MenuItem; +import androidx.activity.OnBackPressedCallback; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; @@ -32,6 +33,7 @@ import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import java.util.List; @@ -100,6 +102,16 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.people_management_activity); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (!navigateBackToPeopleListFragment()) { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + if (savedInstanceState == null) { mSite = (SiteModel) getIntent().getSerializableExtra(WordPress.SITE); } else { @@ -217,17 +229,10 @@ public void onStop() { super.onStop(); } - @Override - public void onBackPressed() { - if (!navigateBackToPeopleListFragment()) { - super.onBackPressed(); - } - } - @Override public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } else if (item.getItemId() == R.id.remove_person) { confirmRemovePerson(); @@ -239,7 +244,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { if (peopleInviteFragment == null) { peopleInviteFragment = PeopleInviteFragment.newInstance(mSite); } - if (peopleInviteFragment != null && !peopleInviteFragment.isAdded()) { + if (!peopleInviteFragment.isAdded()) { FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); fragmentTransaction.replace(R.id.fragment_container, peopleInviteFragment, KEY_PEOPLE_INVITE_FRAGMENT); fragmentTransaction.addToBackStack(null); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/people/WPEditTextWithChipsOutlined.kt b/WordPress/src/main/java/org/wordpress/android/ui/people/WPEditTextWithChipsOutlined.kt index f6244b156306..b7f05a833746 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/people/WPEditTextWithChipsOutlined.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/people/WPEditTextWithChipsOutlined.kt @@ -551,18 +551,18 @@ class WPEditTextWithChipsOutlined @JvmOverloads constructor( .scaleY(1f) .setDuration(LABEL_ANIMATION_DURATION) .setListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animation: Animator?) { + override fun onAnimationStart(animation: Animator) { label.visibility = View.INVISIBLE hint.visibility = View.VISIBLE } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { } - override fun onAnimationCancel(animation: Animator?) { + override fun onAnimationCancel(animation: Animator) { } - override fun onAnimationRepeat(animation: Animator?) { + override fun onAnimationRepeat(animation: Animator) { } }).start() } @@ -581,21 +581,21 @@ class WPEditTextWithChipsOutlined @JvmOverloads constructor( .scaleY(label.height.toFloat() / hint.height) .setDuration(LABEL_ANIMATION_DURATION) .setListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animation: Animator?) { + override fun onAnimationStart(animation: Animator) { setLabelColor(label, colorSurface, outlineColorAlphaFocused) label.visibility = View.VISIBLE hint.visibility = View.VISIBLE } - override fun onAnimationEnd(animation: Animator?) { + override fun onAnimationEnd(animation: Animator) { hint.visibility = View.INVISIBLE setLabelColor(label, outlineColorFocused, outlineColorAlphaFocused) } - override fun onAnimationCancel(animation: Animator?) { + override fun onAnimationCancel(animation: Animator) { } - override fun onAnimationRepeat(animation: Animator?) { + override fun onAnimationRepeat(animation: Animator) { } }).start() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/MediaPickerLauncher.kt b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/MediaPickerLauncher.kt index af6ea34f7f5c..5170e3b31ab1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/MediaPickerLauncher.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/MediaPickerLauncher.kt @@ -47,7 +47,8 @@ class MediaPickerLauncher @Inject constructor( primaryDataSource = DEVICE, availableDataSources = availableDataSources, canMultiselect = false, - requiresStoragePermissions = true, + requiresPhotosVideosPermissions = true, + requiresMusicAudioPermissions = false, allowedTypes = setOf(IMAGE), cameraSetup = ENABLED, systemPickerEnabled = true, @@ -65,14 +66,6 @@ class MediaPickerLauncher @Inject constructor( activity.startActivityForResult(intent, RequestCodes.PHOTO_PICKER) } - fun showSiteIconPicker( - activity: Activity, - site: SiteModel? - ) { - val intent = buildSitePickerIntent(activity, site) - activity.startActivityForResult(intent, RequestCodes.PHOTO_PICKER) - } - @Suppress("DEPRECATION") fun showSiteIconPicker( fragment: Fragment, @@ -90,7 +83,8 @@ class MediaPickerLauncher @Inject constructor( primaryDataSource = DEVICE, availableDataSources = setOf(WP_LIBRARY), canMultiselect = false, - requiresStoragePermissions = true, + requiresPhotosVideosPermissions = true, + requiresMusicAudioPermissions = false, allowedTypes = setOf(IMAGE), cameraSetup = ENABLED, systemPickerEnabled = true, @@ -142,7 +136,8 @@ class MediaPickerLauncher @Inject constructor( primaryDataSource = DEVICE, availableDataSources = setOf(), canMultiselect = false, - requiresStoragePermissions = true, + requiresPhotosVideosPermissions = true, + requiresMusicAudioPermissions = false, allowedTypes = setOf(IMAGE), cameraSetup = ENABLED, systemPickerEnabled = true, @@ -192,7 +187,8 @@ class MediaPickerLauncher @Inject constructor( primaryDataSource = DEVICE, availableDataSources = setOf(), canMultiselect = canMultiselect, - requiresStoragePermissions = true, + requiresPhotosVideosPermissions = allowedTypes.contains(IMAGE) || allowedTypes.contains(VIDEO), + requiresMusicAudioPermissions = allowedTypes.contains(AUDIO), allowedTypes = allowedTypes, cameraSetup = HIDDEN, systemPickerEnabled = true, @@ -236,7 +232,8 @@ class MediaPickerLauncher @Inject constructor( primaryDataSource = STOCK_LIBRARY, availableDataSources = setOf(), canMultiselect = allowMultipleSelection, - requiresStoragePermissions = false, + requiresPhotosVideosPermissions = false, + requiresMusicAudioPermissions = false, allowedTypes = setOf(IMAGE), cameraSetup = HIDDEN, systemPickerEnabled = false, @@ -267,7 +264,8 @@ class MediaPickerLauncher @Inject constructor( primaryDataSource = GIF_LIBRARY, availableDataSources = setOf(), canMultiselect = allowMultipleSelection, - requiresStoragePermissions = false, + requiresPhotosVideosPermissions = false, + requiresMusicAudioPermissions = false, allowedTypes = setOf(IMAGE), cameraSetup = HIDDEN, systemPickerEnabled = false, @@ -303,7 +301,8 @@ class MediaPickerLauncher @Inject constructor( primaryDataSource = DEVICE, availableDataSources = if (browserType.isWPStoriesPicker) setOf(WP_LIBRARY) else setOf(), canMultiselect = browserType.canMultiselect(), - requiresStoragePermissions = true, + requiresPhotosVideosPermissions = browserType.isImagePicker || browserType.isVideoPicker, + requiresMusicAudioPermissions = browserType.isAudioPicker, allowedTypes = allowedTypes, cameraSetup = if (browserType.isWPStoriesPicker) STORIES else HIDDEN, systemPickerEnabled = true, @@ -335,7 +334,8 @@ class MediaPickerLauncher @Inject constructor( primaryDataSource = WP_LIBRARY, availableDataSources = setOf(), canMultiselect = browserType.canMultiselect(), - requiresStoragePermissions = false, + requiresPhotosVideosPermissions = false, + requiresMusicAudioPermissions = false, allowedTypes = allowedTypes, cameraSetup = if (browserType.isWPStoriesPicker) STORIES else HIDDEN, systemPickerEnabled = false, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PermissionsHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PermissionsHandler.kt index e1e7874cfc33..b1e00cd608e6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PermissionsHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/photopicker/PermissionsHandler.kt @@ -3,35 +3,60 @@ package org.wordpress.android.ui.photopicker import android.Manifest.permission import android.content.Context import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat +import org.wordpress.android.util.PermissionUtils import javax.inject.Inject class PermissionsHandler @Inject constructor(private val context: Context) { - fun hasPermissionsToAccessPhotos(): Boolean { - return hasCameraPermission() && hasStoragePermission() + fun hasPermissionsToTakePhoto(): Boolean { + return PermissionUtils.checkCameraAndStoragePermissions(context) } - fun hasStoragePermission(): Boolean { - return hasReadStoragePermission() && hasWriteStoragePermission() + fun hasPhotosVideosPermission(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + hasReadMediaImagesPermission() && hasReadMediaVideoPermission() + } else { + // For devices lower than API 33, storage permission is the equivalent of Photos and Videos permission + hasReadStoragePermission() + } } - fun hasWriteStoragePermission(): Boolean { + fun hasMusicAudioPermission(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + hasReadMediaAudioPermission() + } else { + // For devices lower than API 33, storage permission is the equivalent of Music and Audio permission + hasReadStoragePermission() + } + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun hasReadMediaImagesPermission(): Boolean { return ContextCompat.checkSelfPermission( - context, permission.WRITE_EXTERNAL_STORAGE + context, permission.READ_MEDIA_IMAGES ) == PackageManager.PERMISSION_GRANTED } - private fun hasReadStoragePermission(): Boolean { + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun hasReadMediaVideoPermission(): Boolean { return ContextCompat.checkSelfPermission( - context, permission.READ_EXTERNAL_STORAGE + context, permission.READ_MEDIA_VIDEO + ) == PackageManager.PERMISSION_GRANTED + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun hasReadMediaAudioPermission(): Boolean { + return ContextCompat.checkSelfPermission( + context, permission.READ_MEDIA_AUDIO ) == PackageManager.PERMISSION_GRANTED } - private fun hasCameraPermission(): Boolean { + private fun hasReadStoragePermission(): Boolean { return ContextCompat.checkSelfPermission( - context, - permission.CAMERA + context, permission.READ_EXTERNAL_STORAGE ) == PackageManager.PERMISSION_GRANTED } } 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 3ad1acb4abae..f8cfdfc8c226 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 @@ -2,6 +2,7 @@ package org.wordpress.android.ui.photopicker import android.Manifest.permission import android.net.Uri +import android.os.Build import android.os.Bundle import android.os.Parcelable import android.view.View @@ -31,10 +32,13 @@ import org.wordpress.android.util.AniUtils.Duration.MEDIUM import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T.POSTS import org.wordpress.android.util.DisplayUtils +import org.wordpress.android.util.PermissionUtils import org.wordpress.android.util.UriWrapper import org.wordpress.android.util.ViewWrapper import org.wordpress.android.util.WPMediaUtils import org.wordpress.android.util.WPPermissionUtils +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -79,6 +83,7 @@ class PhotoPickerFragment : Fragment(R.layout.photo_picker_fragment) { @Suppress("DEPRECATION") private lateinit var viewModel: PhotoPickerViewModel private var binding: PhotoPickerFragmentBinding? = null + private lateinit var browserType: MediaBrowserType @Suppress("DEPRECATION") override fun onCreate(savedInstanceState: Bundle?) { @@ -90,8 +95,10 @@ class PhotoPickerFragment : Fragment(R.layout.photo_picker_fragment) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val browserType = requireArguments().getSerializable(MediaBrowserActivity.ARG_BROWSER_TYPE) as MediaBrowserType - val site = requireArguments().getSerializable(WordPress.SITE) as? SiteModel + browserType = requireNotNull( + arguments?.getSerializableCompat(MediaBrowserActivity.ARG_BROWSER_TYPE) + ) + val site = requireArguments().getSerializableCompat(WordPress.SITE) var selectedIds: List? = null var lastTappedIcon: PhotoPickerIcon? = null if (savedInstanceState != null) { @@ -111,7 +118,7 @@ class PhotoPickerFragment : Fragment(R.layout.photo_picker_fragment) { NUM_COLUMNS ) - savedInstanceState?.getParcelable(KEY_LIST_STATE)?.let { + savedInstanceState?.getParcelableCompat(KEY_LIST_STATE)?.let { layoutManager.onRestoreInstanceState(it) } @@ -127,7 +134,7 @@ class PhotoPickerFragment : Fragment(R.layout.photo_picker_fragment) { observeOnShowPopupMenu() - observeOnPermissionsRequested() + observeOnCameraPermissionsRequested() setupProgressDialog() @@ -136,13 +143,8 @@ class PhotoPickerFragment : Fragment(R.layout.photo_picker_fragment) { } @Suppress("DEPRECATION") - private fun observeOnPermissionsRequested() { - viewModel.onPermissionsRequested.observeEvent(viewLifecycleOwner) { - when (it) { - PhotoPickerViewModel.PermissionsRequested.CAMERA -> requestCameraPermission() - PhotoPickerViewModel.PermissionsRequested.STORAGE -> requestStoragePermission() - } - } + private fun observeOnCameraPermissionsRequested() { + viewModel.onCameraPermissionsRequested.observeEvent(viewLifecycleOwner) { requestCameraPermission() } } private fun observeOnShowPopupMenu() { @@ -221,7 +223,7 @@ class PhotoPickerFragment : Fragment(R.layout.photo_picker_fragment) { if (uiModel.isAlwaysDenied) { WPPermissionUtils.showAppSettings(requireActivity()) } else { - requestStoragePermission() + requestMediaPermission() } } softAskView.visibility = View.VISIBLE @@ -371,7 +373,7 @@ class PhotoPickerFragment : Fragment(R.layout.photo_picker_fragment) { override fun onResume() { super.onResume() - checkStoragePermission() + checkMediaPermission() } fun performActionOrShowPopup(view: View) { @@ -416,37 +418,62 @@ class PhotoPickerFragment : Fragment(R.layout.photo_picker_fragment) { viewModel.refreshData(false) } - private val isStoragePermissionAlwaysDenied: Boolean - get() = WPPermissionUtils.isPermissionAlwaysDenied( - requireActivity(), permission.WRITE_EXTERNAL_STORAGE - ) - /* * load the photos if we have the necessary permission, otherwise show the "soft ask" view * which asks the user to allow the permission */ - private fun checkStoragePermission() { + private fun checkMediaPermission() { if (!isAdded) { return } - viewModel.checkStoragePermission(isStoragePermissionAlwaysDenied) + + // Storage permission is available only for API lower than 33 + val isStoragePermissionAlwaysDenied = WPPermissionUtils.isPermissionAlwaysDenied( + requireActivity(), + permission.WRITE_EXTERNAL_STORAGE + ) + + val isPhotosVideosPermissionAlwaysDenied = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + WPPermissionUtils.isPermissionAlwaysDenied(requireActivity(), permission.READ_MEDIA_IMAGES) || + WPPermissionUtils.isPermissionAlwaysDenied(requireActivity(), permission.READ_MEDIA_VIDEO) + } else { + // For devices lower than API 33, storage permission is the equivalent of Photos and Videos permission + isStoragePermissionAlwaysDenied + } + val isMusicAudioPermissionAlwaysDenied = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + WPPermissionUtils.isPermissionAlwaysDenied( + requireActivity(), + permission.READ_MEDIA_AUDIO + ) + } else { + // For devices lower than API 33, storage permission is the equivalent of Music and Audio permission + isStoragePermissionAlwaysDenied + } + viewModel.checkMediaPermissions(isPhotosVideosPermissionAlwaysDenied, isMusicAudioPermissionAlwaysDenied) } @Suppress("DEPRECATION") - private fun requestStoragePermission() { - val permissions = arrayOf(permission.WRITE_EXTERNAL_STORAGE) - requestPermissions( - permissions, WPPermissionUtils.PHOTO_PICKER_STORAGE_PERMISSION_REQUEST_CODE - ) + private fun requestMediaPermission() { + val permissions = arrayListOf() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (browserType.isImagePicker || browserType.isVideoPicker) { + permissions.add(permission.READ_MEDIA_IMAGES) + permissions.add(permission.READ_MEDIA_VIDEO) + } + if (browserType.isAudioPicker) { + permissions.add(permission.READ_MEDIA_AUDIO) + } + } else { + // READ_EXTERNAL_STORAGE is the equivalent of READ_MEDIA_IMAGES, READ_MEDIA_VIDEO and READ_MEDIA_AUDIO on + // devices lower than API 33. + permissions.add(permission.READ_EXTERNAL_STORAGE) + } + requestPermissions(permissions.toTypedArray(), WPPermissionUtils.PHOTO_PICKER_MEDIA_PERMISSION_REQUEST_CODE) } @Suppress("DEPRECATION") private fun requestCameraPermission() { - // in addition to CAMERA permission we also need a storage permission, to store media from the camera - val permissions = arrayOf( - permission.CAMERA, - permission.WRITE_EXTERNAL_STORAGE - ) + val permissions = PermissionUtils.getCameraAndStoragePermissions() requestPermissions(permissions, WPPermissionUtils.PHOTO_PICKER_CAMERA_PERMISSION_REQUEST_CODE) } @@ -461,7 +488,7 @@ class PhotoPickerFragment : Fragment(R.layout.photo_picker_fragment) { requireActivity(), requestCode, permissions, grantResults, checkForAlwaysDenied ) when (requestCode) { - WPPermissionUtils.PHOTO_PICKER_STORAGE_PERMISSION_REQUEST_CODE -> checkStoragePermission() + WPPermissionUtils.PHOTO_PICKER_MEDIA_PERMISSION_REQUEST_CODE -> checkMediaPermission() WPPermissionUtils.PHOTO_PICKER_CAMERA_PERMISSION_REQUEST_CODE -> if (allGranted) { viewModel.clickOnLastTappedIcon() } 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 6ea2dd6fc531..01b2ff1674bb 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,7 +1,7 @@ package org.wordpress.android.ui.photopicker -import android.Manifest.permission import android.net.Uri +import android.os.Build import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineDispatcher @@ -34,7 +34,6 @@ import org.wordpress.android.util.AppLog.T.MEDIA import org.wordpress.android.util.MediaUtils import org.wordpress.android.util.UriWrapper import org.wordpress.android.util.ViewWrapper -import org.wordpress.android.util.WPPermissionUtils import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper import org.wordpress.android.util.distinct @@ -68,7 +67,7 @@ class PhotoPickerViewModel @Inject constructor( private val _photoPickerItems = MutableLiveData>() private val _selectedIds = MutableLiveData?>() private val _onIconClicked = MutableLiveData>() - private val _onPermissionsRequested = MutableLiveData>() + private val _onCameraPermissionsRequested = MutableLiveData>() private val _softAskRequest = MutableLiveData() private val _showProgressDialog = MutableLiveData() @@ -77,7 +76,7 @@ class PhotoPickerViewModel @Inject constructor( val onIconClicked: LiveData> = _onIconClicked val onShowPopupMenu: LiveData> = _showPopupMenu - val onPermissionsRequested: LiveData> = _onPermissionsRequested + val onCameraPermissionsRequested: LiveData> = _onCameraPermissionsRequested val selectedIds: LiveData?> = _selectedIds @@ -219,14 +218,13 @@ class PhotoPickerViewModel @Inject constructor( } fun refreshData(forceReload: Boolean) { - if (!permissionsHandler.hasWriteStoragePermission()) { - return - } - launch(bgDispatcher) { - val result = deviceMediaListBuilder.buildDeviceMedia(browserType) - val currentItems = _photoPickerItems.value ?: listOf() - if (forceReload || currentItems != result) { - _photoPickerItems.postValue(result) + if (!needPhotosVideoPermission() && !needMusicAudioPermission()) { + launch(bgDispatcher) { + val result = deviceMediaListBuilder.buildDeviceMedia(browserType) + val currentItems = _photoPickerItems.value ?: listOf() + if (forceReload || currentItems != result) { + _photoPickerItems.postValue(result) + } } } } @@ -318,8 +316,8 @@ class PhotoPickerViewModel @Inject constructor( icon == PhotoPickerFragment.PhotoPickerIcon.ANDROID_CAPTURE_VIDEO || icon == PhotoPickerFragment.PhotoPickerIcon.WP_STORIES_CAPTURE ) { - if (!permissionsHandler.hasPermissionsToAccessPhotos()) { - _onPermissionsRequested.value = Event(PermissionsRequested.CAMERA) + if (!permissionsHandler.hasPermissionsToTakePhoto()) { + _onCameraPermissionsRequested.value = Event(Unit) lastTappedIcon = icon return } @@ -417,8 +415,11 @@ class PhotoPickerViewModel @Inject constructor( } } - fun checkStoragePermission(isAlwaysDenied: Boolean) { - if (permissionsHandler.hasWriteStoragePermission()) { + fun checkMediaPermissions(isPhotosVideosAlwaysDenied: Boolean, isMusicAudioAlwaysDenied: Boolean) { + val isAlwaysDenied = ((browserType.isImagePicker || browserType.isVideoPicker) && isPhotosVideosAlwaysDenied) || + (browserType.isAudioPicker && isMusicAudioAlwaysDenied) + + if (!needPhotosVideoPermission() && !needMusicAudioPermission()) { _softAskRequest.value = SoftAskRequest(show = false, isAlwaysDenied = isAlwaysDenied) if (_photoPickerItems.value.isNullOrEmpty()) { refreshData(false) @@ -428,25 +429,47 @@ class PhotoPickerViewModel @Inject constructor( } } + private fun getRequiredPermissionsNames(): String { + val permissionName = when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU -> R.string.permission_storage + needPhotosVideoPermission() && needMusicAudioPermission() -> R.string.permission_images_video_audio + needPhotosVideoPermission() -> R.string.permission_images + needMusicAudioPermission() -> R.string.permission_audio + else -> R.string.unknown + } + return resourceProvider.getString(permissionName) + } + + private fun needPhotosVideoPermission() = + (browserType.isImagePicker || browserType.isVideoPicker) && !permissionsHandler.hasPhotosVideosPermission() + + private fun needMusicAudioPermission() = + browserType.isAudioPicker && !permissionsHandler.hasMusicAudioPermission() + private fun buildSoftAskView(softAskRequest: SoftAskRequest?): SoftAskViewUiModel { if (softAskRequest != null && softAskRequest.show) { val appName = "${resourceProvider.getString(R.string.app_name)}" val label = if (softAskRequest.isAlwaysDenied) { - val permissionName = ("${ - WPPermissionUtils.getPermissionName( - resourceProvider, - permission.WRITE_EXTERNAL_STORAGE - ) - }") + val permission = ("${getRequiredPermissionsNames()}") String.format( - resourceProvider.getString(R.string.photo_picker_soft_ask_permissions_denied), appName, - permissionName + resourceProvider.getString(R.string.media_picker_soft_ask_media_permissions_denied), + appName, + permission ) } else { - String.format( - resourceProvider.getString(R.string.photo_picker_soft_ask_label), - appName - ) + val description = when { + browserType.isImagePicker && browserType.isVideoPicker && browserType.isAudioPicker -> { + R.string.photo_picker_soft_ask_photos_videos_audio_label + } + browserType.isImagePicker && browserType.isVideoPicker -> { + R.string.photo_picker_soft_ask_photos_videos_label + } + browserType.isImagePicker -> R.string.photo_picker_soft_ask_photos_label + browserType.isVideoPicker -> R.string.photo_picker_soft_ask_videos_label + browserType.isAudioPicker -> R.string.photo_picker_soft_ask_audios_label + else -> R.string.unknown + } + String.format(resourceProvider.getString(description), appName) } val allowId = if (softAskRequest.isAlwaysDenied) { R.string.button_edit_permissions @@ -471,7 +494,7 @@ class PhotoPickerViewModel @Inject constructor( } } - fun copySelectedUrisLocally(uris: List) { + private fun copySelectedUrisLocally(uris: List) { launch { _showProgressDialog.value = ProgressDialogUiModel.Visible(R.string.uploading_title) { _showProgressDialog.postValue(ProgressDialogUiModel.Hidden) @@ -540,10 +563,6 @@ class PhotoPickerViewModel @Inject constructor( val allowMultipleSelection: Boolean ) - enum class PermissionsRequested { - CAMERA, STORAGE - } - data class PopupMenuUiModel(val view: ViewWrapper, val items: List) { data class PopupMenuItem(val title: UiStringRes, val action: () -> Unit) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanDetailsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanDetailsFragment.kt index 7de5b92db14a..23a6411ad2f8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanDetailsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlanDetailsFragment.kt @@ -14,6 +14,7 @@ import org.wordpress.android.fluxc.model.plans.PlanOffersModel import org.wordpress.android.ui.FullScreenDialogFragment.FullScreenDialogContent import org.wordpress.android.ui.FullScreenDialogFragment.FullScreenDialogController import org.wordpress.android.util.StringUtils +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.image.ImageType import javax.inject.Inject @@ -42,9 +43,9 @@ class PlanDetailsFragment : Fragment(), FullScreenDialogContent { (requireActivity().application as WordPress).component().inject(this) plan = if (savedInstanceState != null) { - savedInstanceState.getParcelable(KEY_PLAN) + savedInstanceState.getParcelableCompat(KEY_PLAN) } else { - arguments?.getParcelable(EXTRA_PLAN) + arguments?.getParcelableCompat(EXTRA_PLAN) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginBrowserActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginBrowserActivity.java index 75d16600eefa..9100438a7a41 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginBrowserActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginBrowserActivity.java @@ -11,6 +11,7 @@ import android.widget.RatingBar; import android.widget.TextView; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.ColorRes; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; @@ -40,13 +41,14 @@ import org.wordpress.android.ui.LocaleAwareActivity; import org.wordpress.android.util.ActivityUtils; import org.wordpress.android.util.AniUtils; -import org.wordpress.android.util.extensions.AppBarLayoutExtensionsKt; import org.wordpress.android.util.ColorUtils; -import org.wordpress.android.util.extensions.ContextExtensionsKt; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.extensions.AppBarLayoutExtensionsKt; +import org.wordpress.android.util.extensions.CompatExtensionsKt; +import org.wordpress.android.util.extensions.ContextExtensionsKt; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.util.image.ImageType; import org.wordpress.android.viewmodel.plugins.PluginBrowserViewModel; @@ -80,6 +82,18 @@ public void onCreate(Bundle savedInstanceState) { ((WordPress) getApplication()).component().inject(this); setContentView(R.layout.plugin_browser_activity); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (getSupportFragmentManager().getBackStackEntryCount() > 0) { + // update the lift on scroll target id when we return to the root fragment + AppBarLayoutExtensionsKt.setLiftOnScrollTargetViewIdAndRequestLayout(mAppBar, R.id.scroll_view); + } + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + mViewModel = new ViewModelProvider(this, mViewModelFactory).get(PluginBrowserViewModel.class); mSitePluginsRecycler = findViewById(R.id.installed_plugins_recycler); @@ -225,21 +239,12 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); } - @Override - public void onBackPressed() { - if (getSupportFragmentManager().getBackStackEntryCount() > 0) { - // update the lift on scroll target id when we return to the root fragment - AppBarLayoutExtensionsKt.setLiftOnScrollTargetViewIdAndRequestLayout(mAppBar, R.id.scroll_view); - } - super.onBackPressed(); - } - private void reloadPluginAdapterAndVisibility(@NonNull PluginListType pluginType, @Nullable ListState listState) { if (listState == null) { @@ -315,7 +320,7 @@ private void showListFragment(@NonNull PluginListType listType) { private void hideListFragment() { if (getSupportFragmentManager().getBackStackEntryCount() > 0) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginDetailActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginDetailActivity.java index f218343e1467..e9ca8483951d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginDetailActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/plugins/PluginDetailActivity.java @@ -80,7 +80,6 @@ import org.wordpress.android.util.AniUtils; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; -import org.wordpress.android.util.extensions.ContextExtensionsKt; import org.wordpress.android.util.DateTimeUtils; import org.wordpress.android.util.DisplayUtils; import org.wordpress.android.util.FormatUtils; @@ -91,6 +90,7 @@ import org.wordpress.android.util.ToastUtils.Duration; import org.wordpress.android.util.WPLinkMovementMethod; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.extensions.ContextExtensionsKt; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.util.image.ImageType; import org.wordpress.android.widgets.WPSnackbar; @@ -413,7 +413,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { // user is leaving the page dispatchConfigurePluginAction(true); } - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } else if (item.getItemId() == R.id.menu_trash) { if (NetworkUtils.checkConnection(this)) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/BasicDialog.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/BasicDialog.kt index 671faa0af244..b461e9979fb7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/BasicDialog.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/BasicDialog.kt @@ -1,19 +1,20 @@ package org.wordpress.android.ui.posts import android.app.Dialog -import android.content.Context import android.content.DialogInterface import android.os.Bundle import androidx.appcompat.app.AppCompatDialogFragment import androidx.lifecycle.ViewModelProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.android.support.AndroidSupportInjection +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.ui.posts.BasicDialogViewModel.BasicDialogModel +import org.wordpress.android.util.extensions.getParcelableCompat import javax.inject.Inject /** * Basic dialog fragment with support for 1,2 or 3 buttons. */ +@AndroidEntryPoint class BasicDialog : AppCompatDialogFragment() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -34,7 +35,7 @@ class BasicDialog : AppCompatDialogFragment() { setStyle(STYLE_NORMAL, theme) if (savedInstanceState != null) { - model = requireNotNull(savedInstanceState.getParcelable(STATE_KEY_MODEL)) + model = requireNotNull(savedInstanceState.getParcelableCompat(STATE_KEY_MODEL)) } } @@ -75,11 +76,6 @@ class BasicDialog : AppCompatDialogFragment() { return builder.create() } - override fun onAttach(context: Context) { - super.onAttach(context) - AndroidSupportInjection.inject(this) - } - override fun onDismiss(dialog: DialogInterface) { if (!dismissedByPositiveButton && !dismissedByNegativeButton && !dismissedByCancelButton) { viewModel.onDismissByOutsideTouch(model.tag) 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 ef2fc12e20db..d29388ccb997 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 @@ -18,6 +18,7 @@ import android.webkit.MimeTypeMap; import android.widget.Toast; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -524,6 +525,15 @@ private void createPostEditorAnalyticsSessionTracker(boolean showGutenbergEditor protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((WordPress) getApplication()).component().inject(this); + + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + handleBackPressed(); + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + mDispatcher.register(this); mViewModel = new ViewModelProvider(this, mViewModelFactory).get(StorePostViewModel.class); mStorageUtilsViewModel = new ViewModelProvider(this, mViewModelFactory).get(StorageUtilsViewModel.class); @@ -1335,8 +1345,8 @@ public boolean onPrepareOptionsMenu(Menu menu) { if (helpMenuItem != null) { // Support section will be disabled in WordPress app when Jetpack-powered features are removed. // Therefore, we have to update the Help menu item accordingly. - boolean jetpackFeaturesRemoved = mJetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures(); - int helpMenuTitle = jetpackFeaturesRemoved ? R.string.help : R.string.help_and_support; + boolean showHelpAndSupport = mJetpackFeatureRemovalPhaseHelper.shouldShowHelpAndSupportOnEditor(); + int helpMenuTitle = showHelpAndSupport ? R.string.help_and_support : R.string.help; helpMenuItem.setTitle(helpMenuTitle); if (mEditorFragment instanceof GutenbergEditorFragment @@ -1728,6 +1738,9 @@ private void setGutenbergEnabledIfNeeded() { } private ActivityFinishState savePostOnline(boolean isFirstTimePublish) { + if (mEditorFragment instanceof GutenbergEditorFragment) { + ((GutenbergEditorFragment) mEditorFragment).sendToJSPostSaveEvent(); + } return mViewModel.savePostOnline(isFirstTimePublish, this, mEditPostRepository, mSite); } @@ -1968,11 +1981,6 @@ public interface OnPostUpdatedFromUIListener { void onPostUpdatedFromUI(@Nullable UpdatePostResult updatePostResult); } - @Override - public void onBackPressed() { - handleBackPressed(); - } - @Override public void onHistoryItemClicked(@NonNull Revision revision, @NonNull List revisions) { AnalyticsTracker.track(Stat.REVISIONS_DETAIL_VIEWED_FROM_LIST); @@ -2272,7 +2280,7 @@ public Fragment getItem(int position) { gutenbergWebViewAuthorizationData, gutenbergPropsBuilder, RequestCodes.EDIT_STORY, - !mJetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures() + mJetpackFeatureRemovalPhaseHelper.shouldShowJetpackPoweredEditorFeatures() ); } else { // If gutenberg editor is not selected, default to Aztec. @@ -2360,7 +2368,7 @@ private GutenbergPropsBuilder getGutenbergPropsBuilder() { String hostAppNamespace = mBuildConfigWrapper.isJetpackApp() ? "Jetpack" : "WordPress"; // Disable Jetpack-powered editor features in WordPress app based on Jetpack Features Removal Phase helper - boolean jetpackFeaturesRemoved = mJetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures(); + boolean jetpackFeaturesRemoved = !mJetpackFeatureRemovalPhaseHelper.shouldShowJetpackPoweredEditorFeatures(); if (jetpackFeaturesRemoved) { return new GutenbergPropsBuilder( false, @@ -3130,6 +3138,17 @@ public void onAddFileClicked(boolean allowMultipleSelection) { } } + @Override public void onPerformPost( + String path, + Map body, + Consumer onResult, + Consumer onError + ) { + if (mSite != null) { + mReactNativeRequestHandler.performPostRequest(path, body, mSite, onResult, onError); + } + } + @Override public void onCaptureVideoClicked() { onPhotoPickerIconClicked(PhotoPickerIcon.ANDROID_CAPTURE_VIDEO, false); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/HistoryListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/HistoryListFragment.kt index f406562bf047..ba5fb62aabb4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/HistoryListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/HistoryListFragment.kt @@ -17,6 +17,7 @@ import org.wordpress.android.ui.history.HistoryAdapter import org.wordpress.android.ui.history.HistoryListItem import org.wordpress.android.ui.history.HistoryListItem.Revision import org.wordpress.android.util.WPSwipeToRefreshHelper +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.helpers.SwipeToRefreshHelper import org.wordpress.android.viewmodel.history.HistoryViewModel import org.wordpress.android.viewmodel.history.HistoryViewModel.HistoryListStatus @@ -84,10 +85,11 @@ class HistoryListFragment : Fragment(R.layout.history_list_fragment) { (nonNullActivity.application as WordPress).component().inject(this@HistoryListFragment) - viewModel = ViewModelProvider(this@HistoryListFragment, viewModelFactory).get(HistoryViewModel::class.java) + viewModel = ViewModelProvider(this@HistoryListFragment, viewModelFactory)[HistoryViewModel::class.java] + val site = requireNotNull(arguments?.getSerializableCompat(KEY_SITE)) viewModel.create( localPostId = arguments?.getInt(KEY_POST_LOCAL_ID) ?: 0, - site = arguments?.get(KEY_SITE) as SiteModel + site = site ) updatePostOrPageEmptyView() setObservers() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/JetpackSecuritySettingsActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/JetpackSecuritySettingsActivity.kt index e9d911010b89..2816b22a829d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/JetpackSecuritySettingsActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/JetpackSecuritySettingsActivity.kt @@ -2,6 +2,7 @@ package org.wordpress.android.ui.posts import android.os.Bundle import android.view.MenuItem +import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity import org.wordpress.android.R.string import org.wordpress.android.databinding.FragmentJetpackSecuritySettingsBinding @@ -13,6 +14,11 @@ class JetpackSecuritySettingsActivity : AppCompatActivity() { setContentView(root) setupToolbar() } + + onBackPressedDispatcher.addCallback(this) { + setResult(RESULT_OK, null) + finish() + } } private fun FragmentJetpackSecuritySettingsBinding.setupToolbar() { @@ -35,11 +41,6 @@ class JetpackSecuritySettingsActivity : AppCompatActivity() { return super.onOptionsItemSelected(item) } - override fun onBackPressed() { - setResult(RESULT_OK, null) - finish() - } - companion object { const val JETPACK_SECURITY_SETTINGS_REQUEST_CODE = 101 } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostDatePickerDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostDatePickerDialogFragment.kt index 6ccc4df769ac..c40d280a8cc1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostDatePickerDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostDatePickerDialogFragment.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModelProvider import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.ui.posts.prepublishing.PrepublishingPublishSettingsViewModel +import org.wordpress.android.util.extensions.getParcelableCompat import javax.inject.Inject class PostDatePickerDialogFragment : DialogFragment() { @@ -18,15 +19,19 @@ class PostDatePickerDialogFragment : DialogFragment() { private lateinit var viewModel: PublishSettingsViewModel override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val publishSettingsFragmentType = arguments?.getParcelable( + val publishSettingsFragmentType = arguments?.getParcelableCompat( ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE ) viewModel = when (publishSettingsFragmentType) { - PublishSettingsFragmentType.EDIT_POST -> ViewModelProvider(requireActivity(), viewModelFactory) - .get(EditPostPublishSettingsViewModel::class.java) - PublishSettingsFragmentType.PREPUBLISHING_NUDGES -> ViewModelProvider(requireActivity(), viewModelFactory) - .get(PrepublishingPublishSettingsViewModel::class.java) + PublishSettingsFragmentType.EDIT_POST -> ViewModelProvider( + requireActivity(), + viewModelFactory + )[EditPostPublishSettingsViewModel::class.java] + PublishSettingsFragmentType.PREPUBLISHING_NUDGES -> ViewModelProvider( + requireActivity(), + viewModelFactory + )[PrepublishingPublishSettingsViewModel::class.java] null -> error("PublishSettingsViewModel not initialized") } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListFragment.kt index 7d88c9d37027..619c6f24ba92 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListFragment.kt @@ -27,6 +27,8 @@ import org.wordpress.android.util.DisplayUtils import org.wordpress.android.util.NetworkUtils import org.wordpress.android.util.ToastUtils import org.wordpress.android.util.WPSwipeToRefreshHelper.buildSwipeToRefreshHelper +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.util.helpers.SwipeToRefreshHelper import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout @@ -80,7 +82,7 @@ class PostListFragment : ViewPagerFragment() { (nonNullActivity.application as WordPress).component().inject(this) val nonNullIntent = checkNotNull(nonNullActivity.intent) - val site: SiteModel? = nonNullIntent.getSerializableExtra(WordPress.SITE) as SiteModel? + val site = nonNullIntent.getSerializableExtraCompat(WordPress.SITE) if (site == null) { ToastUtils.showToast(nonNullActivity, R.string.blog_not_found, ToastUtils.Duration.SHORT) @@ -97,14 +99,13 @@ class PostListFragment : ViewPagerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - postListType = requireNotNull(arguments).getSerializable(EXTRA_POST_LIST_TYPE) as PostListType + postListType = requireNotNull(arguments?.getSerializableCompat(EXTRA_POST_LIST_TYPE)) if (postListType == SEARCH) { recyclerView?.id = R.id.posts_search_recycler_view_id } - mainViewModel = ViewModelProvider(nonNullActivity, viewModelFactory) - .get(PostListMainViewModel::class.java) + mainViewModel = ViewModelProvider(nonNullActivity, viewModelFactory)[PostListMainViewModel::class.java] mainViewModel.viewLayoutType.observe(viewLifecycleOwner, Observer { optionaLayoutType -> optionaLayoutType?.let { layoutType -> @@ -126,19 +127,19 @@ class PostListFragment : ViewPagerFragment() { } }) - mainViewModel.authorSelectionUpdated.observe(viewLifecycleOwner, Observer { + mainViewModel.authorSelectionUpdated.observe(viewLifecycleOwner) { if (it != null) { if (viewModel.updateAuthorFilterIfNotSearch(it)) { recyclerView?.scrollToPosition(0) } } - }) + } actionableEmptyView?.updateLayoutForSearch(postListType == SEARCH, 0) val postListViewModelConnector = mainViewModel.getPostListViewModelConnector(postListType) - viewModel = ViewModelProvider(this, viewModelFactory).get(PostListViewModel::class.java) + viewModel = ViewModelProvider(this, viewModelFactory)[PostListViewModel::class.java] val displayWidth = DisplayUtils.getWindowPixelWidth(requireContext()) val contentSpacing = nonNullActivity.resources.getDimensionPixelSize(R.dimen.content_margin) 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 178ff6d62899..56af91ef0ecd 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 @@ -52,7 +52,7 @@ import org.wordpress.android.ui.uploads.UploadStarter import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.util.AppLog import org.wordpress.android.util.NetworkUtilsWrapper -import org.wordpress.android.util.SiteUtils +import org.wordpress.android.util.SiteUtilsWrapper import org.wordpress.android.util.ToastUtils.Duration import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.analytics.AnalyticsUtils @@ -92,7 +92,8 @@ class PostListMainViewModel @Inject constructor( private val uploadStarter: UploadStarter, private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper, private val blazeFeatureUtils: BlazeFeatureUtils, - private val blazeStore: BlazeStore + private val blazeStore: BlazeStore, + private val siteUtilsWrapper: SiteUtilsWrapper ) : ViewModel(), CoroutineScope { private val lifecycleOwner = object : LifecycleOwner { val lifecycleRegistry = LifecycleRegistry(this) @@ -401,7 +402,7 @@ class PostListMainViewModel @Inject constructor( } fun fabClicked() { - if (SiteUtils.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper)) { + if (siteUtilsWrapper.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper)) { _onFabClicked.postValue(Event(Unit)) } else { newPost() @@ -643,7 +644,7 @@ class PostListMainViewModel @Inject constructor( } fun onFabLongPressed() { - if (SiteUtils.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper)) { + if (siteUtilsWrapper.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper)) { _onFabLongPressedForCreateMenu.postValue(Event(Unit)) } else { _onFabLongPressedForPostList.postValue(Event(Unit)) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostNotificationScheduleTimeDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostNotificationScheduleTimeDialogFragment.kt index 2f736ec0a560..1b408616310f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostNotificationScheduleTimeDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostNotificationScheduleTimeDialogFragment.kt @@ -15,6 +15,7 @@ import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.Schedul import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.TEN_MINUTES import org.wordpress.android.fluxc.store.PostSchedulingNotificationStore.SchedulingReminderModel.Period.WHEN_PUBLISHED import org.wordpress.android.ui.posts.prepublishing.PrepublishingPublishSettingsViewModel +import org.wordpress.android.util.extensions.getParcelableCompat import javax.inject.Inject class PostNotificationScheduleTimeDialogFragment : DialogFragment() { @@ -23,15 +24,19 @@ class PostNotificationScheduleTimeDialogFragment : DialogFragment() { private lateinit var viewModel: PublishSettingsViewModel override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val publishSettingsFragmentType = arguments?.getParcelable( + val publishSettingsFragmentType = arguments?.getParcelableCompat( ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE ) viewModel = when (publishSettingsFragmentType) { - PublishSettingsFragmentType.EDIT_POST -> ViewModelProvider(requireActivity(), viewModelFactory) - .get(EditPostPublishSettingsViewModel::class.java) - PublishSettingsFragmentType.PREPUBLISHING_NUDGES -> ViewModelProvider(requireActivity(), viewModelFactory) - .get(PrepublishingPublishSettingsViewModel::class.java) + PublishSettingsFragmentType.EDIT_POST -> ViewModelProvider( + requireActivity(), + viewModelFactory + )[EditPostPublishSettingsViewModel::class.java] + PublishSettingsFragmentType.PREPUBLISHING_NUDGES -> ViewModelProvider( + requireActivity(), + viewModelFactory + )[PrepublishingPublishSettingsViewModel::class.java] null -> error("PublishSettingsViewModel not initialized") } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsInputDialogFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsInputDialogFragment.java index 459ebd249990..dd8b5c2a67bb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsInputDialogFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsInputDialogFragment.java @@ -93,7 +93,7 @@ public void onDismiss(DialogInterface dialog) { public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new MaterialAlertDialogBuilder(new ContextThemeWrapper(getActivity(), R.style.PostSettingsTheme)); - LayoutInflater layoutInflater = LayoutInflater.from(getActivity()); + LayoutInflater layoutInflater = getActivity().getLayoutInflater(); //noinspection InflateParams View dialogView = layoutInflater.inflate(R.layout.post_settings_input_dialog, null); builder.setView(dialogView); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsTagsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsTagsActivity.java index db3e82ff28a8..6c6d0674db5d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsTagsActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostSettingsTagsActivity.java @@ -4,6 +4,7 @@ import android.os.Bundle; import android.view.MenuItem; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; @@ -14,6 +15,7 @@ import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.ui.LocaleAwareActivity; import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.extensions.CompatExtensionsKt; public class PostSettingsTagsActivity extends LocaleAwareActivity implements TagsSelectedListener { public static final String KEY_TAGS = "KEY_TAGS"; @@ -25,6 +27,15 @@ public class PostSettingsTagsActivity extends LocaleAwareActivity implements Tag public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + saveAndFinish(); + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + if (savedInstanceState == null) { mSite = (SiteModel) getIntent().getSerializableExtra(WordPress.SITE); mTags = getIntent().getStringExtra(KEY_TAGS); @@ -77,12 +88,6 @@ public boolean onOptionsItemSelected(final MenuItem item) { return super.onOptionsItemSelected(item); } - @Override - public void onBackPressed() { - saveAndFinish(); - super.onBackPressed(); - } - private void saveAndFinish() { closeKeyboard(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostTimePickerDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostTimePickerDialogFragment.kt index 43da4c08fff9..f22fff7d9373 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostTimePickerDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostTimePickerDialogFragment.kt @@ -2,7 +2,6 @@ package org.wordpress.android.ui.posts import android.app.Dialog import android.app.TimePickerDialog -import android.app.TimePickerDialog.OnTimeSetListener import android.content.Context import android.os.Bundle import android.text.format.DateFormat @@ -13,6 +12,7 @@ import org.wordpress.android.R.style import org.wordpress.android.WordPress import org.wordpress.android.ui.posts.prepublishing.PrepublishingPublishSettingsViewModel +import org.wordpress.android.util.extensions.getParcelableCompat import javax.inject.Inject class PostTimePickerDialogFragment : DialogFragment() { @@ -21,30 +21,31 @@ class PostTimePickerDialogFragment : DialogFragment() { private lateinit var viewModel: PublishSettingsViewModel override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val publishSettingsFragmentType = arguments?.getParcelable( + val publishSettingsFragmentType = arguments?.getParcelableCompat( ARG_PUBLISH_SETTINGS_FRAGMENT_TYPE ) viewModel = when (publishSettingsFragmentType) { - PublishSettingsFragmentType.EDIT_POST -> ViewModelProvider(requireActivity(), viewModelFactory) - .get(EditPostPublishSettingsViewModel::class.java) - PublishSettingsFragmentType.PREPUBLISHING_NUDGES -> ViewModelProvider(requireActivity(), viewModelFactory) - .get(PrepublishingPublishSettingsViewModel::class.java) + PublishSettingsFragmentType.EDIT_POST -> ViewModelProvider( + requireActivity(), + viewModelFactory + )[EditPostPublishSettingsViewModel::class.java] + PublishSettingsFragmentType.PREPUBLISHING_NUDGES -> ViewModelProvider( + requireActivity(), + viewModelFactory + )[PrepublishingPublishSettingsViewModel::class.java] null -> error("PublishSettingsViewModel not initialized") } val is24HrFormat = DateFormat.is24HourFormat(activity) val context = ContextThemeWrapper(activity, style.PostSettingsCalendar) - val timePickerDialog = TimePickerDialog( + return TimePickerDialog( context, - OnTimeSetListener { _, selectedHour, selectedMinute -> - viewModel.onTimeSelected(selectedHour, selectedMinute) - }, + { _, selectedHour, selectedMinute -> viewModel.onTimeSelected(selectedHour, selectedMinute) }, viewModel.hour ?: 0, viewModel.minute ?: 0, is24HrFormat ) - return timePickerDialog } override fun onAttach(context: Context) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java index b36ae046d5c8..4bdb6a2d34be 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java @@ -266,7 +266,13 @@ private static String makeExcerpt(String description) { return null; } - String s = HtmlUtils.fastStripHtml(removeWPGallery(description)); + /** + * Remove certain Gutenberg blocks that would display markup, rather than their associated + * media, in the excerpt. + */ + String s = removeWPGallery(description); + s = removeWPVideoPress(s); + s = HtmlUtils.fastStripHtml(s); if (s.length() < MAX_EXCERPT_LEN) { return trimEx(s); } @@ -303,6 +309,13 @@ public static String removeWPGallery(String str) { return str.replaceAll("(?s)", ""); } + /** + * Removes the VideoPress block tag from the given string. + */ + public static String removeWPVideoPress(String str) { + return str.replaceAll("(?s)\\n?", ""); + } + public static String getFormattedDate(PostModel post) { if (PostStatus.fromPost(post) == PostStatus.SCHEDULED) { return DateUtils.formatDateTime(WordPress.getContext(), diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt index 290772f67fbf..49a806cfd899 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt @@ -60,6 +60,8 @@ import org.wordpress.android.ui.utils.UiString import org.wordpress.android.util.AppLog import org.wordpress.android.util.SnackbarItem import org.wordpress.android.util.SnackbarSequencer +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.util.extensions.redirectContextClickToLongPressListener import org.wordpress.android.util.extensions.setLiftOnScrollTargetViewIdAndRequestLayout import org.wordpress.android.viewmodel.observeEvent @@ -165,7 +167,7 @@ class PostsListActivity : LocaleAwareActivity(), } private fun restartWhenSiteHasChanged(intent: Intent) { - val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel + val site = requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE)) if (site.id != this.site.id) { finish() startActivity(intent) @@ -180,14 +182,14 @@ class PostsListActivity : LocaleAwareActivity(), setContentView(root) binding = this - site = if (savedInstanceState == null) { - checkNotNull(intent.getSerializableExtra(WordPress.SITE) as? SiteModel) { - "SiteModel cannot be null, check the PendingIntent starting PostsListActivity" + site = requireNotNull( + if (savedInstanceState == null) { + intent.getSerializableExtraCompat(WordPress.SITE) + } else { + restorePreviousSearch = true + savedInstanceState.getSerializableCompat(WordPress.SITE) } - } else { - restorePreviousSearch = true - savedInstanceState.getSerializable(WordPress.SITE) as SiteModel - } + ) { "SiteModel cannot be null, check the PendingIntent starting PostsListActivity" } val initPreviewState = if (savedInstanceState == null) { PostListRemotePreviewState.NONE @@ -477,8 +479,9 @@ class PostsListActivity : LocaleAwareActivity(), private fun loadIntentData(intent: Intent) { if (intent.hasExtra(ARG_NOTIFICATION_TYPE)) { - val notificationType: NotificationType = - intent.getSerializableExtra(ARG_NOTIFICATION_TYPE) as NotificationType + val notificationType = requireNotNull( + intent.getSerializableExtraCompat(ARG_NOTIFICATION_TYPE) + ) systemNotificationTracker.trackTappedNotification(notificationType) } @@ -537,7 +540,7 @@ class PostsListActivity : LocaleAwareActivity(), override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } else if (item.itemId == R.id.toggle_post_list_item_layout) { viewModel.toggleViewLayout() @@ -580,12 +583,12 @@ class PostsListActivity : LocaleAwareActivity(), private fun PostListActivityBinding.initSearchView() { searchActionButton.setOnActionExpandListener(object : OnActionExpandListener { - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { viewModel.onSearchExpanded(restorePreviousSearch) return true } - override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { viewModel.onSearchCollapsed() return true } @@ -609,10 +612,10 @@ class PostsListActivity : LocaleAwareActivity(), } }) - viewModel.isSearchExpanded.observe(this@PostsListActivity, { isExpanded -> + viewModel.isSearchExpanded.observe(this@PostsListActivity) { isExpanded -> toggleViewLayoutMenuItem.isVisible = !isExpanded toggleSearch(isExpanded) - }) + } } private fun PostListActivityBinding.toggleSearch(isExpanded: Boolean) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingAddCategoryFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingAddCategoryFragment.kt index ed5e7b3b89e0..abcacf66bc18 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingAddCategoryFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingAddCategoryFragment.kt @@ -21,6 +21,7 @@ import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.ActivityUtils import org.wordpress.android.util.ToastUtils import org.wordpress.android.util.ToastUtils.Duration.SHORT +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -137,15 +138,19 @@ class PrepublishingAddCategoryFragment : Fragment(R.layout.prepublishing_add_cat } private fun PrepublishingAddCategoryFragmentBinding.initViewModel() { - viewModel = ViewModelProvider(this@PrepublishingAddCategoryFragment, viewModelFactory) - .get(PrepublishingAddCategoryViewModel::class.java) - parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory) - .get(PrepublishingViewModel::class.java) + viewModel = ViewModelProvider( + this@PrepublishingAddCategoryFragment, + viewModelFactory + )[PrepublishingAddCategoryViewModel::class.java] + parentViewModel = ViewModelProvider( + requireParentFragment(), + viewModelFactory + )[PrepublishingViewModel::class.java] startObserving() val needsRequestLayout = requireArguments().getBoolean(PrepublishingTagsFragment.NEEDS_REQUEST_LAYOUT) - val siteModel = requireArguments().getSerializable(WordPress.SITE) as SiteModel + val siteModel = requireNotNull(arguments?.getSerializableCompat(WordPress.SITE)) viewModel.start(siteModel, !needsRequestLayout) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingBottomSheetFragment.kt index cf6d17cb2e6c..bd1094d0b0ee 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingBottomSheetFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingBottomSheetFragment.kt @@ -28,6 +28,8 @@ import org.wordpress.android.ui.posts.prepublishing.PrepublishingPublishSettings import org.wordpress.android.util.ActivityUtils import org.wordpress.android.util.KeyboardResizeViewUtil import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -133,30 +135,28 @@ class PrepublishingBottomSheetFragment : WPBottomSheetDialogFragment(), } private fun initViewModel(savedInstanceState: Bundle?) { - viewModel = ViewModelProvider(this, viewModelFactory) - .get(PrepublishingViewModel::class.java) + viewModel = ViewModelProvider(this, viewModelFactory)[PrepublishingViewModel::class.java] - viewModel.navigationTarget.observeEvent(this, { navigationState -> + viewModel.navigationTarget.observeEvent(this) { navigationState -> navigateToScreen(navigationState) - }) + } - viewModel.dismissBottomSheet.observeEvent(this, { + viewModel.dismissBottomSheet.observeEvent(this) { dismiss() - }) + } - viewModel.triggerOnSubmitButtonClickedListener.observeEvent(this, { publishPost -> + viewModel.triggerOnSubmitButtonClickedListener.observeEvent(this) { publishPost -> prepublishingBottomSheetListener?.onSubmitButtonClicked(publishPost) - }) + } - viewModel.dismissKeyboard.observeEvent(this, { + viewModel.dismissKeyboard.observeEvent(this) { ActivityUtils.hideKeyboardForced(view) - }) + } - val prepublishingScreenState = savedInstanceState?.getParcelable( + val prepublishingScreenState = savedInstanceState?.getParcelableCompat( KEY_SCREEN_STATE ) - val site = arguments?.getSerializable(SITE) as SiteModel - + val site = requireNotNull(arguments?.getSerializableCompat(SITE)) viewModel.start(site, prepublishingScreenState) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingCategoriesFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingCategoriesFragment.kt index 509fe77e4fb3..00d787313280 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingCategoriesFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PrepublishingCategoriesFragment.kt @@ -24,6 +24,7 @@ import org.wordpress.android.ui.posts.PrepublishingHomeItemUiState.ActionType.AD import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.ToastUtils import org.wordpress.android.util.ToastUtils.Duration.SHORT +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -116,14 +117,17 @@ class PrepublishingCategoriesFragment : Fragment(R.layout.prepublishing_categori } private fun PrepublishingCategoriesFragmentBinding.initViewModel() { - viewModel = ViewModelProvider(this@PrepublishingCategoriesFragment, viewModelFactory) - .get(PrepublishingCategoriesViewModel::class.java) - parentViewModel = ViewModelProvider(requireParentFragment(), viewModelFactory) - .get(PrepublishingViewModel::class.java) + viewModel = ViewModelProvider( + this@PrepublishingCategoriesFragment, + viewModelFactory + )[PrepublishingCategoriesViewModel::class.java] + parentViewModel = ViewModelProvider( + requireParentFragment(), + viewModelFactory + )[PrepublishingViewModel::class.java] startObserving() - val siteModel = requireArguments().getSerializable(WordPress.SITE) as SiteModel - val addCategoryRequest: PrepublishingAddCategoryRequest? = - arguments?.getSerializable(ADD_CATEGORY_REQUEST) as? PrepublishingAddCategoryRequest + val siteModel = requireNotNull(arguments?.getSerializableCompat(WordPress.SITE)) + val addCategoryRequest = arguments?.getSerializableCompat(ADD_CATEGORY_REQUEST) val selectedCategoryIds: List = arguments?.getLongArray(SELECTED_CATEGORY_IDS)?.toList() ?: listOf() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishSettingsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishSettingsFragment.kt index 635a5ea439de..3bb76562cda1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishSettingsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PublishSettingsFragment.kt @@ -79,8 +79,7 @@ abstract class PublishSettingsFragment : Fragment() { private fun observeOnAddToCalendar() { viewModel.onAddToCalendar.observeEvent(viewLifecycleOwner) { calendarEvent -> val calIntent = Intent(Intent.ACTION_INSERT) - calIntent.data = Events.CONTENT_URI - calIntent.type = "vnd.android.cursor.item/event" + calIntent.setDataAndType(Events.CONTENT_URI, "vnd.android.cursor.item/event") calIntent.putExtra(Events.TITLE, calendarEvent.title) calIntent.putExtra(Events.DESCRIPTION, calendarEvent.description) calIntent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, calendarEvent.startTime) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/SelectCategoriesActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/SelectCategoriesActivity.java index f71f0af6dcaf..3042107eacc8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/SelectCategoriesActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/SelectCategoriesActivity.java @@ -10,6 +10,7 @@ import android.widget.ListView; import android.widget.TextView; +import androidx.activity.OnBackPressedCallback; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; @@ -35,6 +36,7 @@ import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.ToastUtils.Duration; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import org.wordpress.android.util.helpers.ListScrollPositionManager; import org.wordpress.android.util.helpers.SwipeToRefreshHelper; import org.wordpress.android.util.helpers.SwipeToRefreshHelper.RefreshListener; @@ -69,6 +71,16 @@ public class SelectCategoriesActivity extends LocaleAwareActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((WordPress) getApplication()).component().inject(this); + + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + saveAndFinish(); + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + mDispatcher.register(this); if (savedInstanceState == null) { @@ -237,12 +249,6 @@ private void refreshCategories() { mDispatcher.dispatch(TaxonomyActionBuilder.newFetchCategoriesAction(mSite)); } - @Override - public void onBackPressed() { - saveAndFinish(); - super.onBackPressed(); - } - private void updateSelectedCategoryList() { SparseBooleanArray selectedItems = mListView.getCheckedItemPositions(); for (int i = 0; i < selectedItems.size(); i++) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.java index 21dba8597cc6..5a3e3a03a569 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.java @@ -12,6 +12,7 @@ import java.util.regex.Matcher; import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaUploadCompletionProcessorPatterns.PATTERN_BLOCK_CAPTURES; +import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaUploadCompletionProcessorPatterns.PATTERN_SELF_CLOSING_BLOCK_CAPTURES; /** * Abstract class to be extended for each enumerated {@link MediaBlockType}. @@ -30,6 +31,7 @@ public abstract class BlockProcessor { String mLocalId; String mRemoteId; String mRemoteUrl; + String mRemoteGuid; private String mBlockName; private JsonObject mJsonAttributes; @@ -46,6 +48,7 @@ public abstract class BlockProcessor { mRemoteId = mediaFile.getMediaId(); mRemoteUrl = org.wordpress.android.util.StringUtils.notNullStr(Utils.escapeQuotes(mediaFile .getOptimalFileURL())); + mRemoteGuid = mediaFile.getVideoPressGuid(); } private JsonObject parseJson(String blockJson) { @@ -60,16 +63,18 @@ private Document parseHTML(String blockContent) { return document; } - private boolean splitBlock(String block) { - Matcher captures = PATTERN_BLOCK_CAPTURES.matcher(block); + private boolean splitBlock(String block, Boolean isSelfClosingTag) { + Matcher captures = ( + isSelfClosingTag ? PATTERN_SELF_CLOSING_BLOCK_CAPTURES : PATTERN_BLOCK_CAPTURES + ).matcher(block); boolean capturesFound = captures.find(); if (capturesFound) { mBlockName = captures.group(1); mJsonAttributes = parseJson(captures.group(2)); - mBlockContentDocument = parseHTML(captures.group(3)); - mClosingComment = captures.group(4); + mBlockContentDocument = isSelfClosingTag ? null : parseHTML(captures.group(3)); + mClosingComment = isSelfClosingTag ? null : captures.group(4); return true; } else { mBlockName = null; @@ -85,12 +90,22 @@ private boolean splitBlock(String block) { * method should return the original block contents unchanged. * * @param block The raw block contents + * @param isSelfClosingTag True if the block tag is self-closing (e.g. ) * @return A string containing content with ids and urls replaced */ - String processBlock(String block) { - if (splitBlock(block)) { + String processBlock(String block, Boolean isSelfClosingTag) { + if (splitBlock(block, isSelfClosingTag)) { if (processBlockJsonAttributes(mJsonAttributes)) { - if (processBlockContentDocument(mBlockContentDocument)) { + if (isSelfClosingTag) { + // return injected block + return new StringBuilder() + .append("") + .toString(); + } else if (processBlockContentDocument(mBlockContentDocument)) { // return injected block return new StringBuilder() .append(""); + if (isSelfClosingTag) { + positionBlockEnd = headerMatcher.end(); + } else { + Matcher blockBoundaryMatcher = + Pattern.compile(String.format(PATTERN_TEMPLATE_BLOCK_BOUNDARY, blockType), + Pattern.DOTALL).matcher(content.substring(headerMatcher.end())); - int nestLevel = 1; + int nestLevel = 1; - while (0 < nestLevel && blockBoundaryMatcher.find()) { - if (blockBoundaryMatcher.group(1).equals("/")) { - positionBlockEnd = headerMatcher.end() + blockBoundaryMatcher.end(); - nestLevel--; - } else { - nestLevel++; + while (0 < nestLevel && blockBoundaryMatcher.find()) { + if (blockBoundaryMatcher.group(1).equals("/")) { + positionBlockEnd = headerMatcher.end() + blockBoundaryMatcher.end(); + nestLevel--; + } else { + nestLevel++; + } } } return new StringBuilder() .append(content.substring(0, positionBlockStart)) - .append(processBlock(content.substring(positionBlockStart, positionBlockEnd))) + .append(processBlock(content.substring(positionBlockStart, positionBlockEnd), isSelfClosingTag)) .append(processContent(content.substring(positionBlockEnd))) .toString(); } else { @@ -70,12 +77,12 @@ public String processContent(String content) { * @param block The raw block contents * @return A string containing content with ids and urls replaced */ - private String processBlock(String block) { + private String processBlock(String block, Boolean isSelfClosingTag) { final MediaBlockType blockType = MediaBlockType.detectBlockType(block); final BlockProcessor blockProcessor = mBlockProcessorFactory.getProcessorForMediaBlockType(blockType); if (blockProcessor != null) { - return blockProcessor.processBlock(block); + return blockProcessor.processBlock(block, isSelfClosingTag); } return block; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorPatterns.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorPatterns.java index 643fada1f527..0750256253ec 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorPatterns.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorPatterns.java @@ -11,7 +11,7 @@ public class MediaUploadCompletionProcessorPatterns { public static final Pattern PATTERN_BLOCK_HEADER = Pattern.compile(new StringBuilder() .append(PATTERN_BLOCK_PREFIX) .append(MediaBlockType.getMatchingGroup()) - .append(").*? -->\n?") + .append(").*? (/?-->)\n?") .toString(), Pattern.DOTALL); /** @@ -39,4 +39,11 @@ public class MediaUploadCompletionProcessorPatterns { .append("(.*)") // group: html content .append("(.*)") // group: closing-comment (name must match group 1: block type) .toString(), Pattern.DOTALL); + + public static final Pattern PATTERN_SELF_CLOSING_BLOCK_CAPTURES = Pattern.compile(new StringBuilder() + .append(PATTERN_BLOCK_PREFIX) // start-of-group: block type + .append(MediaBlockType.getMatchingGroup()) + .append(")") // end-of-group: block type + .append(" (\\{.*?\\}) /-->\n?") // group: block header json + .toString(), Pattern.DOTALL); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoPressBlockProcessor.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoPressBlockProcessor.kt new file mode 100644 index 000000000000..449055b67c96 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoPressBlockProcessor.kt @@ -0,0 +1,38 @@ +package org.wordpress.android.ui.posts.mediauploadcompletionprocessors + +import com.google.gson.JsonObject +import org.jsoup.nodes.Document +import org.wordpress.android.util.helpers.MediaFile + +class VideoPressBlockProcessor( + localId: String?, + mediaFile: MediaFile? +) : BlockProcessor(localId, mediaFile) { + override fun processBlockContentDocument(document: Document?): Boolean { + return false + } + + override fun processBlockJsonAttributes(jsonAttributes: JsonObject?): Boolean { + val id = jsonAttributes?.get(ID_ATTRIBUTE) + val src = jsonAttributes?.get(SRC_ATTRIBUTE)?.asString + + return if (id != null && !id.isJsonNull && id.asString == mLocalId) { + jsonAttributes.apply { + addProperty(ID_ATTRIBUTE, Integer.parseInt(mRemoteId)) + addProperty(GUID_ATTRIBUTE, mRemoteGuid) + if (src?.startsWith("file:") == true) { + remove(SRC_ATTRIBUTE) + } + } + true + } else { + false + } + } + + companion object { + const val ID_ATTRIBUTE = "id" + const val GUID_ATTRIBUTE = "guid" + const val SRC_ATTRIBUTE = "src" + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/reactnative/ReactNativeRequestHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/reactnative/ReactNativeRequestHandler.kt index 446bdb0f63c4..f39fb91a6a16 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/reactnative/ReactNativeRequestHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/reactnative/ReactNativeRequestHandler.kt @@ -31,7 +31,20 @@ class ReactNativeRequestHandler @Inject constructor( onError: Consumer ) { launch { - val response = reactNativeStore.executeRequest(mSite, pathWithParams, enableCaching) + val response = reactNativeStore.executeGetRequest(mSite, pathWithParams, enableCaching) + handleResponse(response, onSuccess::accept, onError::accept) + } + } + + fun performPostRequest( + pathWithParams: String, + body: Map, + mSite: SiteModel, + onSuccess: Consumer, + onError: Consumer + ) { + launch { + val response = reactNativeStore.executePostRequest(mSite, pathWithParams, body) handleResponse(response, onSuccess::accept, onError::accept) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java index c5b9fb9d979a..89f4e78fbbc5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AccountSettingsActivity.java @@ -9,6 +9,9 @@ import org.wordpress.android.R; import org.wordpress.android.ui.LocaleAwareActivity; +import dagger.hilt.android.AndroidEntryPoint; + +@AndroidEntryPoint public class AccountSettingsActivity extends LocaleAwareActivity { @Override public void onCreate(Bundle savedInstanceState) { @@ -29,7 +32,7 @@ public void onCreate(Bundle savedInstanceState) { @Override public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index cf707b5afd0d..eaf7ec019ec3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -185,6 +185,12 @@ public enum DeletablePrefKey implements PrefKey { SHOULD_HIDE_JETPACK_INSTALL_FULL_PLUGIN_CARD, SHOULD_SHOW_JETPACK_FULL_PLUGIN_INSTALL_ONBOARDING, SHOULD_HIDE_PROMOTE_WITH_BLAZE_CARD, + SHOULD_HIDE_DASHBOARD_DOMAIN_CARD, + + // Jetpack Individual Plugin overlay for WordPress app + WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_SHOWN_COUNT, + WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_LAST_SHOWN_TIMESTAMP, + NOTIFICATIONS_PERMISSION_WARNING_DISMISSED, } /** @@ -228,8 +234,13 @@ public enum UndeletablePrefKey implements PrefKey { // permission keys - set once a specific permission has been asked, regardless of response ASKED_PERMISSION_STORAGE_WRITE, ASKED_PERMISSION_STORAGE_READ, + ASKED_PERMISSION_IMAGES_READ, + ASKED_PERMISSION_VIDEO_READ, + ASKED_PERMISSION_AUDIO_READ, ASKED_PERMISSION_CAMERA, + ASKED_PERMISSION_NOTIFICATIONS, + // Updated after WP.com themes have been fetched LAST_WP_COM_THEMES_SYNC, @@ -1612,4 +1623,41 @@ public static void setShouldHidePromoteWithBlazeCard(long siteId, final boolean @NonNull private static String getSiteIdHideBlazeKey(long siteId) { return DeletablePrefKey.SHOULD_HIDE_PROMOTE_WITH_BLAZE_CARD.name() + siteId; } + + public static Boolean getShouldHideDashboardDomainCard(long siteId) { + return prefs().getBoolean(getSiteIdHideDashboardDomainCardKey(siteId), false); + } + + public static void setShouldHideDashboardDomainCard(long siteId, final boolean isHidden) { + prefs().edit().putBoolean(getSiteIdHideDashboardDomainCardKey(siteId), isHidden).apply(); + } + + @NonNull private static String getSiteIdHideDashboardDomainCardKey(long siteId) { + return DeletablePrefKey.SHOULD_HIDE_DASHBOARD_DOMAIN_CARD.name() + siteId; + } + + public static int getWPJetpackIndividualPluginOverlayShownCount() { + return getInt(DeletablePrefKey.WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_SHOWN_COUNT, 0); + } + + public static void incrementWPJetpackIndividualPluginOverlayShownCount() { + int count = getWPJetpackIndividualPluginOverlayShownCount(); + setInt(DeletablePrefKey.WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_SHOWN_COUNT, count + 1); + } + + public static long getWPJetpackIndividualPluginOverlayLastShownTimestamp() { + return getLong(DeletablePrefKey.WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_LAST_SHOWN_TIMESTAMP, 0); + } + + public static void setWPJetpackIndividualPluginOverlayLastShownTimestamp(long timestamp) { + setLong(DeletablePrefKey.WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_LAST_SHOWN_TIMESTAMP, timestamp); + } + + public static boolean getNotificationsPermissionsWarningDismissed() { + return getBoolean(DeletablePrefKey.NOTIFICATIONS_PERMISSION_WARNING_DISMISSED, false); + } + + public static void setNotificationsPermissionWarningDismissed(boolean dismissed) { + setBoolean(DeletablePrefKey.NOTIFICATIONS_PERMISSION_WARNING_DISMISSED, dismissed); + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt index a76875ae9cef..c239d7edd8a5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefsWrapper.kt @@ -86,6 +86,17 @@ class AppPrefsWrapper @Inject constructor() { get() = AppPrefs.shouldScheduleCreateSiteNotification() set(shouldSchedule) = AppPrefs.setShouldScheduleCreateSiteNotification(shouldSchedule) + val wpJetpackIndividualPluginOverlayShownCount: Int + get() = AppPrefs.getWPJetpackIndividualPluginOverlayShownCount() + + var wpJetpackIndividualPluginOverlayLastShownTimestamp: Long + get() = AppPrefs.getWPJetpackIndividualPluginOverlayLastShownTimestamp() + set(timestamp) = AppPrefs.setWPJetpackIndividualPluginOverlayLastShownTimestamp(timestamp) + + var notificationPermissionsWarningDismissed: Boolean + get() = AppPrefs.getNotificationsPermissionsWarningDismissed() + set(dismissed) = AppPrefs.setNotificationsPermissionWarningDismissed(dismissed) + fun getAppWidgetSiteId(appWidgetId: Int) = AppPrefs.getStatsWidgetSelectedSiteId(appWidgetId) fun setAppWidgetSiteId(siteId: Long, appWidgetId: Int) = AppPrefs.setStatsWidgetSelectedSiteId(siteId, appWidgetId) fun removeAppWidgetSiteId(appWidgetId: Int) = AppPrefs.removeStatsWidgetSelectedSiteId(appWidgetId) @@ -327,6 +338,15 @@ class AppPrefsWrapper @Inject constructor() { fun setShouldHidePromoteWithBlazeCard(siteId: Long, isHidden: Boolean) = AppPrefs.setShouldHidePromoteWithBlazeCard(siteId, isHidden) + fun getShouldHideDashboardDomainCard(siteId: Long): Boolean = + AppPrefs.getShouldHideDashboardDomainCard(siteId) + + fun setShouldHideDashboardDomainCard(siteId: Long, isHidden: Boolean) = + AppPrefs.setShouldHideDashboardDomainCard(siteId, isHidden) + + fun incrementWPJetpackIndividualPluginOverlayShownCount() = + AppPrefs.incrementWPJetpackIndividualPluginOverlayShownCount() + fun getAllPrefs(): Map = AppPrefs.getAllPrefs() fun setString(prefKey: PrefKey, value: String) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java index 19066a83c25f..37a291651a38 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppSettingsActivity.java @@ -35,7 +35,7 @@ public void onCreate(Bundle savedInstanceState) { @Override public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java index b16ea68032ef..03e50ff2e6b1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/MyProfileActivity.java @@ -39,7 +39,7 @@ public void onCreate(Bundle savedInstanceState) { @Override public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java index 5637b6a4abdf..723203d04c49 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/NumberPickerDialog.java @@ -146,7 +146,7 @@ public void setNumberFormat(NumberPicker.Formatter format) { } private View getDialogTitleView(String title) { - LayoutInflater inflater = LayoutInflater.from(getActivity()); + LayoutInflater inflater = getActivity().getLayoutInflater(); @SuppressLint("InflateParams") View titleView = inflater.inflate(R.layout.detail_list_preference_title, null); TextView titleText = titleView.findViewById(R.id.title); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java index 102a1d401dd1..d4fc63548af1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsFragment.java @@ -558,8 +558,8 @@ public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference prefere AnalyticsUtils.trackWithSiteDetails(AnalyticsTracker.Stat.SITE_SETTINGS_START_OVER_ACCESSED, mSite); - if (mSite.getHasFreePlan()) { - // Don't show the start over detail screen for free users, instead show the support page + if (!BuildConfig.IS_JETPACK_APP || mSite.getHasFreePlan()) { + // Don't show the start over detail screen for free users or WP app users, instead show the support page dialog.dismiss(); WPWebViewActivity.openUrlByUsingGlobalWPCOMCredentials(getActivity(), WORDPRESS_EMPTY_SITE_SUPPORT_URL); } else { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsTagListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsTagListActivity.java index 3b567c9c4a0c..fa56f4935322 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsTagListActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/SiteSettingsTagListActivity.java @@ -16,6 +16,7 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -50,6 +51,7 @@ import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import org.wordpress.android.util.extensions.ViewExtensionsKt; import java.util.ArrayList; @@ -98,6 +100,24 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.site_settings_tag_list_activity); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (getFragmentManager().getBackStackEntryCount() > 0) { + SiteSettingsTagDetailFragment fragment = getDetailFragment(); + if (fragment != null && fragment.hasChanges()) { + saveTag(fragment.getTerm(), fragment.isNewTerm()); + } else { + hideDetailFragment(); + loadTags(); + } + } else { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + Toolbar toolbar = findViewById(R.id.toolbar_main); setSupportActionBar(toolbar); @@ -217,28 +237,13 @@ public boolean onCreateOptionsMenu(Menu menu) { @Override public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } else { return super.onOptionsItemSelected(item); } } - @Override - public void onBackPressed() { - if (getFragmentManager().getBackStackEntryCount() > 0) { - SiteSettingsTagDetailFragment fragment = getDetailFragment(); - if (fragment != null && fragment.hasChanges()) { - saveTag(fragment.getTerm(), fragment.isNewTerm()); - } else { - hideDetailFragment(); - loadTags(); - } - } else { - super.onBackPressed(); - } - } - private void showFabIfHidden() { // redisplay hidden fab after a short delay long delayMs = getResources().getInteger(R.integer.fab_animation_delay); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/detail/CategoryDetailActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/detail/CategoryDetailActivity.kt index d7a9c347a051..80d8e1d9a497 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/detail/CategoryDetailActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/detail/CategoryDetailActivity.kt @@ -22,7 +22,7 @@ class CategoryDetailActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/list/CategoriesListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/list/CategoriesListActivity.kt index 5cf95ad7e8d2..ace469515a1f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/list/CategoriesListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/list/CategoriesListActivity.kt @@ -22,7 +22,7 @@ class CategoriesListActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/list/CategoriesListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/list/CategoriesListFragment.kt index 78eb781534f4..3f1b5666f233 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/list/CategoriesListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/categories/list/CategoriesListFragment.kt @@ -17,6 +17,8 @@ import org.wordpress.android.ui.prefs.categories.list.CategoryDetailNavigation.E import org.wordpress.android.ui.prefs.categories.list.UiState.Content import org.wordpress.android.ui.prefs.categories.list.UiState.Loading import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import javax.inject.Inject @AndroidEntryPoint @@ -68,11 +70,14 @@ class CategoriesListFragment : Fragment(R.layout.site_settings_categories_list_f } private fun getSite(savedInstanceState: Bundle?): SiteModel { - return if (savedInstanceState == null) { - requireActivity().intent.getSerializableExtra(WordPress.SITE) as SiteModel - } else { - savedInstanceState.getSerializable(WordPress.SITE) as SiteModel - } + val site = requireNotNull( + if (savedInstanceState == null) { + requireActivity().intent.getSerializableExtraCompat(WordPress.SITE) + } else { + savedInstanceState.getSerializableCompat(WordPress.SITE) + } + ) + return site } private fun SiteSettingsCategoriesListFragmentBinding.setupObservers() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/homepage/HomepageSettingsDialog.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/homepage/HomepageSettingsDialog.kt index 4f15c7c921d0..58525591ee05 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/homepage/HomepageSettingsDialog.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/homepage/HomepageSettingsDialog.kt @@ -39,9 +39,9 @@ class HomepageSettingsDialog : DialogFragment() { var pageForPostsId: Long? = null (arguments ?: savedInstanceState)?.let { bundle -> siteId = bundle.getInt(KEY_SITE_ID) - isClassicBlog = bundle.get(KEY_IS_CLASSIC_BLOG)?.let { it as Boolean } - pageOnFrontId = bundle.get(KEY_PAGE_ON_FRONT)?.let { it as Long } - pageForPostsId = bundle.get(KEY_PAGE_FOR_POSTS)?.let { it as Long } + isClassicBlog = bundle.getBoolean(KEY_IS_CLASSIC_BLOG) + pageOnFrontId = bundle.getLong(KEY_PAGE_ON_FRONT) + pageForPostsId = bundle.getLong(KEY_PAGE_FOR_POSTS) } ?: throw IllegalArgumentException("Site has to be initialized") val builder = MaterialAlertDialogBuilder(requireActivity()) builder.setPositiveButton(R.string.site_settings_accept_homepage) { _, _ -> } @@ -55,8 +55,10 @@ class HomepageSettingsDialog : DialogFragment() { } builder.setView(root) - viewModel = ViewModelProvider(this@HomepageSettingsDialog, viewModelFactory) - .get(HomepageSettingsViewModel::class.java) + viewModel = ViewModelProvider( + this@HomepageSettingsDialog, + viewModelFactory + )[HomepageSettingsViewModel::class.java] viewModel.uiState.observe(this@HomepageSettingsDialog) { uiState -> uiState?.let { loadingPages.visibility = if (uiState.isLoading) View.VISIBLE else View.GONE diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.kt index 6f0a71f774a9..09d5663e78e1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/notifications/NotificationsSettingsActivity.kt @@ -76,7 +76,7 @@ class NotificationsSettingsActivity : LocaleAwareActivity(), MainSwitchToolbarLi override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeListActivity.java index 0c8a7d88bdea..f81060a7077e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeListActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/publicize/PublicizeListActivity.java @@ -4,6 +4,7 @@ import android.os.Bundle; import android.view.MenuItem; +import androidx.activity.OnBackPressedCallback; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; @@ -38,6 +39,7 @@ import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; import org.wordpress.android.util.extensions.AppBarLayoutExtensionsKt; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import java.util.HashMap; import java.util.Map; @@ -67,6 +69,18 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.publicize_list_activity); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (getSupportFragmentManager().getBackStackEntryCount() > 0) { + getSupportFragmentManager().popBackStack(); + } else { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + Toolbar toolbar = findViewById(R.id.toolbar_main); setSupportActionBar(toolbar); @@ -209,21 +223,12 @@ private void closeWebViewFragment() { public boolean onOptionsItemSelected(final MenuItem item) { int itemId = item.getItemId(); if (itemId == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); } - @Override - public void onBackPressed() { - if (getSupportFragmentManager().getBackStackEntryCount() > 0) { - getSupportFragmentManager().popBackStack(); - } else { - super.onBackPressed(); - } - } - /* * user tapped a service in the list fragment */ 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 8644943757db..dc31dc7464c1 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 @@ -35,7 +35,6 @@ public class PublicizeWebViewFragment extends PublicizeBaseFragment { private int mConnectionId; private WebView mWebView; private ProgressBar mProgress; - private View mNestedScrollView; @Inject AccountStore mAccountStore; @@ -98,7 +97,6 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mProgress = rootView.findViewById(R.id.progress); mWebView = rootView.findViewById(R.id.webView); - mNestedScrollView = rootView.findViewById(R.id.publicize_webview_nested_scroll_view); mWebView.setWebViewClient(new PublicizeWebViewClient()); mWebView.setWebChromeClient(new PublicizeWebChromeClient()); @@ -127,7 +125,7 @@ public void onResume() { super.onResume(); setNavigationIcon(R.drawable.ic_close_white_24dp); if (getActivity() instanceof ScrollableViewInitializedListener) { - ((ScrollableViewInitializedListener) getActivity()).onScrollableViewInitialized(mNestedScrollView.getId()); + ((ScrollableViewInitializedListener) getActivity()).onScrollableViewInitialized(mWebView.getId()); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthFragment.kt index 1398c43c489d..2ecb5e21c959 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/qrcodeauth/QRCodeAuthFragment.kt @@ -4,7 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -115,14 +115,9 @@ class QRCodeAuthFragment : Fragment() { } private fun initBackPressHandler() { - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - qrCodeAuthViewModel.onBackPressed() - } - } - ) + requireActivity().onBackPressedDispatcher.addCallback(this) { + qrCodeAuthViewModel.onBackPressed() + } } override fun onSaveInstanceState(outState: Bundle) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartFullScreenDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartFullScreenDialogFragment.kt index 6b36462f4e75..6b13a36ff9a7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartFullScreenDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/quickstart/QuickStartFullScreenDialogFragment.kt @@ -15,7 +15,6 @@ import org.wordpress.android.fluxc.store.QuickStartStore import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartNewSiteTask.CREATE_SITE import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType -import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType.UNKNOWN import org.wordpress.android.ui.FullScreenDialogFragment.FullScreenDialogContent import org.wordpress.android.ui.FullScreenDialogFragment.FullScreenDialogController import org.wordpress.android.ui.mysite.SelectedSiteRepository @@ -28,6 +27,7 @@ import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.util.DisplayUtilsWrapper import org.wordpress.android.util.QuickStartUtils.getQuickStartListSkippedTracker import org.wordpress.android.util.QuickStartUtils.getQuickStartListTappedTracker +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.widgets.WPSnackbar.Companion.make import java.io.Serializable import javax.inject.Inject @@ -74,7 +74,7 @@ class QuickStartFullScreenDialogFragment : Fragment(R.layout.quick_start_dialog_ } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - tasksType = arguments?.getSerializable(EXTRA_TYPE) as QuickStartTaskType? ?: QuickStartTaskType.UNKNOWN + tasksType = arguments?.getSerializableCompat(EXTRA_TYPE) ?: QuickStartTaskType.UNKNOWN quickStartTracker.trackQuickStartListViewed(tasksType) binding.setupQuickStartList() } @@ -150,7 +150,7 @@ class QuickStartFullScreenDialogFragment : Fragment(R.layout.quick_start_dialog_ ) private fun buildTaskCards(): List { - val tasks = QuickStartTask.getTasksByTaskType(tasksType).filterNot { it.taskType == UNKNOWN } + val tasks = QuickStartTask.getTasksByTaskType(tasksType).filterNot { it.taskType == QuickStartTaskType.UNKNOWN } val selectedSiteLocalId = selectedSiteRepository.getSelectedSiteLocalId().toLong() val tasksCompleted = quickStartStore.getCompletedTasksByType(selectedSiteLocalId, tasksType) return tasks.mapToQuickStartTaskCard(tasksCompleted) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java index 754090bed0eb..82e689be2cd9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderCommentListActivity.java @@ -18,6 +18,7 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -39,7 +40,6 @@ import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.wordpress.android.R; -import org.wordpress.android.WordPress; import org.wordpress.android.analytics.AnalyticsTracker; import org.wordpress.android.analytics.AnalyticsTracker.Stat; import org.wordpress.android.datasets.ReaderCommentTable; @@ -86,10 +86,11 @@ import org.wordpress.android.util.EditTextUtils; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; -import org.wordpress.android.util.extensions.ViewExtensionsKt; import org.wordpress.android.util.WPActivityUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; import org.wordpress.android.util.analytics.AnalyticsUtils.AnalyticsCommentActionSource; +import org.wordpress.android.util.extensions.CompatExtensionsKt; +import org.wordpress.android.util.extensions.ViewExtensionsKt; import org.wordpress.android.util.helpers.SwipeToRefreshHelper; import org.wordpress.android.widgets.RecyclerItemDecoration; import org.wordpress.android.widgets.SuggestionAutoCompleteText; @@ -106,8 +107,10 @@ import static org.wordpress.android.ui.reader.FollowConversationUiStateKt.FOLLOW_CONVERSATION_UI_STATE_FLAGS_KEY; import static org.wordpress.android.util.WPSwipeToRefreshHelper.buildSwipeToRefreshHelper; +import dagger.hilt.android.AndroidEntryPoint; import kotlin.Unit; +@AndroidEntryPoint public class ReaderCommentListActivity extends LocaleAwareActivity implements OnConfirmListener, OnCollapseListener { private static final String KEY_REPLY_TO_COMMENT_ID = "reply_to_comment_id"; @@ -150,24 +153,26 @@ public class ReaderCommentListActivity extends LocaleAwareActivity implements On private ReaderCommentListViewModel mViewModel; private ConversationNotificationsViewModel mConversationViewModel; - @Override - public void onBackPressed() { - CollapseFullScreenDialogFragment fragment = (CollapseFullScreenDialogFragment) - getSupportFragmentManager().findFragmentByTag(CollapseFullScreenDialogFragment.TAG); - - if (fragment != null) { - fragment.onBackPressed(); - } else { - super.onBackPressed(); - } - } - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ((WordPress) getApplication()).component().inject(this); setContentView(R.layout.reader_activity_comment_list); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + CollapseFullScreenDialogFragment fragment = (CollapseFullScreenDialogFragment) + getSupportFragmentManager().findFragmentByTag(CollapseFullScreenDialogFragment.TAG); + + if (fragment != null) { + fragment.collapse(); + } else { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + Toolbar toolbar = findViewById(R.id.toolbar_main); setSupportActionBar(toolbar); @@ -248,9 +253,7 @@ public void afterTextChanged(Editable s) { mSuggestionServiceConnectionManager, mPost.isWP() ); - if (mSuggestionAdapter != null) { - mEditComment.setAdapter(mSuggestionAdapter); - } + mEditComment.setAdapter(mSuggestionAdapter); mReaderTracker.trackPost(AnalyticsTracker.Stat.READER_ARTICLE_COMMENTS_OPENED, mPost, mSource); @@ -336,8 +339,8 @@ private void initObservers(Bundle savedInstanceState) { snackbarMessageHolderEvent.applyIfNotHandled(holder -> { WPSnackbar.make(mCoordinator, - mUiHelpers.getTextOfUiString(ReaderCommentListActivity.this, holder.getMessage()), - Snackbar.LENGTH_LONG) + mUiHelpers.getTextOfUiString(ReaderCommentListActivity.this, holder.getMessage()), + Snackbar.LENGTH_LONG) .setAction( holder.getButtonTitle() != null ? mUiHelpers.getTextOfUiString( @@ -805,8 +808,8 @@ private void doDirectOperation() { getCommentAdapter().setHighlightCommentId(mCommentId, false); if (!mAccountStore.hasAccessToken()) { WPSnackbar.make(mCoordinator, - R.string.reader_snackbar_err_cannot_like_post_logged_out, - Snackbar.LENGTH_INDEFINITE) + R.string.reader_snackbar_err_cannot_like_post_logged_out, + Snackbar.LENGTH_INDEFINITE) .setAction(R.string.sign_in, mSignInClickListener) .show(); } else { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt index 706d83a6b680..28b304990b40 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderFragment.kt @@ -24,6 +24,7 @@ import org.greenrobot.eventbus.ThreadMode.MAIN import org.wordpress.android.R import org.wordpress.android.R.string import org.wordpress.android.databinding.ReaderFragmentLayoutBinding +import org.wordpress.android.models.JetpackPoweredScreen import org.wordpress.android.models.ReaderTagList import org.wordpress.android.ui.ScrollableViewInitializedListener import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayFragment @@ -44,7 +45,6 @@ import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState. import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.ui.utils.UiString.UiStringText import org.wordpress.android.util.JetpackBrandingUtils -import org.wordpress.android.models.JetpackPoweredScreen import org.wordpress.android.util.QuickStartUtilsWrapper import org.wordpress.android.util.SnackbarItem import org.wordpress.android.util.SnackbarItem.Action @@ -126,11 +126,11 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), MenuProvider, } menu.findItem(R.id.menu_settings).apply { settingsMenuItem = this - settingsMenuItemFocusPoint = this.actionView.findViewById(R.id.menu_quick_start_focus_point) + settingsMenuItemFocusPoint = this.actionView?.findViewById(R.id.menu_quick_start_focus_point) this.isVisible = viewModel.uiState.value?.settingsMenuItemUiState?.isVisible ?: false settingsMenuItemFocusPoint?.isVisible = viewModel.uiState.value?.settingsMenuItemUiState?.showQuickStartFocusPoint ?: false - this.actionView.setOnClickListener { viewModel.onSettingsActionClicked() } + this.actionView?.setOnClickListener { viewModel.onSettingsActionClicked() } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java index 3fa2eedc8b54..9e8dc104c46c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPhotoViewerActivity.java @@ -158,7 +158,7 @@ public void onAnimationRepeat(Animation animation) { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt index f090619b06e0..4485c9e38b5d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.kt @@ -132,6 +132,8 @@ import org.wordpress.android.util.WPSwipeToRefreshHelper.buildSwipeToRefreshHelp import org.wordpress.android.util.config.CommentsSnippetFeatureConfig import org.wordpress.android.util.config.LikesEnhancementsFeatureConfig import org.wordpress.android.util.extensions.getColorFromAttribute +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.extensions.isDarkTheme import org.wordpress.android.util.extensions.setVisible import org.wordpress.android.util.helpers.SwipeToRefreshHelper @@ -317,12 +319,14 @@ class ReaderPostDetailFragment : ViewPagerFragment(), if (args != null) { blogId = args.getLong(ReaderConstants.ARG_BLOG_ID) postId = args.getLong(ReaderConstants.ARG_POST_ID) - directOperation = args.getSerializable(ReaderConstants.ARG_DIRECT_OPERATION) as? DirectOperation + directOperation = args.getSerializableCompat(ReaderConstants.ARG_DIRECT_OPERATION) commentId = args.getInt(ReaderConstants.ARG_COMMENT_ID) isRelatedPost = args.getBoolean(ReaderConstants.ARG_IS_RELATED_POST) interceptedUri = args.getString(ReaderConstants.ARG_INTERCEPTED_URI) if (args.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) { - this.postListType = args.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE) as ReaderPostListType + postListType = requireNotNull( + args.getSerializableCompat(ReaderConstants.ARG_POST_LIST_TYPE) + ) } postSlugsResolutionUnderway = args.getBoolean(ReaderConstants.KEY_POST_SLUGS_RESOLUTION_UNDERWAY) } @@ -400,7 +404,7 @@ class ReaderPostDetailFragment : ViewPagerFragment(), toolBar.setTitle(R.string.reader_title_related_post_detail) } else { toolBar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp) - toolBar.setNavigationOnClickListener { requireActivity().onBackPressed() } + toolBar.setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } } } @@ -499,7 +503,7 @@ class ReaderPostDetailFragment : ViewPagerFragment(), private fun initLikeFacesRecycler(savedInstanceState: Bundle?) { if (!likesEnhancementsFeatureConfig.isEnabled()) return val layoutManager = LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false) - savedInstanceState?.getParcelable(KEY_LIKERS_LIST_STATE)?.let { + savedInstanceState?.getParcelableCompat(KEY_LIKERS_LIST_STATE)?.let { layoutManager.onRestoreInstanceState(it) } @@ -518,7 +522,7 @@ class ReaderPostDetailFragment : ViewPagerFragment(), if (!commentsSnippetFeatureConfig.isEnabled()) return val layoutManager = LinearLayoutManager(activity) - savedInstanceState?.getParcelable(KEY_COMMENTS_SNIPPET_LIST_STATE)?.let { + savedInstanceState?.getParcelableCompat(KEY_COMMENTS_SNIPPET_LIST_STATE)?.let { layoutManager.onRestoreInstanceState(it) } @@ -1082,8 +1086,7 @@ class ReaderPostDetailFragment : ViewPagerFragment(), savedInstanceState?.let { blogId = it.getLong(ReaderConstants.ARG_BLOG_ID) postId = it.getLong(ReaderConstants.ARG_POST_ID) - directOperation = it - .getSerializable(ReaderConstants.ARG_DIRECT_OPERATION) as? DirectOperation + directOperation = it.getSerializableCompat(ReaderConstants.ARG_DIRECT_OPERATION) commentId = it.getInt(ReaderConstants.ARG_COMMENT_ID) isRelatedPost = it.getBoolean(ReaderConstants.ARG_IS_RELATED_POST) interceptedUri = it.getString(ReaderConstants.ARG_INTERCEPTED_URI) @@ -1092,7 +1095,7 @@ class ReaderPostDetailFragment : ViewPagerFragment(), hasTrackedGlobalRelatedPosts = it.getBoolean(ReaderConstants.KEY_ALREADY_TRACKED_GLOBAL_RELATED_POSTS) hasTrackedLocalRelatedPosts = it.getBoolean(ReaderConstants.KEY_ALREADY_TRACKED_LOCAL_RELATED_POSTS) if (it.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) { - this.postListType = it.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE) as ReaderPostListType + postListType = requireNotNull(it.getSerializableCompat(ReaderConstants.ARG_POST_LIST_TYPE)) } if (it.containsKey(ReaderConstants.KEY_ERROR_MESSAGE)) { errorMessage = it.getString(ReaderConstants.KEY_ERROR_MESSAGE) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java index f84c7c373756..8f6df01769f0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java @@ -8,6 +8,7 @@ import android.view.MenuItem; import android.view.View; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; @@ -41,12 +42,16 @@ import org.wordpress.android.ui.uploads.UploadUtils; import org.wordpress.android.ui.uploads.UploadUtilsWrapper; import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import javax.inject.Inject; +import dagger.hilt.android.AndroidEntryPoint; + /* * serves as the host for ReaderPostListFragment when showing blog preview & tag preview */ +@AndroidEntryPoint public class ReaderPostListActivity extends LocaleAwareActivity { private String mSource; private ReaderPostListType mPostListType; @@ -63,10 +68,20 @@ public class ReaderPostListActivity extends LocaleAwareActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ((WordPress) getApplication()).component().inject(this); setContentView(R.layout.reader_activity_post_list); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + ReaderPostListFragment fragment = getListFragment(); + if (fragment == null || !fragment.onActivityBackPressed()) { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + Toolbar toolbar = findViewById(R.id.toolbar_main); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); @@ -188,14 +203,6 @@ public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); } - @Override - public void onBackPressed() { - ReaderPostListFragment fragment = getListFragment(); - if (fragment == null || !fragment.onActivityBackPressed()) { - super.onBackPressed(); - } - } - @Override public boolean onCreateOptionsMenu(Menu menu) { if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) { @@ -209,7 +216,7 @@ public boolean onCreateOptionsMenu(Menu menu) { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; case R.id.menu_share: shareSite(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java index 4b0d69038fc5..af0330ca09ac 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostPagerActivity.java @@ -13,6 +13,7 @@ import android.view.ViewGroup; import android.widget.ProgressBar; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -49,6 +50,7 @@ import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureOverlayActions.ForwardToJetpack; import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil.JetpackFeatureCollectionOverlaySource; import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper; +import org.wordpress.android.ui.main.WPMainActivity; import org.wordpress.android.ui.mysite.SelectedSiteRepository; import org.wordpress.android.ui.posts.EditPostActivity; import org.wordpress.android.ui.prefs.AppPrefs; @@ -78,6 +80,7 @@ import org.wordpress.android.util.WPActivityUtils; import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper; import org.wordpress.android.util.config.SeenUnseenWithCounterFeatureConfig; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import org.wordpress.android.widgets.WPSwipeSnackbar; import org.wordpress.android.widgets.WPViewPager; import org.wordpress.android.widgets.WPViewPagerTransformer; @@ -94,6 +97,9 @@ import dagger.hilt.android.AndroidEntryPoint; +import static org.wordpress.android.ui.main.WPMainActivity.ARG_OPEN_PAGE; +import static org.wordpress.android.ui.main.WPMainActivity.ARG_READER; + /* * shows reader post detail fragments in a ViewPager - primarily used for easy swiping between * posts with a specific tag or in a specific blog, but can also be used to show a single @@ -177,6 +183,24 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.reader_activity_post_pager); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + ReaderPostDetailFragment fragment = getActiveDetailFragment(); + if (fragment != null && fragment.isCustomViewShowing()) { + // if full screen video is showing, hide the custom view rather than navigate back + fragment.hideCustomView(); + } else { + if (fragment != null && fragment.goBackInPostHistory()) { + // noop - fragment moved back to a previous post + } else { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + // Start migration flow passing deep link data if requirements are met if (mJetpackAppMigrationFlowUtils.shouldShowMigrationFlow()) { PreMigrationDeepLinkData deepLinkData = new PreMigrationDeepLinkData( @@ -299,12 +323,22 @@ private void handleDeepLinking() { host = uri.getHost(); } - if (uri == null || mJetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()) { + if (uri == null + || mJetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures() + || mJetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { mReaderTracker.trackDeepLink(AnalyticsTracker.Stat.DEEP_LINKED, action, host, uri); // invalid uri so, just show the entry screen - Intent intent = new Intent(this, WPLaunchActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); + if (mJetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { + Intent intent = new Intent(this, WPMainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(ARG_OPEN_PAGE, ARG_READER); + startActivity(intent); + } else { + Intent intent = new Intent(this, WPLaunchActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(ARG_OPEN_PAGE, ARG_READER); + startActivity(intent); + } finish(); return; } @@ -693,21 +727,6 @@ private ReaderBlogIdPostId getAdapterBlogIdPostIdAtPosition(int position) { return adapter.getBlogIdPostIdAtPosition(position); } - @Override - public void onBackPressed() { - ReaderPostDetailFragment fragment = getActiveDetailFragment(); - if (fragment != null && fragment.isCustomViewShowing()) { - // if full screen video is showing, hide the custom view rather than navigate back - fragment.hideCustomView(); - } else { - if (fragment != null && fragment.goBackInPostHistory()) { - // noop - fragment moved back to a previous post - } else { - super.onBackPressed(); - } - } - } - /* * perform analytics tracking and bump the page view for the post at the passed position * if it hasn't already been done diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostWebViewCachingFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostWebViewCachingFragment.java index 89c214c6c53c..c9f0b2c40775 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostWebViewCachingFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostWebViewCachingFragment.java @@ -8,6 +8,7 @@ import android.webkit.WebView; import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import org.wordpress.android.datasets.ReaderPostTable; @@ -20,13 +21,14 @@ import javax.inject.Inject; -import dagger.android.support.DaggerFragment; +import dagger.hilt.android.AndroidEntryPoint; /** * Fragment responsible for caching post content into WebView. * Caching happens on UI thread, so any configuration change will restart it from scratch. */ -public class ReaderPostWebViewCachingFragment extends DaggerFragment { +@AndroidEntryPoint +public class ReaderPostWebViewCachingFragment extends Fragment { private static final String ARG_BLOG_ID = "blog_id"; private static final String ARG_POST_ID = "post_id"; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java index f07cb273da73..b3fa4d9de841 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderSubsActivity.java @@ -10,6 +10,7 @@ import android.widget.EditText; import android.widget.ProgressBar; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -54,6 +55,7 @@ import org.wordpress.android.util.EditTextUtils; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.UrlUtils; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import org.wordpress.android.widgets.WPSnackbar; import org.wordpress.android.widgets.WPViewPager; @@ -93,6 +95,18 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((WordPress) getApplication()).component().inject(this); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (!TextUtils.isEmpty(mLastAddedTagName)) { + EventBus.getDefault().postSticky(new ReaderEvents.TagAdded(mLastAddedTagName)); + } + mReaderTracker.track(Stat.READER_MANAGE_VIEW_DISMISSED); + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + setContentView(R.layout.reader_activity_subs); restoreState(savedInstanceState); @@ -107,7 +121,7 @@ protected void onCreate(Bundle savedInstanceState) { Toolbar toolbar = findViewById(R.id.toolbar_main); if (toolbar != null) { setSupportActionBar(toolbar); - toolbar.setNavigationOnClickListener(v -> onBackPressed()); + toolbar.setNavigationOnClickListener(v -> getOnBackPressedDispatcher().onBackPressed()); } ActionBar actionBar = getSupportActionBar(); @@ -236,15 +250,6 @@ public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); } - @Override - public void onBackPressed() { - if (!TextUtils.isEmpty(mLastAddedTagName)) { - EventBus.getDefault().postSticky(new ReaderEvents.TagAdded(mLastAddedTagName)); - } - mReaderTracker.track(Stat.READER_MANAGE_VIEW_DISMISSED); - super.onBackPressed(); - } - /* * follow the tag or url the user typed into the EditText */ diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderUserListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderUserListActivity.java index e7f47bf31dca..eaf13a0a0171 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderUserListActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderUserListActivity.java @@ -1,7 +1,6 @@ package org.wordpress.android.ui.reader; import android.os.Bundle; -import android.view.View; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; @@ -41,12 +40,7 @@ protected void onCreate(Bundle savedInstanceState) { Toolbar toolbar = findViewById(R.id.toolbar_main); if (toolbar != null) { setSupportActionBar(toolbar); - toolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onBackPressed(); - } - }); + toolbar.setNavigationOnClickListener(v -> getOnBackPressedDispatcher().onBackPressed()); } ActionBar actionBar = getSupportActionBar(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt index d183bf67fa3f..438701ca1492 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/SubfilterBottomSheetFragment.kt @@ -24,6 +24,7 @@ import org.wordpress.android.ui.reader.subfilter.SubfilterCategory.SITES import org.wordpress.android.ui.reader.subfilter.SubfilterCategory.TAGS import org.wordpress.android.ui.reader.subfilter.SubfilterListItem.Tag import org.wordpress.android.ui.reader.subfilter.SubfilterPagerAdapter +import org.wordpress.android.util.extensions.getParcelableArrayListCompat import javax.inject.Inject class SubfilterBottomSheetFragment : BottomSheetDialogFragment() { @@ -66,11 +67,14 @@ class SubfilterBottomSheetFragment : BottomSheetDialogFragment() { val subfilterVmKey = requireArguments().getString(SUBFILTER_VIEW_MODEL_KEY)!! val bottomSheetTitle = requireArguments().getCharSequence(SUBFILTER_TITLE_KEY)!! - val categories: ArrayList = requireArguments() - .getParcelableArrayList(SUBFILTER_CATEGORIES_KEY)!! + val categories = requireNotNull( + requireArguments().getParcelableArrayListCompat(SUBFILTER_CATEGORIES_KEY) + ) - viewModel = ViewModelProvider(parentFragment as ViewModelStoreOwner, viewModelFactory) - .get(subfilterVmKey, SubFilterViewModel::class.java) + viewModel = ViewModelProvider( + parentFragment as ViewModelStoreOwner, + viewModelFactory + )[subfilterVmKey, SubFilterViewModel::class.java] val pager = view.findViewById(R.id.view_pager) val tabLayout = view.findViewById(R.id.tab_layout) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsActivity.kt index 5aad63751ee0..3f082043b856 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsActivity.kt @@ -23,7 +23,7 @@ class ReaderInterestsActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsFragment.kt index 9679ce2f978d..29415dce9f88 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/interests/ReaderInterestsFragment.kt @@ -17,6 +17,7 @@ import org.wordpress.android.ui.reader.discover.interests.ReaderInterestsViewMod import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.LocaleManager +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.viewmodel.observeEvent import org.wordpress.android.widgets.WPSnackbar import javax.inject.Inject @@ -37,7 +38,7 @@ class ReaderInterestsFragment : Fragment(R.layout.reader_interests_fragment_layo override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val entryPoint = requireActivity().intent.getSerializableExtra(READER_INTEREST_ENTRY_POINT) as? EntryPoint + val entryPoint = requireActivity().intent.getSerializableExtraCompat(READER_INTEREST_ENTRY_POINT) ?: EntryPoint.DISCOVER with(ReaderInterestsFragmentLayoutBinding.bind(view)) { initDoneButton() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderInterestsCardViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderInterestsCardViewHolder.kt index 8507fbf875b1..0ad4b9ad5c32 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderInterestsCardViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/discover/viewholders/ReaderInterestsCardViewHolder.kt @@ -58,14 +58,14 @@ class ReaderInterestsCardViewHolder( * We need to do this immediately, because if we don't, then the next move event could potentially * trigger the viewPager to switch tabs */ - override fun onDown(e: MotionEvent?): Boolean = with(binding) { + override fun onDown(e: MotionEvent): Boolean = with(binding) { interestsList.parent.requestDisallowInterceptTouchEvent(true) return super.onDown(e) } override fun onScroll( - e1: MotionEvent?, - e2: MotionEvent?, + e1: MotionEvent, + e2: MotionEvent, distanceX: Float, distanceY: Float ): Boolean = with(binding) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverJobService.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverJobService.kt index 64bc0036f843..418b14ae67f5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverJobService.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverJobService.kt @@ -7,6 +7,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import org.wordpress.android.modules.IO_THREAD import org.wordpress.android.ui.reader.services.ServiceCompletionListener import org.wordpress.android.ui.reader.services.discover.ReaderDiscoverLogic.DiscoverTasks import org.wordpress.android.util.AppLog @@ -19,7 +20,7 @@ import kotlin.coroutines.CoroutineContext @AndroidEntryPoint class ReaderDiscoverJobService : JobService(), ServiceCompletionListener, CoroutineScope { @Inject - @field:Named("IO_THREAD") + @Named(IO_THREAD) lateinit var ioDispatcher: CoroutineDispatcher @Inject @@ -37,7 +38,7 @@ class ReaderDiscoverJobService : JobService(), ServiceCompletionListener, Corout override fun onStartJob(params: JobParameters): Boolean { AppLog.i(READER, "reader discover job service > started") - val task = DiscoverTasks.values()[(params.extras[ReaderDiscoverServiceStarter.ARG_DISCOVER_TASK] as Int)] + val task = DiscoverTasks.values()[params.extras.getInt(ReaderDiscoverServiceStarter.ARG_DISCOVER_TASK)] readerDiscoverLogic.performTasks(task, params, this, this) return true diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverService.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverService.kt index 05853601af7d..d34ca7f8da7e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverService.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/discover/ReaderDiscoverService.kt @@ -8,12 +8,14 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import org.wordpress.android.modules.IO_THREAD import org.wordpress.android.ui.reader.services.ServiceCompletionListener import org.wordpress.android.ui.reader.services.discover.ReaderDiscoverLogic.DiscoverTasks import org.wordpress.android.ui.reader.services.discover.ReaderDiscoverServiceStarter.ARG_DISCOVER_TASK import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T.READER import org.wordpress.android.util.LocaleManager +import org.wordpress.android.util.extensions.getSerializableExtraCompat import javax.inject.Inject import javax.inject.Named import kotlin.coroutines.CoroutineContext @@ -24,7 +26,7 @@ import kotlin.coroutines.CoroutineContext @AndroidEntryPoint class ReaderDiscoverService : Service(), ServiceCompletionListener, CoroutineScope { @Inject - @field:Named("IO_THREAD") + @Named(IO_THREAD) lateinit var ioDispatcher: CoroutineDispatcher @Inject @@ -56,7 +58,7 @@ class ReaderDiscoverService : Service(), ServiceCompletionListener, CoroutineSco override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent != null && intent.hasExtra(ARG_DISCOVER_TASK)) { - val task = intent.getSerializableExtra(ARG_DISCOVER_TASK) as DiscoverTasks + val task = requireNotNull(intent.getSerializableExtraCompat(ARG_DISCOVER_TASK)) readerDiscoverLogic.performTasks(task, null, this, this) } return START_NOT_STICKY diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogic.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogic.java index c71f056cc779..ca1619ae3788 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogic.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/services/post/ReaderPostLogic.java @@ -2,6 +2,8 @@ import android.text.TextUtils; +import androidx.annotation.NonNull; + import com.android.volley.VolleyError; import com.wordpress.rest.RestRequest; @@ -309,8 +311,16 @@ private static String getRelativeEndpointForTag(ReaderTag tag) { if (tag.tagType == ReaderTagType.DEFAULT) { return null; } + return formatRelativeEndpointForTag(tag.getTagSlug()); + } + + private static String formatRelativeEndpointForTag(@NonNull final String tagSlug) { + return String.format("read/tags/%s/posts", ReaderUtils.sanitizeWithDashes(tagSlug)); + } - return String.format("read/tags/%s/posts", ReaderUtils.sanitizeWithDashes(tag.getTagSlug())); + public static String formatFullEndpointForTag(@NonNull final String tagSlug) { + return WordPress.getRestClientUtilsV1_2().getRestClient().getEndpointURL() + + formatRelativeEndpointForTag(tagSlug); } /* diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt index 5059cf202a3f..d95e8c22407e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/subfilter/SubfilterPageFragment.kt @@ -16,12 +16,11 @@ import androidx.annotation.StringRes import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentPagerAdapter -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import dagger.android.support.DaggerFragment +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.ui.reader.subfilter.SubfilterBottomSheetEmptyUiState.HiddenEmptyUiState import org.wordpress.android.ui.reader.subfilter.SubfilterBottomSheetEmptyUiState.VisibleEmptyUiState @@ -35,11 +34,13 @@ import org.wordpress.android.ui.reader.viewmodels.SubfilterPageViewModel import org.wordpress.android.ui.stats.refresh.utils.StatsUtils import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.config.SeenUnseenWithCounterFeatureConfig +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.widgets.WPTextView import java.lang.ref.WeakReference import javax.inject.Inject -class SubfilterPageFragment : DaggerFragment() { +@AndroidEntryPoint +class SubfilterPageFragment : Fragment() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -80,10 +81,10 @@ class SubfilterPageFragment : DaggerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val category = requireArguments().getSerializable(CATEGORY_KEY) as SubfilterCategory + val category = requireNotNull(arguments?.getSerializableCompat(CATEGORY_KEY)) val subfilterVmKey = requireArguments().getString(SUBFILTER_VIEW_MODEL_KEY)!! - viewModel = ViewModelProvider(this, viewModelFactory).get(SubfilterPageViewModel::class.java) + viewModel = ViewModelProvider(this, viewModelFactory)[SubfilterPageViewModel::class.java] viewModel.start(category) recyclerView = view.findViewById(R.id.content_recycler_view) @@ -99,7 +100,7 @@ class SubfilterPageFragment : DaggerFragment() { viewModelFactory ).get(subfilterVmKey, SubFilterViewModel::class.java) - subFilterViewModel.subFilters.observe(viewLifecycleOwner, Observer { + subFilterViewModel.subFilters.observe(viewLifecycleOwner) { (recyclerView.adapter as? SubfilterListAdapter)?.let { adapter -> var items = it?.filter { it.type == category.type } ?: listOf() @@ -116,9 +117,9 @@ class SubfilterPageFragment : DaggerFragment() { adapter.update(items) subFilterViewModel.onSubfilterPageUpdated(category, items.size) } - }) + } - viewModel.emptyState.observe(viewLifecycleOwner, Observer { uiState -> + viewModel.emptyState.observe(viewLifecycleOwner) { uiState -> if (isAdded) { when (uiState) { HiddenEmptyUiState -> emptyStateContainer.visibility = View.GONE @@ -132,7 +133,7 @@ class SubfilterPageFragment : DaggerFragment() { } } } - }) + } } fun setNestedScrollBehavior(enable: Boolean) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationActivity.kt index 5cde5e92fb98..59a4c1f3d0a6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationActivity.kt @@ -4,9 +4,10 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import android.view.MenuItem +import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer +import androidx.fragment.app.commit import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.cancel import org.wordpress.android.R @@ -15,6 +16,7 @@ import org.wordpress.android.ui.ActivityLauncherWrapper import org.wordpress.android.ui.ActivityLauncherWrapper.Companion.JETPACK_PACKAGE_NAME import org.wordpress.android.ui.LocaleAwareActivity import org.wordpress.android.ui.accounts.HelpActivity.Origin +import org.wordpress.android.ui.domains.DomainRegistrationCheckoutWebViewActivity.OpenCheckout import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayFragment import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureFullScreenOverlayViewModel import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureOverlayActions.DismissDialog @@ -26,21 +28,24 @@ import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogPositiveCli import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleEmpty import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleGeneral import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleStepCount +import org.wordpress.android.ui.sitecreation.SiteCreationResult.Completed +import org.wordpress.android.ui.sitecreation.SiteCreationResult.Created +import org.wordpress.android.ui.sitecreation.SiteCreationResult.CreatedButNotFetched import org.wordpress.android.ui.sitecreation.SiteCreationStep.DOMAINS import org.wordpress.android.ui.sitecreation.SiteCreationStep.INTENTS +import org.wordpress.android.ui.sitecreation.SiteCreationStep.PROGRESS import org.wordpress.android.ui.sitecreation.SiteCreationStep.SITE_DESIGNS import org.wordpress.android.ui.sitecreation.SiteCreationStep.SITE_NAME import org.wordpress.android.ui.sitecreation.SiteCreationStep.SITE_PREVIEW +import org.wordpress.android.ui.sitecreation.domains.DomainModel import org.wordpress.android.ui.sitecreation.domains.DomainsScreenListener import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsFragment import org.wordpress.android.ui.sitecreation.misc.OnHelpClickedListener import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource import org.wordpress.android.ui.sitecreation.previews.SiteCreationPreviewFragment -import org.wordpress.android.ui.sitecreation.previews.SitePreviewScreenListener -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState.SiteCreationCompleted -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState.SiteNotCreated -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState.SiteNotInLocalDb +import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressFragment +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressViewModel import org.wordpress.android.ui.sitecreation.sitename.SiteCreationSiteNameFragment import org.wordpress.android.ui.sitecreation.sitename.SiteCreationSiteNameViewModel import org.wordpress.android.ui.sitecreation.sitename.SiteNameScreenListener @@ -53,6 +58,7 @@ import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.ActivityUtils import org.wordpress.android.util.config.SiteNameFeatureConfig import org.wordpress.android.util.extensions.exhaustive +import org.wordpress.android.util.extensions.onBackPressedCompat import org.wordpress.android.util.wizard.WizardNavigationTarget import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -62,7 +68,6 @@ class SiteCreationActivity : LocaleAwareActivity(), IntentsScreenListener, SiteNameScreenListener, DomainsScreenListener, - SitePreviewScreenListener, OnHelpClickedListener, BasicDialogPositiveClickInterface, BasicDialogNegativeClickInterface { @@ -76,12 +81,31 @@ class SiteCreationActivity : LocaleAwareActivity(), private val siteCreationIntentsViewModel: SiteCreationIntentsViewModel by viewModels() private val siteCreationSiteNameViewModel: SiteCreationSiteNameViewModel by viewModels() private val jetpackFullScreenViewModel: JetpackFeatureFullScreenOverlayViewModel by viewModels() - @Inject internal lateinit var jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil - @Inject internal lateinit var activityLauncherWrapper: ActivityLauncherWrapper + private val progressViewModel: SiteCreationProgressViewModel by viewModels() + private val previewViewModel: SitePreviewViewModel by viewModels() + + @Inject + internal lateinit var jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil + + @Inject + internal lateinit var activityLauncherWrapper: ActivityLauncherWrapper + + private val backPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + mainViewModel.onBackPressed() + } + } + + private val domainCheckoutActivityLauncher = registerForActivityResult(OpenCheckout()) { + mainViewModel.onCheckoutResult(it) + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.site_creation_activity) + + onBackPressedDispatcher.addCallback(this, backPressedCallback) + mainViewModel.start(savedInstanceState, getSiteCreationSource()) mainViewModel.preloadThumbnails(this) @@ -95,77 +119,66 @@ class SiteCreationActivity : LocaleAwareActivity(), @Suppress("LongMethod") private fun observeVMState() { - mainViewModel.navigationTargetObservable - .observe(this, Observer { target -> target?.let { showStep(target) } }) - mainViewModel.wizardFinishedObservable.observe(this, Observer { createSiteState -> - createSiteState?.let { - val intent = Intent() - val (siteCreated, localSiteId, titleTaskComplete) = when (createSiteState) { - // site creation flow was canceled - is SiteNotCreated -> Triple(false, null, false) - is SiteNotInLocalDb -> { - // Site was created, but we haven't been able to fetch it, let `SitePickerActivity` handle - // this with a Snackbar message. - intent.putExtra(SitePickerActivity.KEY_SITE_CREATED_BUT_NOT_FETCHED, true) - Triple(true, null, createSiteState.isSiteTitleTaskComplete) - } - is SiteCreationCompleted -> Triple( - true, createSiteState.localSiteId, - createSiteState.isSiteTitleTaskComplete - ) - } - intent.putExtra(SitePickerActivity.KEY_SITE_LOCAL_ID, localSiteId) - intent.putExtra(SitePickerActivity.KEY_SITE_TITLE_TASK_COMPLETED, titleTaskComplete) - setResult(if (siteCreated) Activity.RESULT_OK else Activity.RESULT_CANCELED, intent) - finish() - } - }) - mainViewModel.dialogActionObservable.observe(this, Observer { dialogHolder -> - dialogHolder?.let { - val supportFragmentManager = requireNotNull(supportFragmentManager) { - "FragmentManager can't be null at this point" - } - dialogHolder.show(this, supportFragmentManager, uiHelpers) + mainViewModel.navigationTargetObservable.observe(this) { it?.let(::showStep) } + mainViewModel.onCompleted.observe(this) { (result, isTitleTaskComplete) -> + val intent = Intent().apply { + putExtra(SitePickerActivity.KEY_SITE_LOCAL_ID, (result as? Created)?.site?.id) + putExtra(SitePickerActivity.KEY_SITE_TITLE_TASK_COMPLETED, isTitleTaskComplete) + // Let `SitePickerActivity` handle this with a SnackBar message + putExtra(SitePickerActivity.KEY_SITE_CREATED_BUT_NOT_FETCHED, result is CreatedButNotFetched) } - }) - mainViewModel.exitFlowObservable.observe(this, Observer { + setResult(if (result is Completed) Activity.RESULT_OK else Activity.RESULT_CANCELED, intent) + finish() + } + mainViewModel.dialogActionObservable.observe(this) { + it?.show(this, supportFragmentManager, uiHelpers) + } + mainViewModel.exitFlowObservable.observe(this) { setResult(Activity.RESULT_CANCELED) finish() - }) - mainViewModel.onBackPressedObservable.observe(this, Observer { + } + mainViewModel.onBackPressedObservable.observe(this) { ActivityUtils.hideKeyboard(this) - super.onBackPressed() - }) - siteCreationIntentsViewModel.onBackButtonPressed.observe(this, Observer { + onBackPressedDispatcher.onBackPressedCompat(backPressedCallback) + } + mainViewModel.showDomainCheckout.observe(this, domainCheckoutActivityLauncher::launch) + siteCreationIntentsViewModel.onBackButtonPressed.observe(this) { mainViewModel.onBackPressed() - }) - siteCreationIntentsViewModel.onSkipButtonPressed.observe(this, Observer { + } + siteCreationIntentsViewModel.onSkipButtonPressed.observe(this) { mainViewModel.onSiteIntentSkipped() - }) - siteCreationSiteNameViewModel.onBackButtonPressed.observe(this, Observer { + } + siteCreationSiteNameViewModel.onBackButtonPressed.observe(this) { mainViewModel.onBackPressed() ActivityUtils.hideKeyboard(this) - }) - siteCreationSiteNameViewModel.onSkipButtonPressed.observe(this, Observer { + } + siteCreationSiteNameViewModel.onSkipButtonPressed.observe(this) { ActivityUtils.hideKeyboard(this) mainViewModel.onSiteNameSkipped() - }) - hppViewModel.onBackButtonPressed.observe(this, Observer { + } + hppViewModel.onBackButtonPressed.observe(this) { mainViewModel.onBackPressed() - }) - hppViewModel.onDesignActionPressed.observe(this, Observer { design -> + } + hppViewModel.onDesignActionPressed.observe(this) { design -> mainViewModel.onSiteDesignSelected(design.template) - }) - + } + progressViewModel.onCancelWizardClicked.observe(this) { + mainViewModel.onWizardCancelled() + } + progressViewModel.onFreeSiteCreated.observe(this, mainViewModel::onFreeSiteCreated) + progressViewModel.onCartCreated.observe(this, mainViewModel::onCartCreated) + previewViewModel.onOkButtonClicked.observe(this) { result -> + mainViewModel.onWizardFinished(result) + } observeOverlayEvents() } private fun observeOverlayEvents() { val fragment = JetpackFeatureFullScreenOverlayFragment - .newInstance( - isSiteCreationOverlay = true, - siteCreationSource = getSiteCreationSource() - ) + .newInstance( + isSiteCreationOverlay = true, + siteCreationSource = getSiteCreationSource() + ) jetpackFullScreenViewModel.action.observe(this) { action -> if (mainViewModel.siteCreationDisabled) finish() @@ -183,7 +196,7 @@ class SiteCreationActivity : LocaleAwareActivity(), mainViewModel.showJetpackOverlay.observeEvent(this) { if (mainViewModel.siteCreationDisabled) - slideInFragment(fragment, JetpackFeatureFullScreenOverlayFragment.TAG) + showFragment(fragment, JetpackFeatureFullScreenOverlayFragment.TAG) else fragment.show(supportFragmentManager, JetpackFeatureFullScreenOverlayFragment.TAG) } } @@ -205,18 +218,10 @@ class SiteCreationActivity : LocaleAwareActivity(), ActivityUtils.hideKeyboard(this) } - override fun onDomainSelected(domain: String) { + override fun onDomainSelected(domain: DomainModel) { mainViewModel.onDomainsScreenFinished(domain) } - override fun onSiteCreationCompleted() { - mainViewModel.onSiteCreationCompleted() - } - - override fun onSitePreviewScreenDismissed(createSiteState: CreateSiteState) { - mainViewModel.onSitePreviewScreenFinished(createSiteState) - } - override fun onHelpClicked(origin: Origin) { ActivityLauncher.viewHelp(this, origin, null, null) } @@ -231,12 +236,11 @@ class SiteCreationActivity : LocaleAwareActivity(), mainViewModel.preloadingJob?.cancel("Preload did not complete before theme picker was shown.") HomePagePickerFragment.newInstance(target.wizardState.siteIntent) } - DOMAINS -> SiteCreationDomainsFragment.newInstance( - screenTitle - ) + DOMAINS -> SiteCreationDomainsFragment.newInstance(screenTitle) + PROGRESS -> SiteCreationProgressFragment.newInstance(target.wizardState) SITE_PREVIEW -> SiteCreationPreviewFragment.newInstance(screenTitle, target.wizardState) } - slideInFragment(fragment, target.wizardStep.toString()) + showFragment(fragment, target.wizardStep.toString(), target.wizardStep != SITE_PREVIEW) } private fun getScreenTitle(step: SiteCreationStep): String { @@ -251,17 +255,22 @@ class SiteCreationActivity : LocaleAwareActivity(), } } - private fun slideInFragment(fragment: Fragment, tag: String) { - val fragmentTransaction = supportFragmentManager.beginTransaction() - if (supportFragmentManager.findFragmentById(R.id.fragment_container) != null) { - // add to back stack and animate all screen except of the first one - fragmentTransaction.addToBackStack(null).setCustomAnimations( - R.anim.activity_slide_in_from_right, R.anim.activity_slide_out_to_left, - R.anim.activity_slide_in_from_left, R.anim.activity_slide_out_to_right - ) + private fun showFragment(fragment: Fragment, tag: String, slideIn: Boolean = true) { + supportFragmentManager.commit { + if (supportFragmentManager.findFragmentById(R.id.fragment_container) != null) { + // add to back stack and animate all screen except of the first one + addToBackStack(null) + if (slideIn) { + setCustomAnimations( + R.anim.activity_slide_in_from_right, R.anim.activity_slide_out_to_left, + R.anim.activity_slide_in_from_left, R.anim.activity_slide_out_to_right + ) + } else { + setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) + } + } + replace(R.id.fragment_container, fragment, tag) } - fragmentTransaction.replace(R.id.fragment_container, fragment, tag) - fragmentTransaction.commit() } override fun onPositiveClicked(instanceTag: String) { @@ -274,17 +283,14 @@ class SiteCreationActivity : LocaleAwareActivity(), override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return false } - override fun onBackPressed() { - mainViewModel.onBackPressed() - } - companion object { const val ARG_CREATE_SITE_SOURCE = "ARG_CREATE_SITE_SOURCE" + const val ARG_STATE = "ARG_SITE_CREATION_STATE" } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVM.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVM.kt index 26a0d4ea9fa5..29b46106c5b8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVM.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVM.kt @@ -1,6 +1,5 @@ package org.wordpress.android.ui.sitecreation -import android.annotation.SuppressLint import android.content.Context import android.os.Bundle import android.os.Parcelable @@ -17,14 +16,22 @@ import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.wordpress.android.R import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.networking.MShot +import org.wordpress.android.ui.domains.DomainRegistrationCheckoutWebViewActivity.OpenCheckout.CheckoutDetails +import org.wordpress.android.ui.domains.DomainRegistrationCompletedEvent +import org.wordpress.android.ui.domains.DomainsRegistrationTracker import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleEmpty import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleGeneral import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleStepCount +import org.wordpress.android.ui.sitecreation.SiteCreationResult.Completed +import org.wordpress.android.ui.sitecreation.SiteCreationResult.Created +import org.wordpress.android.ui.sitecreation.SiteCreationResult.CreatedButNotFetched +import org.wordpress.android.ui.sitecreation.SiteCreationResult.NotCreated +import org.wordpress.android.ui.sitecreation.domains.DomainModel import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState import org.wordpress.android.ui.sitecreation.usecases.FetchHomePageLayoutsUseCase import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.util.AppLog @@ -32,6 +39,7 @@ import org.wordpress.android.util.AppLog.T import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.config.SiteCreationDomainPurchasingFeatureConfig import org.wordpress.android.util.experiments.SiteCreationDomainPurchasingExperiment +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.wizard.WizardManager import org.wordpress.android.util.wizard.WizardNavigationTarget @@ -44,21 +52,55 @@ import javax.inject.Inject const val TAG_WARNING_DIALOG = "back_pressed_warning_dialog" const val KEY_CURRENT_STEP = "key_current_step" -const val KEY_SITE_CREATION_COMPLETED = "key_site_creation_completed" const val KEY_SITE_CREATION_STATE = "key_site_creation_state" @Parcelize -@SuppressLint("ParcelCreator") data class SiteCreationState( val siteIntent: String? = null, val siteName: String? = null, val segmentId: Long? = null, val siteDesign: String? = null, - val domain: String? = null + val domain: DomainModel? = null, + val result: SiteCreationResult = NotCreated, ) : WizardState, Parcelable typealias NavigationTarget = WizardNavigationTarget +sealed interface SiteCreationResult : Parcelable { + @Parcelize + object NotCreated : SiteCreationResult + + sealed interface Created: SiteCreationResult { + val site: SiteModel + } + + sealed interface CreatedButNotFetched : Created { + @Parcelize + data class NotInLocalDb( + override val site: SiteModel, + ) : CreatedButNotFetched + + @Parcelize + data class InCart( + override val site: SiteModel, + ) : CreatedButNotFetched + + @Parcelize + data class DomainRegistrationPurchased( + val domainName: String, + val email: String, + override val site: SiteModel, + ) : CreatedButNotFetched + } + + @Parcelize + data class Completed( + override val site: SiteModel, + ) : Created +} + +typealias SiteCreationCompletionEvent = Pair + @HiltViewModel class SiteCreationMainVM @Inject constructor( private val tracker: SiteCreationTracker, @@ -70,6 +112,7 @@ class SiteCreationMainVM @Inject constructor( private val jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil, private val domainPurchasingExperiment: SiteCreationDomainPurchasingExperiment, private val domainPurchasingFeatureConfig: SiteCreationDomainPurchasingFeatureConfig, + private val domainsRegistrationTracker: DomainsRegistrationTracker, ) : ViewModel() { init { dispatcher.register(fetchHomePageLayoutsUseCase) @@ -82,7 +125,6 @@ class SiteCreationMainVM @Inject constructor( var siteCreationDisabled: Boolean = false private var isStarted = false - private var siteCreationCompleted = false private lateinit var siteCreationState: SiteCreationState @@ -100,8 +142,8 @@ class SiteCreationMainVM @Inject constructor( private val _dialogAction = SingleLiveEvent() val dialogActionObservable: LiveData = _dialogAction - private val _wizardFinishedObservable = SingleLiveEvent() - val wizardFinishedObservable: LiveData = _wizardFinishedObservable + private val _onCompleted = SingleLiveEvent() + val onCompleted: LiveData = _onCompleted private val _exitFlowObservable = SingleLiveEvent() val exitFlowObservable: LiveData = _exitFlowObservable @@ -112,6 +154,9 @@ class SiteCreationMainVM @Inject constructor( private val _showJetpackOverlay = MutableLiveData>() val showJetpackOverlay: LiveData> = _showJetpackOverlay + private val _showDomainCheckout = SingleLiveEvent() + val showDomainCheckout: LiveData = _showDomainCheckout + fun start(savedInstanceState: Bundle?, siteCreationSource: SiteCreationSource) { if (isStarted) return if (savedInstanceState == null) { @@ -127,8 +172,7 @@ class SiteCreationMainVM @Inject constructor( else showSiteCreationNextStep() } else { - siteCreationCompleted = savedInstanceState.getBoolean(KEY_SITE_CREATION_COMPLETED, false) - siteCreationState = requireNotNull(savedInstanceState.getParcelable(KEY_SITE_CREATION_STATE)) + siteCreationState = requireNotNull(savedInstanceState.getParcelableCompat(KEY_SITE_CREATION_STATE)) val currentStepIndex = savedInstanceState.getInt(KEY_CURRENT_STEP) wizardManager.setCurrentStepIndex(currentStepIndex) } @@ -169,7 +213,6 @@ class SiteCreationMainVM @Inject constructor( } fun writeToBundle(outState: Bundle) { - outState.putBoolean(KEY_SITE_CREATION_COMPLETED, siteCreationCompleted) outState.putInt(KEY_CURRENT_STEP, wizardManager.currentStep) outState.putParcelable(KEY_SITE_CREATION_STATE, siteCreationState) } @@ -201,7 +244,7 @@ class SiteCreationMainVM @Inject constructor( fun onBackPressed() { return if (wizardManager.isLastStep()) { - if (siteCreationCompleted) { + if (siteCreationState.result is Completed) { exitFlow(false) } else { _dialogAction.value = DialogHolder( @@ -219,18 +262,14 @@ class SiteCreationMainVM @Inject constructor( } private fun clearOldSiteCreationState(wizardStep: SiteCreationStep) { - when (wizardStep) { - SiteCreationStep.SITE_DESIGNS -> Unit // Do nothing - SiteCreationStep.DOMAINS -> siteCreationState.domain?.let { + if (wizardStep == SiteCreationStep.DOMAINS) { + siteCreationState.domain?.let { siteCreationState = siteCreationState.copy(domain = null) } - SiteCreationStep.SITE_PREVIEW -> Unit // Do nothing - SiteCreationStep.INTENTS -> Unit // Do nothing - SiteCreationStep.SITE_NAME -> Unit // Do nothing } } - fun onDomainsScreenFinished(domain: String) { + fun onDomainsScreenFinished(domain: DomainModel) { siteCreationState = siteCreationState.copy(domain = domain) wizardManager.showNextStep() } @@ -254,10 +293,43 @@ class SiteCreationMainVM @Inject constructor( } } - fun onSiteCreationCompleted() { - siteCreationCompleted = true + fun onCartCreated(checkoutDetails: CheckoutDetails) { + siteCreationState = siteCreationState.copy(result = CreatedButNotFetched.InCart(checkoutDetails.site)) + domainsRegistrationTracker.trackDomainsPurchaseWebviewViewed(checkoutDetails.site, isSiteCreation = true) + _showDomainCheckout.value = checkoutDetails + } + + fun onCheckoutResult(event: DomainRegistrationCompletedEvent?) { + if (event == null) return onBackPressed() + siteCreationState = siteCreationState.run { + check(result is CreatedButNotFetched.InCart) + copy( + result = CreatedButNotFetched.DomainRegistrationPurchased( + event.domainName, + event.email, + result.site, + ) + ) + } + wizardManager.showNextStep() } + fun onFreeSiteCreated(site: SiteModel) { + siteCreationState = siteCreationState.copy(result = CreatedButNotFetched.NotInLocalDb(site)) + wizardManager.showNextStep() + } + + fun onWizardCancelled() { + _onCompleted.value = NotCreated to isSiteTitleTaskCompleted() + } + + fun onWizardFinished(result: Created) { + siteCreationState = siteCreationState.copy(result = result) + _onCompleted.value = result to isSiteTitleTaskCompleted() + } + + private fun isSiteTitleTaskCompleted() = !siteCreationState.siteName.isNullOrBlank() + /** * Exits the flow and tracks an event when the user force-exits the "site creation in progress" before it completes. */ @@ -268,26 +340,14 @@ class SiteCreationMainVM @Inject constructor( _exitFlowObservable.call() } - fun onSitePreviewScreenFinished(createSiteState: CreateSiteState) { - _wizardFinishedObservable.value = createSiteState - } - fun onPositiveDialogButtonClicked(instanceTag: String) { - when (instanceTag) { - TAG_WARNING_DIALOG -> { - exitFlow(true) - } - else -> NotImplementedError("Unknown dialog tag: $instanceTag") - } + check(instanceTag == TAG_WARNING_DIALOG) { "Unknown dialog tag: $instanceTag" } + exitFlow(true) } fun onNegativeDialogButtonClicked(instanceTag: String) { - when (instanceTag) { - TAG_WARNING_DIALOG -> { - // do nothing - } - else -> NotImplementedError("Unknown dialog tag: $instanceTag") - } + check(instanceTag == TAG_WARNING_DIALOG) { "Unknown dialog tag: $instanceTag" } + // do nothing } sealed class SiteCreationScreenTitle { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationStep.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationStep.kt index c46b0dfda43d..6a2e52a4fead 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationStep.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/SiteCreationStep.kt @@ -2,6 +2,7 @@ package org.wordpress.android.ui.sitecreation import org.wordpress.android.ui.sitecreation.SiteCreationStep.DOMAINS import org.wordpress.android.ui.sitecreation.SiteCreationStep.INTENTS +import org.wordpress.android.ui.sitecreation.SiteCreationStep.PROGRESS import org.wordpress.android.ui.sitecreation.SiteCreationStep.SITE_DESIGNS import org.wordpress.android.ui.sitecreation.SiteCreationStep.SITE_NAME import org.wordpress.android.ui.sitecreation.SiteCreationStep.SITE_PREVIEW @@ -12,7 +13,7 @@ import javax.inject.Inject import javax.inject.Singleton enum class SiteCreationStep : WizardStep { - SITE_DESIGNS, DOMAINS, SITE_PREVIEW, INTENTS, SITE_NAME; + SITE_DESIGNS, DOMAINS, PROGRESS, SITE_PREVIEW, INTENTS, SITE_NAME; } @Singleton @@ -24,8 +25,8 @@ class SiteCreationStepsProvider @Inject constructor( private val isIntentsEnabled get() = siteIntentQuestionFeatureConfig.isEnabled() fun getSteps(): List = when { - isSiteNameEnabled -> listOf(INTENTS, SITE_NAME, SITE_DESIGNS, SITE_PREVIEW) - isIntentsEnabled -> listOf(INTENTS, SITE_DESIGNS, DOMAINS, SITE_PREVIEW) - else -> listOf(SITE_DESIGNS, DOMAINS, SITE_PREVIEW) + isSiteNameEnabled -> listOf(INTENTS, SITE_NAME, SITE_DESIGNS, PROGRESS, SITE_PREVIEW) + isIntentsEnabled -> listOf(INTENTS, SITE_DESIGNS, DOMAINS, PROGRESS, SITE_PREVIEW) + else -> listOf(SITE_DESIGNS, DOMAINS, PROGRESS, SITE_PREVIEW) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/DomainsScreenListener.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/DomainsScreenListener.kt index d79672868bde..96ea55347d88 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/DomainsScreenListener.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/DomainsScreenListener.kt @@ -1,5 +1,17 @@ package org.wordpress.android.ui.sitecreation.domains +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + interface DomainsScreenListener { - fun onDomainSelected(domain: String) + fun onDomainSelected(domain: DomainModel) } + +@Parcelize +data class DomainModel( + val domainName: String, + val isFree: Boolean, + val cost: String, + val productId: Int, + val supportsPrivacy: Boolean, +) : Parcelable diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainViewHolder.kt index ac5d14c53216..9b425fc77ff7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainViewHolder.kt @@ -10,7 +10,7 @@ import org.wordpress.android.R import org.wordpress.android.databinding.SiteCreationDomainsItemBinding import org.wordpress.android.databinding.SiteCreationDomainsItemV2Binding import org.wordpress.android.databinding.SiteCreationSuggestionsErrorItemBinding -import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.theme.AppThemeWithoutBackground import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.Old import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.Old.DomainUiState.AvailableDomain @@ -72,7 +72,7 @@ sealed class SiteCreationDomainViewHolder(protected val binding fun onBind(uiState: New.DomainUiState) = with(binding) { composeView.setContent { - AppTheme { + AppThemeWithoutBackground { DomainItem(uiState) } } 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 22b0449ce705..c8d70b174de3 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 @@ -4,6 +4,7 @@ import android.content.Context import android.os.Bundle import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -48,10 +49,7 @@ class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { return R.layout.site_creation_domains_screen } - @Suppress("UseCheckOrError") - override val screenTitle: String - get() = arguments?.getString(EXTRA_SCREEN_TITLE) - ?: throw IllegalStateException("Required argument screen title is missing.") + override val screenTitle get() = requireArguments().getString(EXTRA_SCREEN_TITLE).orEmpty() override fun setBindingViewStubListener(parentBinding: SiteCreationFormScreenBinding) { parentBinding.siteCreationFormContentStub.setOnInflateListener { _, inflated -> @@ -83,25 +81,25 @@ class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { } private fun SiteCreationDomainsScreenBinding.initViewModel() { - viewModel.uiState.observe(this@SiteCreationDomainsFragment, { uiState -> + viewModel.uiState.observe(this@SiteCreationDomainsFragment) { uiState -> uiState?.let { searchInputWithHeader?.updateHeader(requireActivity(), uiState.headerUiState) searchInputWithHeader?.updateSearchInput(requireActivity(), uiState.searchInputUiState) updateContentUiState(uiState.contentState) - uiHelpers.updateVisibility(createSiteButtonContainer, uiState.createSiteButtonContainerVisibility) - uiHelpers.updateVisibility(createSiteButtonShadow, uiState.createSiteButtonContainerVisibility) + createSiteButtonContainer.isVisible = uiState.createSiteButtonState != null + createSiteButton.text = uiState.createSiteButtonState?.stringRes?.let(::getString) updateTitleVisibility(uiState.headerUiState == null) } - }) - viewModel.clearBtnClicked.observe(this@SiteCreationDomainsFragment, { + } + viewModel.clearBtnClicked.observe(this@SiteCreationDomainsFragment) { searchInputWithHeader?.setInputText("") - }) - viewModel.createSiteBtnClicked.observe(this@SiteCreationDomainsFragment, { domain -> + } + viewModel.createSiteBtnClicked.observe(this@SiteCreationDomainsFragment) { domain -> domain?.let { (requireActivity() as DomainsScreenListener).onDomainSelected(domain) } - }) - viewModel.onHelpClicked.observe(this@SiteCreationDomainsFragment, { + } + viewModel.onHelpClicked.observe(this@SiteCreationDomainsFragment) { (requireActivity() as OnHelpClickedListener).onHelpClicked(HelpActivity.Origin.SITE_CREATION_DOMAINS) - }) + } viewModel.start() } @@ -111,6 +109,10 @@ class SiteCreationDomainsFragment : SiteCreationBaseFormFragment() { domainListEmptyViewMessage.text = uiHelpers.getTextOfUiString(requireContext(), contentState.message) } uiHelpers.updateVisibility(siteCreationDomainsScreenExample.root, contentState.exampleViewVisibility) + uiHelpers.updateVisibility( + siteCreationDomainsScreenExampleUpdated.root, + contentState.updatedExampleViewVisibility + ) (recyclerView.adapter as SiteCreationDomainsAdapter).update(contentState.items) if (contentState.items.isNotEmpty()) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt index 55ec92234731..9bd73b95be6e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModel.kt @@ -29,10 +29,11 @@ import org.wordpress.android.models.networkresource.ListState.Success import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.DomainSuggestionsQuery.UserQuery +import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.DomainsUiState.CreateSiteButtonState import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.DomainsUiState.DomainsUiContentState import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Cost -import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Variant +import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Tag import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.Old import org.wordpress.android.ui.sitecreation.misc.SiteCreationErrorType import org.wordpress.android.ui.sitecreation.misc.SiteCreationHeaderUiState @@ -43,13 +44,11 @@ import org.wordpress.android.ui.sitecreation.usecases.FETCH_DOMAINS_VENDOR_MOBIL import org.wordpress.android.ui.sitecreation.usecases.FetchDomainsUseCase import org.wordpress.android.ui.utils.UiString 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.AppLog import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.config.SiteCreationDomainPurchasingFeatureConfig import org.wordpress.android.util.extensions.isOnSale -import org.wordpress.android.util.extensions.saleCostForDisplay import org.wordpress.android.viewmodel.SingleLiveEvent import javax.inject.Inject import javax.inject.Named @@ -89,8 +88,8 @@ class SiteCreationDomainsViewModel @Inject constructor( } } - private val _createSiteBtnClicked = SingleLiveEvent() - val createSiteBtnClicked: LiveData = _createSiteBtnClicked + private val _createSiteBtnClicked = SingleLiveEvent() + val createSiteBtnClicked: LiveData = _createSiteBtnClicked private val _clearBtnClicked = SingleLiveEvent() val clearBtnClicked = _clearBtnClicked @@ -122,6 +121,7 @@ class SiteCreationDomainsViewModel @Inject constructor( result.isError -> { AppLog.e(AppLog.T.DOMAIN_REGISTRATION, "Error while fetching domain products: ${result.error}") } + else -> { AppLog.d(AppLog.T.DOMAIN_REGISTRATION, result.products.toString()) products = result.products.orEmpty().associateBy { it.productId } @@ -135,7 +135,7 @@ class SiteCreationDomainsViewModel @Inject constructor( "Create site button should not be visible if a domain is not selected" } tracker.trackDomainSelected(domain.domainName, currentQuery?.value ?: "") - _createSiteBtnClicked.value = domain.domainName + _createSiteBtnClicked.value = domain } fun onClearTextBtnClicked() = _clearBtnClicked.call() @@ -225,6 +225,7 @@ class SiteCreationDomainsViewModel @Inject constructor( isFree = is_free, cost = cost.orEmpty(), productId = product_id, + supportsPrivacy = supports_privacy, ) } @@ -246,12 +247,15 @@ class SiteCreationDomainsViewModel @Inject constructor( showDivider = state.data.isNotEmpty() ), contentState = createDomainsUiContentState(query, state, emptyListMessage), - createSiteButtonContainerVisibility = getCreateSiteButtonState() + createSiteButtonState = getCreateSiteButtonState() ) } - private fun getCreateSiteButtonState(): Boolean { - return selectedDomain?.isFree ?: false + private fun getCreateSiteButtonState() = selectedDomain?.run { + when (purchasingFeatureConfig.isEnabledOrManuallyOverridden()) { + true -> if (isFree) CreateSiteButtonState.Free else CreateSiteButtonState.Paid + else -> CreateSiteButtonState.Old + } } private fun createDomainsUiContentState( @@ -272,7 +276,7 @@ class SiteCreationDomainsViewModel @Inject constructor( return if (items.isEmpty()) { if (isNonEmptyUserQuery(query) && (state is Success || state is Ready)) { DomainsUiContentState.Empty(emptyListMessage) - } else DomainsUiContentState.Initial + } else DomainsUiContentState.Initial(purchasingFeatureConfig.isEnabledOrManuallyOverridden()) } else { DomainsUiContentState.VisibleItems(items) } @@ -316,18 +320,22 @@ class SiteCreationDomainsViewModel @Inject constructor( domain.domainName, cost = when { domain.isFree -> Cost.Free - product.isOnSale() -> Cost.OnSale(product.saleCostForDisplay(), domain.cost) + product.isOnSale() -> Cost.OnSale(product?.combinedSaleCostDisplay.orEmpty(), domain.cost) else -> Cost.Paid(domain.cost) }, + isSelected = domain.domainName == selectedDomain?.domainName, onClick = { onDomainSelected(domain) }, - variant = when { - index == 0 -> Variant.Recommended - index == 1 -> Variant.BestAlternative - product.isOnSale() -> Variant.Sale - else -> null - }, + tags = listOfNotNull( + when (index) { + 0 -> Tag.Recommended + 1 -> Tag.BestAlternative + else -> null + }, + if (product.isOnSale()) Tag.Sale else null, + ), ) } + else -> { Old.DomainUiState.AvailableDomain( domainSanitizer.getName(domain.domainName), @@ -357,22 +365,31 @@ class SiteCreationDomainsViewModel @Inject constructor( } else null } - private fun createHeaderUiState( - isVisible: Boolean - ): SiteCreationHeaderUiState? { - return if (isVisible) SiteCreationHeaderUiState( - UiStringRes(R.string.new_site_creation_domain_header_title), - UiStringRes(R.string.new_site_creation_domain_header_subtitle) - ) else null - } + private fun createHeaderUiState(isVisible: Boolean) = if (!isVisible) null else + purchasingFeatureConfig.isEnabledOrManuallyOverridden().let { isPurchasingEnabled -> + SiteCreationHeaderUiState( + title = UiStringRes(R.string.new_site_creation_domain_header_title), + subtitle = UiStringRes( + if (isPurchasingEnabled) R.string.site_creation_domain_header_subtitle + else R.string.new_site_creation_domain_header_subtitle, + ), + isStartAligned = isPurchasingEnabled + ) + } - private fun createSearchInputUiState( +private fun createSearchInputUiState( showProgress: Boolean, showClearButton: Boolean, showDivider: Boolean, ): SiteCreationSearchInputUiState { + val hint = UiStringRes( + if (purchasingFeatureConfig.isEnabledOrManuallyOverridden()) + R.string.site_creation_domain_search_input_hint + else + R.string.new_site_creation_search_domain_input_hint + ) return SiteCreationSearchInputUiState( - hint = UiStringRes(R.string.new_site_creation_search_domain_input_hint), + hint = hint, showProgress = showProgress, showClearButton = showClearButton, showDivider = showDivider, @@ -382,47 +399,50 @@ class SiteCreationDomainsViewModel @Inject constructor( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) fun onDomainSelected(domain: DomainModel) { - selectedDomain = domain.takeIf { it.isFree } + selectedDomain = domain } private fun isNonEmptyUserQuery(query: DomainSuggestionsQuery?) = query is UserQuery && query.value.isNotBlank() - data class DomainModel( - val domainName: String, - val isFree: Boolean, - val cost: String, - val productId: Int, - ) - data class DomainsUiState( val headerUiState: SiteCreationHeaderUiState?, val searchInputUiState: SiteCreationSearchInputUiState, - val contentState: DomainsUiContentState = DomainsUiContentState.Initial, - val createSiteButtonContainerVisibility: Boolean + val contentState: DomainsUiContentState, + val createSiteButtonState: CreateSiteButtonState? ) { sealed class DomainsUiContentState( val emptyViewVisibility: Boolean, val exampleViewVisibility: Boolean, + val updatedExampleViewVisibility: Boolean, val items: List ) { - object Initial : DomainsUiContentState( + class Initial(isUpdatedExample: Boolean) : DomainsUiContentState( emptyViewVisibility = false, - exampleViewVisibility = true, + exampleViewVisibility = !isUpdatedExample, + updatedExampleViewVisibility = isUpdatedExample, items = emptyList() ) class Empty(val message: UiString?) : DomainsUiContentState( emptyViewVisibility = true, exampleViewVisibility = false, + updatedExampleViewVisibility = false, items = emptyList() ) class VisibleItems(items: List) : DomainsUiContentState( emptyViewVisibility = false, exampleViewVisibility = false, + updatedExampleViewVisibility = false, items = items ) } + + sealed class CreateSiteButtonState(@StringRes val stringRes: Int) { + object Old : CreateSiteButtonState(R.string.site_creation_domain_finish_button) + object Free : CreateSiteButtonState(R.string.site_creation_domain_button_continue_with_subdomain) + object Paid : CreateSiteButtonState(R.string.site_creation_domain_button_purchase_domain) + } } sealed class ListItemUiState(open val type: Type) { @@ -465,32 +485,33 @@ class SiteCreationDomainsViewModel @Inject constructor( data class DomainUiState( val domainName: String, val cost: Cost, + val isSelected: Boolean = false, val onClick: () -> Unit, - val variant: Variant? = null, + val tags: List = emptyList(), ) : New(Type.DOMAIN_V2) { - sealed class Variant( + sealed class Tag( @ColorRes val dotColor: Int, @ColorRes val subtitleColor: Int? = null, val subtitle: UiString, ) { constructor(@ColorRes color: Int, subtitle: UiString) : this(color, color, subtitle) - object Unavailable : Variant( + object Unavailable : Tag( R.color.red_50, UiStringRes(R.string.site_creation_domain_tag_unavailable), ) - object Recommended : Variant( + object Recommended : Tag( R.color.jetpack_green_50, UiStringRes(R.string.site_creation_domain_tag_recommended), ) - object BestAlternative : Variant( + object BestAlternative : Tag( R.color.purple_50, UiStringRes(R.string.site_creation_domain_tag_best_alternative), ) - object Sale : Variant( + object Sale : Tag( R.color.yellow_50, UiStringRes(R.string.site_creation_domain_tag_sale) ) @@ -499,17 +520,15 @@ class SiteCreationDomainsViewModel @Inject constructor( sealed class Cost(val title: UiString) { object Free : Cost(UiStringRes(R.string.free)) - data class Paid(val cost: String) : Cost( - UiStringResWithParams(R.string.site_creation_domain_cost, UiStringText(cost)) - ) + data class Paid(private val titleCost: String) : Cost(UiStringText(titleCost)) { + val subtitle = UiStringRes(R.string.site_creation_domain_cost) + } - data class OnSale(val titleCost: String, val subtitleCost: String) : Cost( - UiStringResWithParams(R.string.site_creation_domain_cost, UiStringText(titleCost)) + data class OnSale(private val titleCost: String, private val strikeoutTitleCost: String) : Cost( + UiStringText(titleCost) ) { - val subtitle = UiStringResWithParams( - R.string.site_creation_domain_cost, - UiStringText(subtitleCost) - ) + val strikeoutTitle = UiStringText(strikeoutTitleCost) + val subtitle = UiStringRes(R.string.site_creation_domain_cost_sale) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/compose/DomainItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/compose/DomainItem.kt index 89068cddcd13..7b046dd31751 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/compose/DomainItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/domains/compose/DomainItem.kt @@ -1,79 +1,107 @@ package org.wordpress.android.ui.sitecreation.domains.compose import android.content.res.Configuration +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.Divider -import androidx.compose.material.MaterialTheme +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme.colors import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color.Companion.Unspecified import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.wordpress.android.R.string import org.wordpress.android.ui.compose.components.SolidCircle -import org.wordpress.android.ui.compose.theme.AppColor -import org.wordpress.android.ui.compose.theme.AppTheme +import org.wordpress.android.ui.compose.theme.AppThemeWithoutBackground import org.wordpress.android.ui.compose.unit.Margin import org.wordpress.android.ui.compose.utils.asString import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Cost -import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Variant.BestAlternative -import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Variant.Recommended -import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Variant.Sale -import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Variant.Unavailable +import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Tag.BestAlternative +import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Tag.Recommended +import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Tag.Sale +import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Tag.Unavailable -private val SecondaryTextColor @Composable get() = MaterialTheme.colors.onSurface.copy(alpha = 0.46f) +private val HighlightBgColor @Composable get() = colors.primary.copy(0.1f) +private val SecondaryTextColor @Composable get() = colors.onSurface.copy(0.46f) private val SecondaryFontSize = 13.sp private val PrimaryFontSize = 17.sp +private val StartPadding = 40.dp @Composable fun DomainItem(uiState: DomainUiState) = with(uiState) { - Column { + Column(Modifier.background(if (isSelected) HighlightBgColor else Unspecified)) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .clickable { onClick.invoke() } + .clickable( + interactionSource = remember(::MutableInteractionSource), + indication = rememberRipple(color = HighlightBgColor), + onClick = onClick::invoke, + ) .padding(vertical = Margin.ExtraLarge.value) .padding(end = Margin.ExtraLarge.value) ) { Box( contentAlignment = Alignment.Center, - modifier = Modifier.width(Margin.ExtraMediumLarge.value) + modifier = Modifier.width(StartPadding) ) { - variant?.dotColor?.let { - SolidCircle(size = 8.dp, colorResource(it)) + if (isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = stringResource(string.selected), + tint = colors.primary, + modifier = Modifier.size(16.dp), + ) + } else { + tags.firstOrNull()?.dotColor?.let { + SolidCircle(size = 8.dp, colorResource(it)) + } } } Column(verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.weight(1f)) { Text( text = domainName, - color = MaterialTheme.colors.onSurface.takeIf { variant !is Unavailable } ?: SecondaryTextColor, + color = colors.onSurface.takeIf { tags.none { it is Unavailable } } ?: SecondaryTextColor, fontSize = PrimaryFontSize, overflow = TextOverflow.Ellipsis, maxLines = 1, + modifier = Modifier.padding(bottom = 4.dp) ) - variant?.run { - Text( - text = subtitle.asString(), - color = subtitleColor?.let { colorResource(it) } ?: SecondaryTextColor, - fontSize = SecondaryFontSize, - ) + tags.forEach { tag -> + tag.run { + Text( + text = subtitle.asString(), + color = subtitleColor?.let { colorResource(it) } ?: SecondaryTextColor, + fontSize = SecondaryFontSize, + ) + } } } - if (variant !is Unavailable) { + if (tags.none { it is Unavailable }) { if (cost is Cost.OnSale) { SalePrince( - cost.title.asString(), + cost.strikeoutTitle.asString() to cost.title.asString(), cost.subtitle.asString(), modifier = Modifier.padding(start = Margin.ExtraLarge.value) ) @@ -100,21 +128,32 @@ private fun Price(text: String, modifier: Modifier = Modifier) { } @Composable -private fun SalePrince(title: String, subtitle: String, modifier: Modifier = Modifier) { +private fun SalePrince(title: Pair, subtitle: String, modifier: Modifier = Modifier) { Column( modifier, horizontalAlignment = Alignment.End, ) { - Text( - title, - color = AppColor.JetpackGreen50, - fontSize = PrimaryFontSize, - ) + title.let { (strikethroughText, normalText) -> + Row(verticalAlignment = Alignment.Bottom) { + Text( + strikethroughText, + color = SecondaryTextColor, + fontSize = SecondaryFontSize, + textDecoration = TextDecoration.LineThrough, + modifier = Modifier.padding(end = 4.dp) + ) + Text( + normalText, + color = colors.primary, + fontSize = PrimaryFontSize, + ) + } + } Text( subtitle, - color = SecondaryTextColor, + color = colors.primary, fontSize = SecondaryFontSize, - textDecoration = TextDecoration.LineThrough, + modifier = Modifier.padding(top = 4.dp) ) } } @@ -131,20 +170,23 @@ private fun DomainItemPreview() { }, cost = when { it % 3 == 0 -> Cost.Paid("$${it * 5}") - it == 5 -> Cost.OnSale("$${it * 2}", "$${it * 3}") + it in 1..2 -> Cost.OnSale("$${it * 2}", "$${it * 3}") else -> Cost.Free }, - variant = when (it) { - 0 -> Unavailable - 1 -> Recommended - 2 -> BestAlternative - 5 -> Sale - else -> null - }, + tags = listOfNotNull( + when (it) { + 0 -> Unavailable + 1 -> Recommended + 2 -> BestAlternative + else -> null + }, + if (it in 1..2) Sale else null, + ), + isSelected = it == 5, onClick = {} ) } - AppTheme { + AppThemeWithoutBackground { Column { uiStates.forEach { DomainItem(it) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SearchInputWithHeader.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SearchInputWithHeader.kt index 3f60ac1f9ea8..a93344b0bf6b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SearchInputWithHeader.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SearchInputWithHeader.kt @@ -57,6 +57,10 @@ class SearchInputWithHeader(private val uiHelpers: UiHelpers, rootView: View, on uiHelpers.updateVisibility(headerLayout, true) headerTitle.text = uiHelpers.getTextOfUiString(context, uiState.title) headerSubtitle.text = uiHelpers.getTextOfUiString(context, uiState.subtitle) + if (uiState.isStartAligned) { + headerTitle.textAlignment = View.TEXT_ALIGNMENT_VIEW_START + headerSubtitle.textAlignment = View.TEXT_ALIGNMENT_VIEW_START + } } ?: uiHelpers.updateVisibility(headerLayout, false) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationHeaderUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationHeaderUiState.kt index 82f53f7925b9..d18b4129fc01 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationHeaderUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/misc/SiteCreationHeaderUiState.kt @@ -2,4 +2,4 @@ package org.wordpress.android.ui.sitecreation.misc import org.wordpress.android.ui.utils.UiString -data class SiteCreationHeaderUiState(val title: UiString, val subtitle: UiString) +data class SiteCreationHeaderUiState(val title: UiString, val subtitle: UiString, val isStartAligned: Boolean) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt index 24c002fdb551..1bfc05af8d72 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SiteCreationPreviewFragment.kt @@ -1,11 +1,9 @@ package org.wordpress.android.ui.sitecreation.previews -import android.animation.Animator -import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.content.Context -import android.content.res.Configuration +import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.os.Bundle import android.text.Spannable import android.text.SpannableString @@ -15,99 +13,58 @@ import android.view.View.OnLayoutChangeListener import android.view.animation.DecelerateInterpolator import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.WordPress -import org.wordpress.android.databinding.FullscreenErrorWithRetryBinding import org.wordpress.android.databinding.SiteCreationFormScreenBinding import org.wordpress.android.databinding.SiteCreationPreviewScreenBinding import org.wordpress.android.databinding.SiteCreationPreviewScreenDefaultBinding -import org.wordpress.android.databinding.SiteCreationProgressCreatingSiteBinding -import org.wordpress.android.ui.accounts.HelpActivity +import org.wordpress.android.ui.sitecreation.SiteCreationActivity.Companion.ARG_STATE import org.wordpress.android.ui.sitecreation.SiteCreationBaseFormFragment import org.wordpress.android.ui.sitecreation.SiteCreationState -import org.wordpress.android.ui.sitecreation.misc.OnHelpClickedListener -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewData -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewContentUiState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewFullscreenErrorUiState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewFullscreenProgressUiState import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewLoadingShimmerState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewWebErrorUiState -import org.wordpress.android.ui.sitecreation.services.SiteCreationService +import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.UrlData import org.wordpress.android.ui.utils.UiHelpers -import org.wordpress.android.util.AniUtils -import org.wordpress.android.util.AppLog -import org.wordpress.android.util.AutoForeground.ServiceEventConnection import org.wordpress.android.util.ErrorManagedWebViewClient.ErrorManagedWebViewClientListener import org.wordpress.android.util.URLFilteredWebViewClient +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.widgets.NestedWebView import javax.inject.Inject -private const val ARG_DATA = "arg_site_creation_data" private const val SLIDE_IN_ANIMATION_DURATION = 450L @AndroidEntryPoint class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), ErrorManagedWebViewClientListener { - /** - * We need to connect to the service, so the service knows when the app is in the background. The service - * automatically shows system notifications when site creation is in progress and the app is in the background. - */ - private var serviceEventConnection: ServiceEventConnection? = null - private val viewModel: SitePreviewViewModel by viewModels() - private var animatorSet: AnimatorSet? = null - private val isLandscape: Boolean - get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - @Inject internal lateinit var uiHelpers: UiHelpers - private var binding: SiteCreationPreviewScreenBinding? = null + private val isLandscape get() = resources.configuration.orientation == ORIENTATION_LANDSCAPE - @Suppress("UseCheckOrError") - override fun onAttach(context: Context) { - super.onAttach(context) - if (context !is SitePreviewScreenListener) { - throw IllegalStateException("Parent activity must implement SitePreviewScreenListener.") - } - if (context !is OnHelpClickedListener) { - throw IllegalStateException("Parent activity must implement OnHelpClickedListener.") - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (savedInstanceState == null) { - // we need to manually clear the SiteCreationService state so we don't for example receive sticky events - // from the previous run of the SiteCreation flow. - SiteCreationService.clearSiteCreationServiceState() - } - } + private lateinit var binding: SiteCreationPreviewScreenBinding + private val viewModel: SitePreviewViewModel by activityViewModels() - @Suppress("DEPRECATION") + @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - - (requireActivity() as AppCompatActivity).supportActionBar?.hide() - - viewModel.start(requireArguments()[ARG_DATA] as SiteCreationState, savedInstanceState) + init() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + init() + } + + private fun init() { (requireActivity() as AppCompatActivity).supportActionBar?.hide() - viewModel.start(requireArguments()[ARG_DATA] as SiteCreationState, savedInstanceState) + viewModel.start(requireNotNull(requireArguments().getParcelableCompat(ARG_STATE))) } - override fun getContentLayout(): Int { - return R.layout.site_creation_preview_screen - } + override fun getContentLayout() = R.layout.site_creation_preview_screen - @Suppress("UseCheckOrError") - override val screenTitle: String - get() = arguments?.getString(EXTRA_SCREEN_TITLE) - ?: throw IllegalStateException("Required argument screen title is missing.") + override val screenTitle get() = requireArguments().getString(EXTRA_SCREEN_TITLE).orEmpty() override fun setBindingViewStubListener(parentBinding: SiteCreationFormScreenBinding) { parentBinding.siteCreationFormContentStub.setOnInflateListener { _, inflated -> @@ -116,110 +73,43 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), } override fun setupContent() { - binding?.siteCreationPreviewScreenDefault?.initViewModel() - binding?.siteCreationPreviewScreenDefault?.fullscreenErrorWithRetry?.initRetryButton() - binding?.siteCreationPreviewScreenDefault?.initOkButton() - binding?.siteCreationPreviewScreenDefault?.fullscreenErrorWithRetry?.initCancelWizardButton() - binding?.siteCreationPreviewScreenDefault?.fullscreenErrorWithRetry?.initContactSupportButton() + binding.siteCreationPreviewScreenDefault.run { + observeState() + observePreview(siteCreationPreviewWebViewContainer.sitePreviewWebView) + okButton.setOnClickListener { viewModel.onOkButtonClicked() } + } } - private fun SiteCreationPreviewScreenDefaultBinding.initViewModel() { - viewModel.uiState.observe(this@SiteCreationPreviewFragment, { uiState -> - uiState?.let { - when (uiState) { - is SitePreviewContentUiState -> updateContentLayout(uiState.data) - is SitePreviewWebErrorUiState -> updateContentLayout(uiState.data) - is SitePreviewLoadingShimmerState -> updateContentLayout(uiState.data) - is SitePreviewFullscreenProgressUiState -> - siteCreationProgressCreatingSite.updateLoadingLayout(uiState) - is SitePreviewFullscreenErrorUiState -> - fullscreenErrorWithRetry.updateErrorLayout(uiState) + private fun SiteCreationPreviewScreenDefaultBinding.observeState() { + viewModel.uiState.observe(this@SiteCreationPreviewFragment) { + it?.let { ui -> + uiHelpers.setTextOrHide(siteCreationPreviewHeaderItem.sitePreviewSubtitle, ui.subtitle) + uiHelpers.setTextOrHide(sitePreviewCaption, ui.caption) + updateContentLayout(ui.urlData, isFirstContent = ui is SitePreviewLoadingShimmerState) + siteCreationPreviewWebViewContainer.apply { + uiHelpers.updateVisibility(sitePreviewWebView, ui.webViewVisibility) + uiHelpers.updateVisibility(sitePreviewWebError, ui.webViewErrorVisibility) + uiHelpers.updateVisibility(sitePreviewWebViewShimmerLayout, ui.shimmerVisibility) } - uiHelpers.updateVisibility( - siteCreationProgressCreatingSite.progressLayout, - uiState.fullscreenProgressLayoutVisibility - ) - - uiHelpers.updateVisibility(contentLayout, uiState.contentLayoutVisibility) - uiHelpers.updateVisibility( - siteCreationPreviewWebViewContainer.sitePreviewWebView, - uiState.webViewVisibility - ) - uiHelpers.updateVisibility( - siteCreationPreviewWebViewContainer.sitePreviewWebError, - uiState.webViewErrorVisibility - ) - uiHelpers.updateVisibility( - siteCreationPreviewWebViewContainer.sitePreviewWebViewShimmerLayout, - uiState.shimmerVisibility - ) - uiHelpers.updateVisibility( - fullscreenErrorWithRetry.errorLayout, - uiState.fullscreenErrorLayoutVisibility - ) } - }) - - viewModel.preloadPreview.observe(this@SiteCreationPreviewFragment, { url -> - url?.let { urlString -> - siteCreationPreviewWebViewContainer.sitePreviewWebView.webViewClient = - URLFilteredWebViewClient(urlString, this@SiteCreationPreviewFragment) - siteCreationPreviewWebViewContainer.sitePreviewWebView.settings.userAgentString = - WordPress.getUserAgent() - siteCreationPreviewWebViewContainer.sitePreviewWebView.loadUrl(urlString) - } - }) - - viewModel.startCreateSiteService.observe(this@SiteCreationPreviewFragment, { startServiceData -> - startServiceData?.let { - SiteCreationService.createSite( - requireNotNull(activity), - startServiceData.previousState, - startServiceData.serviceData - ) - } - }) - - initClickObservers() + } } - private fun initClickObservers() { - viewModel.onHelpClicked.observe(this, { - (requireActivity() as OnHelpClickedListener).onHelpClicked(HelpActivity.Origin.SITE_CREATION_CREATING) - }) - viewModel.onSiteCreationCompleted.observe(this, { - (requireActivity() as SitePreviewScreenListener).onSiteCreationCompleted() - }) - viewModel.onOkButtonClicked.observe(this, { createSiteState -> - createSiteState?.let { - (requireActivity() as SitePreviewScreenListener).onSitePreviewScreenDismissed(createSiteState) - } - }) - viewModel.onCancelWizardClicked.observe(this, { createSiteState -> - createSiteState?.let { - (requireActivity() as SitePreviewScreenListener).onSitePreviewScreenDismissed(createSiteState) + private fun observePreview(webView: NestedWebView) { + viewModel.preloadPreview.observe(this) { url -> + url?.let { urlString -> + webView.webViewClient = URLFilteredWebViewClient(urlString, this) + webView.settings.userAgentString = WordPress.getUserAgent() + webView.loadUrl(urlString) } - }) - } - - private fun FullscreenErrorWithRetryBinding.initRetryButton() { - errorRetry.setOnClickListener { viewModel.retry() } - } - - private fun SiteCreationPreviewScreenDefaultBinding.initOkButton() { - okButton.setOnClickListener { viewModel.onOkButtonClicked() } - } - - private fun FullscreenErrorWithRetryBinding.initCancelWizardButton() { - cancelWizardButton.setOnClickListener { viewModel.onCancelWizardClicked() } - } - - private fun FullscreenErrorWithRetryBinding.initContactSupportButton() { - contactSupport.setOnClickListener { viewModel.onHelpClicked() } + } } - private fun SiteCreationPreviewScreenDefaultBinding.updateContentLayout(sitePreviewData: SitePreviewData) { - sitePreviewData.apply { + private fun SiteCreationPreviewScreenDefaultBinding.updateContentLayout( + urlData: UrlData, + isFirstContent: Boolean = false, + ) { + urlData.apply { siteCreationPreviewWebViewContainer.sitePreviewWebUrlTitle.text = createSpannableUrl( requireNotNull(activity), shortUrl, @@ -227,69 +117,9 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), domainIndices ) } - if (contentLayout.visibility == View.GONE) { + if (isFirstContent) { animateContentTransition() - view?.announceForAccessibility( - getString(R.string.new_site_creation_preview_title) + - getString(R.string.new_site_creation_site_preview_content_description) - ) - } - } - - private fun SiteCreationProgressCreatingSiteBinding.updateLoadingLayout( - progressUiState: SitePreviewFullscreenProgressUiState - ) { - progressUiState.apply { - val newText = uiHelpers.getTextOfUiString(progressText.context, loadingTextResId) - AppLog.d(AppLog.T.MAIN, "Changing text - animation: $animate") - if (animate) { - updateLoadingTextWithFadeAnimation(newText) - } else { - progressText.text = newText - } - } - } - - private fun SiteCreationProgressCreatingSiteBinding.updateLoadingTextWithFadeAnimation(newText: CharSequence) { - val animationDuration = AniUtils.Duration.SHORT - val fadeOut = AniUtils.getFadeOutAnim( - progressTextLayout, - animationDuration, - View.VISIBLE - ) - val fadeIn = AniUtils.getFadeInAnim( - progressTextLayout, - animationDuration - ) - - // update the text when the view isn't visible - fadeIn.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { - progressText.text = newText - } - - override fun onAnimationEnd(animation: Animator?) { - super.onAnimationEnd(animation) - animatorSet = null - } - }) - // Start the fade-in animation right after the view fades out - fadeIn.startDelay = animationDuration.toMillis(progressTextLayout.context) - - animatorSet = AnimatorSet().apply { - playSequentially(fadeOut, fadeIn) - start() - } - } - - private fun FullscreenErrorWithRetryBinding.updateErrorLayout( - errorUiStateState: SitePreviewFullscreenErrorUiState - ) { - errorUiStateState.apply { - uiHelpers.setTextOrHide(errorTitle, titleResId) - uiHelpers.setTextOrHide(errorSubtitle, subtitleResId) - uiHelpers.updateVisibility(contactSupport, errorUiStateState.showContactSupport) - uiHelpers.updateVisibility(cancelWizardButton, errorUiStateState.showCancelWizardButton) + view?.announceForAccessibility(getString(R.string.new_site_creation_site_preview_content_description)) } } @@ -325,17 +155,11 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), return spannableTitle } - override fun onWebViewPageLoaded() { - viewModel.onUrlLoaded() - } + override fun onWebViewPageLoaded() = viewModel.onUrlLoaded() - override fun onWebViewReceivedError() { - viewModel.onWebViewError() - } + override fun onWebViewReceivedError() = viewModel.onWebViewError() - override fun onHelp() { - viewModel.onHelpClicked() - } + override fun onHelp() = Unit // noop private fun SiteCreationPreviewScreenDefaultBinding.animateContentTransition() { contentLayout.addOnLayoutChangeListener( @@ -360,8 +184,7 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), contentHeight ) - // OK button should slide in if the container exists and fade in otherwise - // difference between land & portrait + // OK button slides in if the container exists else it fades in the diff between land & portrait val okAnim = if (isLandscape) { createFadeInAnimator(okButton) } else { @@ -398,41 +221,17 @@ class SiteCreationPreviewFragment : SiteCreationBaseFormFragment(), private fun createFadeInAnimator(view: View) = ObjectAnimator.ofFloat(view, "alpha", 0f, 1f) - override fun onResume() { - super.onResume() - serviceEventConnection = ServiceEventConnection(context, SiteCreationService::class.java, viewModel) - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - viewModel.writeToBundle(outState) - } - - override fun onPause() { - super.onPause() - serviceEventConnection?.disconnect(context, viewModel) - } - - override fun onStop() { - super.onStop() - if (animatorSet?.isRunning == true) { - animatorSet?.cancel() - } - } - companion object { const val TAG = "site_creation_preview_fragment_tag" fun newInstance( screenTitle: String, - siteCreationData: SiteCreationState - ): SiteCreationPreviewFragment { - val fragment = SiteCreationPreviewFragment() - val bundle = Bundle() - bundle.putString(EXTRA_SCREEN_TITLE, screenTitle) - bundle.putParcelable(ARG_DATA, siteCreationData) - fragment.arguments = bundle - return fragment + siteCreationState: SiteCreationState, + ) = SiteCreationPreviewFragment().apply { + arguments = Bundle().apply { + putString(EXTRA_SCREEN_TITLE, screenTitle) + putParcelable(ARG_STATE, siteCreationState) + } } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewScreenListener.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewScreenListener.kt deleted file mode 100644 index d8c4ea075b2c..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewScreenListener.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.wordpress.android.ui.sitecreation.previews - -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState - -interface SitePreviewScreenListener { - fun onSitePreviewScreenDismissed(createSiteState: CreateSiteState) - fun onSiteCreationCompleted() -} 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 e78740aa5e8b..5e0fdb6ac0a6 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 @@ -1,8 +1,5 @@ package org.wordpress.android.ui.sitecreation.previews -import android.annotation.SuppressLint -import android.os.Bundle -import android.os.Parcelable import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -10,109 +7,46 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.parcelize.Parcelize -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode import org.wordpress.android.R import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD +import org.wordpress.android.ui.sitecreation.SiteCreationResult.Completed +import org.wordpress.android.ui.sitecreation.SiteCreationResult.Created +import org.wordpress.android.ui.sitecreation.SiteCreationResult.CreatedButNotFetched import org.wordpress.android.ui.sitecreation.SiteCreationState -import org.wordpress.android.ui.sitecreation.misc.SiteCreationErrorType.INTERNET_UNAVAILABLE_ERROR -import org.wordpress.android.ui.sitecreation.misc.SiteCreationErrorType.UNKNOWN import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState.SiteCreationCompleted -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState.SiteNotCreated -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState.SiteNotInLocalDb import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewContentUiState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewFullscreenErrorUiState.SitePreviewConnectionErrorUiState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewFullscreenErrorUiState.SitePreviewGenericErrorUiState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewFullscreenProgressUiState import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewLoadingShimmerState import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewWebErrorUiState +import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.UrlData import org.wordpress.android.ui.sitecreation.services.FetchWpComSiteUseCase -import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceData -import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState -import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.CREATE_SITE -import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.FAILURE -import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.IDLE -import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.SUCCESS import org.wordpress.android.ui.sitecreation.usecases.isWordPressComSubDomain import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.ui.utils.UiString.UiStringResWithParams import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T -import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.UrlUtilsWrapper import org.wordpress.android.viewmodel.SingleLiveEvent import javax.inject.Inject import javax.inject.Named import kotlin.coroutines.CoroutineContext -const val KEY_CREATE_SITE_STATE = "CREATE_SITE_STATE" -private const val CONNECTION_ERROR_DELAY_TO_SHOW_LOADING_STATE = 1000L -private const val DELAY_TO_SHOW_WEB_VIEW_LOADING_SHIMMER = 1000L -const val LOADING_STATE_TEXT_ANIMATION_DELAY = 2000L -private const val ERROR_CONTEXT = "site_preview" - -private val loadingTexts = listOf( - UiStringRes(R.string.new_site_creation_creating_site_loading_1), - UiStringRes(R.string.new_site_creation_creating_site_loading_2), - UiStringRes(R.string.new_site_creation_creating_site_loading_3), - UiStringRes(R.string.new_site_creation_creating_site_loading_4) -) - @HiltViewModel class SitePreviewViewModel @Inject constructor( private val dispatcher: Dispatcher, private val siteStore: SiteStore, private val fetchWpComSiteUseCase: FetchWpComSiteUseCase, - private val networkUtils: NetworkUtilsWrapper, private val urlUtils: UrlUtilsWrapper, private val tracker: SiteCreationTracker, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher ) : ViewModel(), CoroutineScope { - private val job = Job() - override val coroutineContext: CoroutineContext - get() = bgDispatcher + job - private var isStarted = false - private var webviewFullyLoadedTracked = false - private var loadingAnimationJob: Job? = null - - private lateinit var siteCreationState: SiteCreationState - private var urlWithoutScheme: String? = null - private var siteTitle: String? = null - private var lastReceivedServiceState: SiteCreationServiceState? = null - private var serviceStateForRetry: SiteCreationServiceState? = null - private var createSiteState: CreateSiteState = SiteNotCreated - - private val _uiState: MutableLiveData = MutableLiveData() - val uiState: LiveData = _uiState - - private val _preloadPreview: MutableLiveData = MutableLiveData() - val preloadPreview: LiveData = _preloadPreview - - private val _startCreateSiteService: SingleLiveEvent = SingleLiveEvent() - val startCreateSiteService: LiveData = _startCreateSiteService - - private val _onHelpClicked = SingleLiveEvent() - val onHelpClicked: LiveData = _onHelpClicked - - private val _onCancelWizardClicked = SingleLiveEvent() - val onCancelWizardClicked: LiveData = _onCancelWizardClicked - - private val _onOkButtonClicked = SingleLiveEvent() - val onOkButtonClicked: LiveData = _onOkButtonClicked - - private val _onSiteCreationCompleted = SingleLiveEvent() - val onSiteCreationCompleted: LiveData = _onSiteCreationCompleted - init { dispatcher.register(fetchWpComSiteUseCase) } @@ -121,167 +55,67 @@ class SitePreviewViewModel @Inject constructor( super.onCleared() dispatcher.unregister(fetchWpComSiteUseCase) job.cancel() - loadingAnimationJob?.cancel() } - fun writeToBundle(outState: Bundle) { - outState.putParcelable(KEY_CREATE_SITE_STATE, createSiteState) - } + private val job = Job() + override val coroutineContext: CoroutineContext + get() = bgDispatcher + job + private var isStarted = false + private var webviewFullyLoadedTracked = false - fun start(siteCreationState: SiteCreationState, savedState: Bundle?) { - if (isStarted) { - return - } - isStarted = true - this.siteCreationState = siteCreationState - urlWithoutScheme = siteCreationState.domain - siteTitle = siteCreationState.siteName + private var siteDesign: String? = null + private var isFree: Boolean = true - val restoredState = savedState?.getParcelable(KEY_CREATE_SITE_STATE) + private lateinit var result: Created + private lateinit var domainName: String - init(restoredState ?: SiteNotCreated) - } + private val _uiState: MutableLiveData = MutableLiveData() + val uiState: LiveData = _uiState - private fun init(state: CreateSiteState) { - createSiteState = state - when (state) { - SiteNotCreated -> { - showFullscreenProgress() - startCreateSiteService() - } - is SiteNotInLocalDb -> { - showFullscreenProgress() - startPreLoadingWebView() - fetchNewlyCreatedSiteModel(state.remoteSiteId) - } - is SiteCreationCompleted -> { - startPreLoadingWebView(skipDelay = true) - } - } - } + private val _preloadPreview: MutableLiveData = MutableLiveData() + val preloadPreview: LiveData = _preloadPreview - private fun startCreateSiteService(previousState: SiteCreationServiceState? = null) { - if (networkUtils.isNetworkAvailable()) { - siteCreationState.apply { - // A non-null [segmentId] may invalidate the [siteDesign] selection - // https://github.com/wordpress-mobile/WordPress-Android/issues/13749 - val segmentIdentifier = if (siteDesign != null) null else segmentId - val serviceData = SiteCreationServiceData( - segmentIdentifier, - siteDesign, - urlWithoutScheme, - siteTitle - ) - _startCreateSiteService.value = SitePreviewStartServiceData(serviceData, previousState) + private val _onOkButtonClicked = SingleLiveEvent() + val onOkButtonClicked: LiveData = _onOkButtonClicked + + fun start(siteCreationState: SiteCreationState) { + if (isStarted) return else isStarted = true + require(siteCreationState.result is Created) + siteDesign = siteCreationState.siteDesign + result = siteCreationState.result + isFree = requireNotNull(siteCreationState.domain).isFree + domainName = requireNotNull(siteCreationState.domain).domainName + startPreLoadingWebView() + if (result is CreatedButNotFetched) { + launch { + fetchNewlyCreatedSiteModel(result.site.siteId)?.let { + result = Completed(it) + } } - } else { - showFullscreenErrorWithDelay() } } - fun retry() { - showFullscreenProgress() - startCreateSiteService(serviceStateForRetry) - } - - fun onHelpClicked() { - _onHelpClicked.call() - } - - fun onCancelWizardClicked() { - _onCancelWizardClicked.value = createSiteState - } - fun onOkButtonClicked() { tracker.trackPreviewOkButtonTapped() - _onOkButtonClicked.value = createSiteState + _onOkButtonClicked.postValue(result) } - private fun showFullscreenErrorWithDelay() { - showFullscreenProgress() - launch(mainDispatcher) { - // We show the loading indicator for a bit so the user has some feedback when they press retry - delay(CONNECTION_ERROR_DELAY_TO_SHOW_LOADING_STATE) - tracker.trackErrorShown(ERROR_CONTEXT, INTERNET_UNAVAILABLE_ERROR) - updateUiState(SitePreviewConnectionErrorUiState) - } - } - - /** - * The service automatically shows system notifications when site creation is in progress and the app is in - * the background. We need to connect to the `AutoForeground` service from the View(Fragment), as only the View - * knows when the app is in the background. Required parameter for `ServiceEventConnection` is also - * the observer/listener of the `SiteCreationServiceState` (VM in our case), therefore we can't simply register - * to the EventBus from the ViewModel and we have to use `sticky` events instead. - */ - @Subscribe(threadMode = ThreadMode.BACKGROUND, sticky = true) - @Suppress("unused") - fun onSiteCreationServiceStateUpdated(event: SiteCreationServiceState) { - if (lastReceivedServiceState == event) return // filter out events which we've already received - lastReceivedServiceState = event - when (event.step) { - IDLE, CREATE_SITE -> { - } // do nothing - SUCCESS -> { - val remoteSiteId = (event.payload as Pair<*, *>).first as Long - urlWithoutScheme = urlUtils.removeScheme(event.payload.second as String).trimEnd('/') - createSiteState = SiteNotInLocalDb(remoteSiteId, !siteTitle.isNullOrBlank()) - startPreLoadingWebView() - fetchNewlyCreatedSiteModel(remoteSiteId) - _onSiteCreationCompleted.asyncCall() - } - FAILURE -> { - serviceStateForRetry = event.payload as SiteCreationServiceState - tracker.trackErrorShown( - ERROR_CONTEXT, - UNKNOWN, - "SiteCreation service failed" - ) - updateUiStateAsync(SitePreviewGenericErrorUiState) - } - } - } - - /** - * Fetch newly created site model - supports retry with linear backoff. - */ - private fun fetchNewlyCreatedSiteModel(remoteSiteId: Long) { + private fun startPreLoadingWebView() { + tracker.trackPreviewLoading(siteDesign) launch { - val onSiteFetched = fetchWpComSiteUseCase.fetchSiteWithRetry(remoteSiteId) - createSiteState = if (!onSiteFetched.isError) { - val siteBySiteId = requireNotNull(siteStore.getSiteBySiteId(remoteSiteId)) { - "Site successfully fetched but has not been found in the local db." - } - CreateSiteState.SiteCreationCompleted(siteBySiteId.id, !siteTitle.isNullOrBlank()) - } else { - SiteNotInLocalDb(remoteSiteId, !siteTitle.isNullOrBlank()) - } - } - } - - private fun startPreLoadingWebView(skipDelay: Boolean = false) { - tracker.trackPreviewLoading(siteCreationState.siteDesign) - launch { - if (!skipDelay) { - /** - * Keep showing the full screen loading screen for 1 more second or until the webview is loaded - * whichever happens first. This will give us some more time to fetch the newly created site. - */ - delay(DELAY_TO_SHOW_WEB_VIEW_LOADING_SHIMMER) - } /** * If the webview is still not loaded after some delay, we'll show the loading shimmer animation instead * of the full screen progress, so the user is not blocked for taking actions. */ withContext(mainDispatcher) { if (uiState.value !is SitePreviewContentUiState) { - tracker.trackPreviewWebviewShown(siteCreationState.siteDesign) - updateUiState(SitePreviewLoadingShimmerState(createSitePreviewData())) + tracker.trackPreviewWebviewShown(siteDesign) + updateUiState(SitePreviewLoadingShimmerState(isFree, createSitePreviewData())) } } } // Load the newly created site in the webview - urlWithoutScheme?.let { url -> + result.site.url?.let { url -> val urlToLoad = urlUtils.addUrlSchemeIfNeeded( url = url, addHttps = isWordPressComSubDomain(url) @@ -291,36 +125,47 @@ class SitePreviewViewModel @Inject constructor( } } + /** + * Fetch newly created site model - supports retry with linear backoff. + */ + private suspend fun fetchNewlyCreatedSiteModel(remoteSiteId: Long): SiteModel? { + val onSiteFetched = fetchWpComSiteUseCase.fetchSiteWithRetry(remoteSiteId) + return if (!onSiteFetched.isError) { + return requireNotNull(siteStore.getSiteBySiteId(remoteSiteId)) { + "Site successfully fetched but has not been found in the local db." + } + } else { + null + } + } + fun onUrlLoaded() { if (!webviewFullyLoadedTracked) { webviewFullyLoadedTracked = true - tracker.trackPreviewWebviewFullyLoaded(siteCreationState.siteDesign) + tracker.trackPreviewWebviewFullyLoaded(siteDesign) } /** * Update the ui state if the loading or error screen is being shown. * In other words don't update it after a configuration change. */ if (uiState.value !is SitePreviewContentUiState) { - updateUiState(SitePreviewContentUiState(createSitePreviewData())) + updateUiState(SitePreviewContentUiState(isFree, createSitePreviewData())) } } fun onWebViewError() { if (uiState.value !is SitePreviewWebErrorUiState) { - updateUiState(SitePreviewWebErrorUiState(createSitePreviewData())) + updateUiState(SitePreviewWebErrorUiState(isFree, createSitePreviewData())) } } - private fun createSitePreviewData(): SitePreviewData { - val url = urlWithoutScheme ?: "" + private fun createSitePreviewData(): UrlData { + val url = domainName val subDomain = urlUtils.extractSubDomain(url) val fullUrl = urlUtils.addUrlSchemeIfNeeded(url, true) - val subDomainIndices: Pair = Pair(0, subDomain.length) - val domainIndices: Pair = Pair( - Math.min(subDomainIndices.second, url.length), - url.length - ) - return SitePreviewData( + val subDomainIndices = 0 to subDomain.length + val domainIndices = subDomainIndices.second.coerceAtMost(url.length) to url.length + return UrlData( fullUrl, url, subDomainIndices, @@ -328,118 +173,72 @@ class SitePreviewViewModel @Inject constructor( ) } - private fun showFullscreenProgress() { - loadingAnimationJob?.cancel() - loadingAnimationJob = launch(mainDispatcher) { - var i = 0 - val listSize = loadingTexts.size - while (isActive) { - updateUiState( - SitePreviewFullscreenProgressUiState( - animate = i != 0, // the first text should appear without an animation - loadingTextResId = loadingTexts[i++ % listSize] - ) - ) - delay(LOADING_STATE_TEXT_ANIMATION_DELAY) - } - } - } - private fun updateUiState(uiState: SitePreviewUiState) { - if (uiState !is SitePreviewFullscreenProgressUiState) { - loadingAnimationJob?.cancel() - } _uiState.value = uiState } - private fun updateUiStateAsync(uiState: SitePreviewUiState) { - if (uiState !is SitePreviewFullscreenProgressUiState) { - loadingAnimationJob?.cancel() - } - _uiState.postValue(uiState) - } - sealed class SitePreviewUiState( - val fullscreenProgressLayoutVisibility: Boolean = false, - val contentLayoutVisibility: Boolean = false, + open val urlData: UrlData, val webViewVisibility: Boolean = false, val webViewErrorVisibility: Boolean = false, val shimmerVisibility: Boolean = false, - val fullscreenErrorLayoutVisibility: Boolean = false + val subtitle: UiString, + val caption: UiString?, ) { - data class SitePreviewContentUiState(val data: SitePreviewData) : SitePreviewUiState( - contentLayoutVisibility = true, + data class SitePreviewContentUiState( + val isFree: Boolean, + override val urlData: UrlData, + ) : SitePreviewUiState( + urlData = urlData, webViewVisibility = true, - webViewErrorVisibility = false + webViewErrorVisibility = false, + subtitle = getSubtitle(isFree), + caption = getCaption(isFree), ) - data class SitePreviewWebErrorUiState(val data: SitePreviewData) : SitePreviewUiState( - contentLayoutVisibility = true, + data class SitePreviewWebErrorUiState( + val isFree: Boolean, + override val urlData: UrlData, + ) : SitePreviewUiState( + urlData = urlData, webViewVisibility = false, - webViewErrorVisibility = true + webViewErrorVisibility = true, + subtitle = getSubtitle(isFree), + caption = getCaption(isFree), ) - data class SitePreviewLoadingShimmerState(val data: SitePreviewData) : SitePreviewUiState( - contentLayoutVisibility = true, - shimmerVisibility = true + data class SitePreviewLoadingShimmerState( + val isFree: Boolean, + override val urlData: UrlData, + ) : SitePreviewUiState( + urlData = urlData, + shimmerVisibility = true, + subtitle = getSubtitle(isFree), + caption = getCaption(isFree), ) - data class SitePreviewFullscreenProgressUiState(val loadingTextResId: UiString, val animate: Boolean) : - SitePreviewUiState(fullscreenProgressLayoutVisibility = true) - - sealed class SitePreviewFullscreenErrorUiState constructor( - val titleResId: Int, - val subtitleResId: Int? = null, - val showContactSupport: Boolean = false, - val showCancelWizardButton: Boolean = true - ) : SitePreviewUiState( - fullscreenErrorLayoutVisibility = true - ) { - object SitePreviewGenericErrorUiState : - SitePreviewFullscreenErrorUiState( - R.string.site_creation_error_generic_title, - R.string.site_creation_error_generic_subtitle, - showContactSupport = true - ) + companion object { + private fun getSubtitle(isFree: Boolean): UiString { + return if (isFree) { + UiStringRes(R.string.new_site_creation_preview_subtitle) + } else { + UiStringResWithParams( + R.string.new_site_creation_preview_subtitle_paid, + UiStringRes(R.string.new_site_creation_preview_subtitle), + ) + } + } - object SitePreviewConnectionErrorUiState : SitePreviewFullscreenErrorUiState( - R.string.no_network_message - ) + private fun getCaption(isFree: Boolean): UiStringRes? { + return UiStringRes(R.string.new_site_creation_preview_caption_paid).takeIf { !isFree } + } } - } - - data class SitePreviewData( - val fullUrl: String, - val shortUrl: String, - val domainIndices: Pair, - val subDomainIndices: Pair - ) - - data class SitePreviewStartServiceData( - val serviceData: SiteCreationServiceData, - val previousState: SiteCreationServiceState? - ) - - @SuppressLint("ParcelCreator") - sealed class CreateSiteState : Parcelable { - /** - * CreateSite request haven't finished yet or failed. - */ - @Parcelize - object SiteNotCreated : CreateSiteState() - /** - * FetchSite request haven't finished yet or failed. - * Since we fetch the site without user awareness in background, the user may potentially leave the screen - * before the request is finished. - */ - @Parcelize - data class SiteNotInLocalDb(val remoteSiteId: Long, val isSiteTitleTaskComplete: Boolean) : CreateSiteState() - - /** - * The site has been successfully created and stored into local db. - */ - @Parcelize - data class SiteCreationCompleted(val localSiteId: Int, val isSiteTitleTaskComplete: Boolean) : CreateSiteState() + data class UrlData( + val fullUrl: String, + val shortUrl: String, + val domainIndices: Pair, + val subDomainIndices: Pair + ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/progress/SiteCreationProgressFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/progress/SiteCreationProgressFragment.kt new file mode 100644 index 000000000000..b898b4c35dba --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/progress/SiteCreationProgressFragment.kt @@ -0,0 +1,193 @@ +package org.wordpress.android.ui.sitecreation.progress + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.content.Context +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.R +import org.wordpress.android.databinding.FullscreenErrorWithRetryBinding +import org.wordpress.android.databinding.SiteCreationProgressCreatingSiteBinding +import org.wordpress.android.databinding.SiteCreationProgressScreenBinding +import org.wordpress.android.ui.accounts.HelpActivity +import org.wordpress.android.ui.sitecreation.SiteCreationActivity.Companion.ARG_STATE +import org.wordpress.android.ui.sitecreation.SiteCreationState +import org.wordpress.android.ui.sitecreation.misc.OnHelpClickedListener +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressViewModel.SiteProgressUiState.Error +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressViewModel.SiteProgressUiState.Loading +import org.wordpress.android.ui.sitecreation.services.SiteCreationService +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.AniUtils +import org.wordpress.android.util.AppLog +import org.wordpress.android.util.AutoForeground.ServiceEventConnection +import org.wordpress.android.util.extensions.getParcelableCompat +import javax.inject.Inject + +@AndroidEntryPoint +class SiteCreationProgressFragment : Fragment(R.layout.site_creation_progress_screen) { + @Inject + internal lateinit var uiHelpers: UiHelpers + + /** + * We need to connect to the service, so the service knows when the app is in the background. The service + * automatically shows system notifications when site creation is in progress and the app is in the background. + */ + private var serviceEventConnection: ServiceEventConnection? = null + private var animatorSet: AnimatorSet? = null + + private lateinit var binding: SiteCreationProgressScreenBinding + private val viewModel: SiteCreationProgressViewModel by activityViewModels() + + override fun onAttach(context: Context) { + super.onAttach(context) + check(context is OnHelpClickedListener) { "Parent activity must implement OnHelpClickedListener." } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState == null) { + // we need to manually clear the service state to avoid sticky events from the previous SiteCreation flow. + SiteCreationService.clearSiteCreationServiceState() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding = SiteCreationProgressScreenBinding.bind(view).apply { + observeState() + observeHelpClicks(requireActivity() as OnHelpClickedListener) + observeSiteCreationService() + fullscreenErrorWithRetry.setOnClickListeners() + } + + (requireActivity() as AppCompatActivity).supportActionBar?.hide() + + viewModel.start(requireNotNull(requireArguments().getParcelableCompat(ARG_STATE))) + } + + private fun SiteCreationProgressScreenBinding.observeState() { + viewModel.uiState.observe(viewLifecycleOwner) { uiState -> + uiState?.run { + when (val ui = this@run) { + is Loading -> siteCreationProgressCreatingSite.updateLoadingLayout(ui) + is Error -> fullscreenErrorWithRetry.updateErrorLayout(ui) + } + siteCreationProgressCreatingSite.progressLayout.isVisible = progressLayoutVisibility + fullscreenErrorWithRetry.errorLayout.isVisible = errorLayoutVisibility + } + } + } + + private fun observeSiteCreationService() { + viewModel.startCreateSiteService.observe(viewLifecycleOwner) { startServiceData -> + startServiceData?.let { + SiteCreationService.createSite(requireNotNull(activity), it.previousState, it.serviceData) + } + } + viewModel.onFreeSiteCreated.observe(viewLifecycleOwner) { + view?.announceForAccessibility(getString(R.string.new_site_creation_preview_title)) + } + } + + private fun observeHelpClicks(listener: OnHelpClickedListener) { + viewModel.onHelpClicked.observe(viewLifecycleOwner) { + listener.onHelpClicked(HelpActivity.Origin.SITE_CREATION_CREATING) + } + } + + private fun FullscreenErrorWithRetryBinding.setOnClickListeners() { + errorRetry.setOnClickListener { viewModel.retry() } + cancelWizardButton.setOnClickListener { viewModel.onCancelWizardClicked() } + contactSupport.setOnClickListener { viewModel.onHelpClicked() } + } + + private fun FullscreenErrorWithRetryBinding.updateErrorLayout(errorUiState: Error) { + errorUiState.run { + uiHelpers.setTextOrHide(errorTitle, titleResId) + uiHelpers.setTextOrHide(errorSubtitle, subtitleResId) + uiHelpers.updateVisibility(contactSupport, errorUiState.showContactSupport) + uiHelpers.updateVisibility(cancelWizardButton, errorUiState.showCancelWizardButton) + } + } + + private fun SiteCreationProgressCreatingSiteBinding.updateLoadingLayout( + progressUiState: Loading + ) { + progressUiState.apply { + val newText = uiHelpers.getTextOfUiString(progressText.context, loadingTextResId) + AppLog.d(AppLog.T.MAIN, "Changing text - animation: $animate") + if (animate) { + updateLoadingTextWithFadeAnimation(newText) + } else { + progressText.text = newText + } + } + } + + private fun SiteCreationProgressCreatingSiteBinding.updateLoadingTextWithFadeAnimation(newText: CharSequence) { + val animationDuration = AniUtils.Duration.SHORT + val fadeOut = AniUtils.getFadeOutAnim( + progressTextLayout, + animationDuration, + View.VISIBLE + ) + val fadeIn = AniUtils.getFadeInAnim( + progressTextLayout, + animationDuration + ) + + // update the text when the view isn't visible + fadeIn.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + progressText.text = newText + } + + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + animatorSet = null + } + }) + // Start the fade-in animation right after the view fades out + fadeIn.startDelay = animationDuration.toMillis(progressTextLayout.context) + + animatorSet = AnimatorSet().apply { + playSequentially(fadeOut, fadeIn) + start() + } + } + + override fun onResume() { + super.onResume() + serviceEventConnection = ServiceEventConnection(context, SiteCreationService::class.java, viewModel) + } + + override fun onPause() { + super.onPause() + serviceEventConnection?.disconnect(context, viewModel) + } + + override fun onStop() { + super.onStop() + if (animatorSet?.isRunning == true) { + animatorSet?.cancel() + } + } + + companion object { + const val TAG = "site_creation_progress_fragment_tag" + + fun newInstance(siteCreationState: SiteCreationState) = SiteCreationProgressFragment() + .apply { + arguments = Bundle().apply { + putParcelable(ARG_STATE, siteCreationState) + } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/progress/SiteCreationProgressViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/progress/SiteCreationProgressViewModel.kt new file mode 100644 index 000000000000..879b278d262b --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/progress/SiteCreationProgressViewModel.kt @@ -0,0 +1,261 @@ +package org.wordpress.android.ui.sitecreation.progress + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.R +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.modules.UI_THREAD +import org.wordpress.android.ui.domains.DomainRegistrationCheckoutWebViewActivity.OpenCheckout.CheckoutDetails +import org.wordpress.android.ui.domains.usecases.CreateCartUseCase +import org.wordpress.android.ui.sitecreation.SiteCreationResult.CreatedButNotFetched +import org.wordpress.android.ui.sitecreation.SiteCreationState +import org.wordpress.android.ui.sitecreation.domains.DomainModel +import org.wordpress.android.ui.sitecreation.misc.SiteCreationErrorType.INTERNET_UNAVAILABLE_ERROR +import org.wordpress.android.ui.sitecreation.misc.SiteCreationErrorType.UNKNOWN +import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressViewModel.SiteProgressUiState.Error.ConnectionError +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressViewModel.SiteProgressUiState.Error.GenericError +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressViewModel.SiteProgressUiState.Loading +import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceData +import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState +import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.CREATE_SITE +import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.FAILURE +import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.IDLE +import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.SUCCESS +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.NetworkUtilsWrapper +import org.wordpress.android.viewmodel.ScopedViewModel +import org.wordpress.android.viewmodel.SingleLiveEvent +import javax.inject.Inject +import javax.inject.Named + +private const val CONNECTION_ERROR_DELAY_TO_SHOW_LOADING_STATE = 1000L +const val LOADING_STATE_TEXT_ANIMATION_DELAY = 2000L +private const val ERROR_CONTEXT = "site_preview" +private val LOG_TAG = AppLog.T.SITE_CREATION + +private val loadingTexts = listOf( + UiStringRes(R.string.new_site_creation_creating_site_loading_1), + UiStringRes(R.string.new_site_creation_creating_site_loading_2), + UiStringRes(R.string.new_site_creation_creating_site_loading_3), + UiStringRes(R.string.new_site_creation_creating_site_loading_4) +) + +@HiltViewModel +class SiteCreationProgressViewModel @Inject constructor( + private val networkUtils: NetworkUtilsWrapper, + private val tracker: SiteCreationTracker, + private val createCartUseCase: CreateCartUseCase, + @Named(UI_THREAD) mainDispatcher: CoroutineDispatcher, +) : ScopedViewModel(mainDispatcher) { + private var loadingAnimationJob: Job? = null + + private lateinit var siteCreationState: SiteCreationState + private lateinit var domain: DomainModel + + private var lastReceivedServiceState: SiteCreationServiceState? = null + private var serviceStateForRetry: SiteCreationServiceState? = null + + private val _uiState: MutableLiveData = MutableLiveData() + val uiState: LiveData = _uiState + + private val _startCreateSiteService: SingleLiveEvent = SingleLiveEvent() + val startCreateSiteService: LiveData = _startCreateSiteService + + private val _onHelpClicked = SingleLiveEvent() + val onHelpClicked: LiveData = _onHelpClicked + + private val _onCancelWizardClicked = SingleLiveEvent() + val onCancelWizardClicked: LiveData = _onCancelWizardClicked + + private val _onFreeSiteCreated = SingleLiveEvent() + val onFreeSiteCreated: LiveData = _onFreeSiteCreated + + private val _onCartCreated = SingleLiveEvent() + val onCartCreated: LiveData = _onCartCreated + + override fun onCleared() { + super.onCleared() + loadingAnimationJob?.cancel() + createCartUseCase.clear() + } + + fun start(siteCreationState: SiteCreationState) { + if (siteCreationState.result is CreatedButNotFetched.InCart) { + // reuse the previously blog when returning with the same domain + if (siteCreationState.domain == this.siteCreationState.domain) { + createCart(siteCreationState.result.site) + return + } + } + this.siteCreationState = siteCreationState + domain = requireNotNull(siteCreationState.domain) { "domain required to create a site" } + + runLoadingAnimationUi() + startCreateSiteService() + } + + private fun startCreateSiteService(previousState: SiteCreationServiceState? = null) { + if (networkUtils.isNetworkAvailable()) { + siteCreationState.let { state -> + // A non-null [segmentId] may invalidate the [siteDesign] selection + // https://github.com/wordpress-mobile/WordPress-Android/issues/13749 + val segmentIdentifier = state.segmentId.takeIf { state.siteDesign != null } + val serviceData = SiteCreationServiceData( + segmentIdentifier, + state.siteDesign, + domain.domainName, + state.siteName, + domain.isFree, + ) + _startCreateSiteService.value = StartServiceData(serviceData, previousState) + } + } else { + showFullscreenErrorWithDelay() + } + } + + fun retry() { + runLoadingAnimationUi() + startCreateSiteService(serviceStateForRetry) + } + + fun onHelpClicked() = _onHelpClicked.call() + + fun onCancelWizardClicked() = _onCancelWizardClicked.call() + + private fun showFullscreenErrorWithDelay() { + runLoadingAnimationUi() + launch { + // We show the loading indicator for a bit so the user has some feedback when they press retry + delay(CONNECTION_ERROR_DELAY_TO_SHOW_LOADING_STATE) + tracker.trackErrorShown(ERROR_CONTEXT, INTERNET_UNAVAILABLE_ERROR) + updateUiState(ConnectionError) + } + } + + /** + * The service automatically shows system notifications when site creation is in progress and the app is in + * the background. We need to connect to the `AutoForeground` service from the View(Fragment), as only the View + * knows when the app is in the background. Required parameter for `ServiceEventConnection` is also + * the observer/listener of the `SiteCreationServiceState` (VM in our case), therefore we can't simply register + * to the EventBus from the ViewModel and we have to use `sticky` events instead. + */ + @Subscribe(threadMode = ThreadMode.BACKGROUND, sticky = true) + @Suppress("unused") + fun onSiteCreationServiceStateUpdated(event: SiteCreationServiceState) { + if (lastReceivedServiceState == event) return // filter out events which we've already received + lastReceivedServiceState = event + when (event.step) { + IDLE, CREATE_SITE -> Unit + SUCCESS -> { + val site = mapPayloadToSiteModel(event.payload) + if (domain.isFree) { + _onFreeSiteCreated.postValue(site) + } else { + createCart(site) + } + } + FAILURE -> { + serviceStateForRetry = event.payload as SiteCreationServiceState + tracker.trackErrorShown( + ERROR_CONTEXT, + UNKNOWN, + "SiteCreation service failed" + ) + updateUiStateAsync(GenericError) + } + } + } + + private fun mapPayloadToSiteModel(payload: Any?): SiteModel { + require(payload is Pair<*, *>) { "Expected Pair in Payload, got: $payload" } + val (blogId, blogUrl) = payload + require(blogId is Long) { "Expected the 1st element in the Payload Pair to be a Long, got: $blogId" } + require(blogUrl is String) { "Expected the 2nd element in the Payload Pair to be a Long, got: $blogUrl" } + return SiteModel().apply { siteId = blogId; url = blogUrl } + } + + private fun createCart(site: SiteModel) = launch { + AppLog.d(LOG_TAG, "Creating cart: $domain") + + val event = createCartUseCase.execute( + site, + domain.productId, + domain.domainName, + domain.supportsPrivacy, + false, + ) + + if (event.isError) { + AppLog.e(LOG_TAG, "Failed cart creation: ${event.error.message}") + updateUiStateAsync(GenericError) + } else { + AppLog.d(LOG_TAG, "Successful cart creation: ${event.cartDetails}") + _onCartCreated.postValue(CheckoutDetails(site, domain.domainName)) + } + } + + private fun runLoadingAnimationUi() { + loadingAnimationJob?.cancel() + loadingAnimationJob = launch { + loadingTexts.forEachIndexed { i, uiString -> + updateUiState( + Loading( + animate = i != 0, // the first text should appear without an animation + loadingTextResId = uiString + ) + ) + delay(LOADING_STATE_TEXT_ANIMATION_DELAY) + } + } + } + + private fun updateUiState(uiState: SiteProgressUiState) { + if (uiState !is Loading) loadingAnimationJob?.cancel() + _uiState.value = uiState + } + + private fun updateUiStateAsync(uiState: SiteProgressUiState) { + if (uiState !is Loading) loadingAnimationJob?.cancel() + _uiState.postValue(uiState) + } + + sealed class SiteProgressUiState( + val progressLayoutVisibility: Boolean = false, + val errorLayoutVisibility: Boolean = false + ) { + data class Loading(val loadingTextResId: UiString, val animate: Boolean) : + SiteProgressUiState(progressLayoutVisibility = true) + + sealed class Error constructor( + val titleResId: Int, + val subtitleResId: Int? = null, + val showContactSupport: Boolean = false, + val showCancelWizardButton: Boolean = true + ) : SiteProgressUiState(errorLayoutVisibility = true) { + object GenericError : Error( + R.string.site_creation_error_generic_title, + R.string.site_creation_error_generic_subtitle, + showContactSupport = true + ) + + object ConnectionError : Error( + R.string.no_network_message + ) + } + } + + data class StartServiceData( + val serviceData: SiteCreationServiceData, + val previousState: SiteCreationServiceState? + ) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationService.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationService.kt index 958c79f64157..3e6556dc90d0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationService.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationService.kt @@ -17,6 +17,7 @@ import org.wordpress.android.util.AppLog.T import org.wordpress.android.util.AutoForeground import org.wordpress.android.util.LocaleManager import org.wordpress.android.util.LocaleManagerWrapper +import org.wordpress.android.util.extensions.getParcelableExtraCompat import javax.inject.Inject private val INITIAL_STATE = IDLE @@ -47,7 +48,7 @@ class SiteCreationService : AutoForeground(SiteCreatio return Service.START_NOT_STICKY } - val data = intent.getParcelableExtra(ARG_DATA)!! + val data = requireNotNull(intent.getParcelableExtraCompat(ARG_DATA)) manager.onStart( LocaleManager.getLanguageWordPressId(this), localeManagerWrapper.getTimeZone().id, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceData.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceData.kt index 14f32fa688e5..7df837d1e770 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceData.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceData.kt @@ -10,5 +10,6 @@ data class SiteCreationServiceData( val segmentId: Long?, val siteDesign: String?, val domain: String?, - val title: String? + val title: String?, + val isFree: Boolean, ) : Parcelable 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 799013410b11..86a62720f401 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 @@ -105,7 +105,7 @@ class SiteCreationServiceManager @Inject constructor( launch { AppLog.i( T.SITE_CREATION, - "Dispatching Create Site Action, SiteName: ${siteData.domain}" + "Dispatching Create Site Action, SiteName: ${siteData.domain}, isFree: ${siteData.isFree}" ) val createSiteEvent: OnNewSiteCreated try { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/usecases/CreateSiteUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/usecases/CreateSiteUseCase.kt index 5ad93e6f89a0..1e190e2a4c5e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/usecases/CreateSiteUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/sitecreation/usecases/CreateSiteUseCase.kt @@ -60,7 +60,8 @@ class CreateSiteUseCase @Inject constructor( siteVisibility, siteData.segmentId, siteData.siteDesign, - dryRun + dryRun, + findAvailableUrl = if (siteData.isFree) null else true ) continuation = cont dispatcher.dispatch(SiteActionBuilder.newCreateNewSiteAction(newSitePayload)) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConnectJetpackActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConnectJetpackActivity.kt index 4a41baa16a05..e63d0018f31a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConnectJetpackActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/StatsConnectJetpackActivity.kt @@ -22,6 +22,7 @@ import org.wordpress.android.ui.WPWebViewActivity import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T.API import org.wordpress.android.util.WPUrlUtils +import org.wordpress.android.util.extensions.getSerializableExtraCompat import javax.inject.Inject /** @@ -72,14 +73,18 @@ class StatsConnectJetpackActivity : LocaleAwareActivity() { if (TextUtils.isEmpty(mAccountStore.account.userName)) { mDispatcher.dispatch(AccountActionBuilder.newFetchAccountAction()) } else { - startJetpackConnectionFlow(intent.getSerializableExtra(WordPress.SITE) as SiteModel) + startJetpackConnectionFlow( + requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE)) + ) } } } private fun StatsJetpackConnectionActivityBinding.initViews() { jetpackSetup.setOnClickListener { - startJetpackConnectionFlow(intent.getSerializableExtra(WordPress.SITE) as SiteModel) + startJetpackConnectionFlow( + requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE)) + ) } jetpackFaq.setOnClickListener { WPWebViewActivity.openURL(this@StatsConnectJetpackActivity, FAQ_URL) @@ -133,7 +138,7 @@ class StatsConnectJetpackActivity : LocaleAwareActivity() { event.causeOfChange == FETCH_ACCOUNT && !TextUtils.isEmpty(mAccountStore.account.userName) ) { - startJetpackConnectionFlow(intent.getSerializableExtra(WordPress.SITE) as SiteModel) + startJetpackConnectionFlow(requireNotNull(intent.getSerializableExtraCompat(WordPress.SITE))) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsActivity.kt index 25ab3eaed2ee..e3d4b2be874a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsActivity.kt @@ -11,7 +11,9 @@ import org.wordpress.android.databinding.StatsListActivityBinding import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.push.NotificationType import org.wordpress.android.push.NotificationsProcessingService.ARG_NOTIFICATION_TYPE +import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.LocaleAwareActivity +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper import org.wordpress.android.ui.stats.StatsTimeframe import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider import org.wordpress.android.util.JetpackBrandingUtils @@ -24,17 +26,24 @@ class StatsActivity : LocaleAwareActivity() { @Inject lateinit var jetpackBrandingUtils: JetpackBrandingUtils + + @Inject + lateinit var jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper private val viewModel: StatsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - setContentView(StatsListActivityBinding.inflate(layoutInflater).root) + if (jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { + ActivityLauncher.showJetpackStaticPoster(this) + finish() + } else { + setContentView(StatsListActivityBinding.inflate(layoutInflater).root) + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllActivity.kt index ee070223d926..8beb4179011c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllActivity.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.WordPress import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.analytics.AnalyticsTracker.Stat @@ -13,6 +14,7 @@ import org.wordpress.android.ui.LocaleAwareActivity import org.wordpress.android.ui.stats.StatsViewType import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider.SelectedDate +@AndroidEntryPoint class StatsViewAllActivity : LocaleAwareActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -23,7 +25,7 @@ class StatsViewAllActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt index 016d5fe6d934..705374bf19b4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewAllFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.os.Parcelable import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager @@ -12,7 +13,7 @@ import com.google.android.material.appbar.AppBarLayout.LayoutParams import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import com.google.android.material.tabs.TabLayout.Tab -import dagger.android.support.DaggerFragment +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.databinding.StatsViewAllFragmentBinding @@ -34,13 +35,18 @@ import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider import org.wordpress.android.ui.stats.refresh.utils.drawDateSelector import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.WPSwipeToRefreshHelper +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.getParcelableExtraCompat +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.util.helpers.SwipeToRefreshHelper import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.observeEvent import org.wordpress.android.widgets.WPSnackbar import javax.inject.Inject -class StatsViewAllFragment : DaggerFragment(R.layout.stats_view_all_fragment) { +@AndroidEntryPoint +class StatsViewAllFragment : Fragment(R.layout.stats_view_all_fragment) { @Inject lateinit var viewModelFactoryBuilder: StatsViewAllViewModelFactory.Builder @@ -78,13 +84,13 @@ class StatsViewAllFragment : DaggerFragment(R.layout.stats_view_all_fragment) { if (intent.hasExtra(ARGS_VIEW_TYPE)) { outState.putSerializable( ARGS_VIEW_TYPE, - intent.getSerializableExtra(ARGS_VIEW_TYPE) + intent.getSerializableExtraCompat(ARGS_VIEW_TYPE) ) } if (intent.hasExtra(ARGS_TIMEFRAME)) { outState.putSerializable( ARGS_TIMEFRAME, - intent.getSerializableExtra(ARGS_TIMEFRAME) + intent.getSerializableExtraCompat(ARGS_TIMEFRAME) ) } outState.putInt(WordPress.LOCAL_SITE_ID, intent.getIntExtra(WordPress.LOCAL_SITE_ID, 0)) @@ -96,7 +102,7 @@ class StatsViewAllFragment : DaggerFragment(R.layout.stats_view_all_fragment) { private fun StatsViewAllFragmentBinding.initializeViews(savedInstanceState: Bundle?) { val layoutManager = LinearLayoutManager(activity, RecyclerView.VERTICAL, false) - savedInstanceState?.getParcelable(listStateKey)?.let { + savedInstanceState?.getParcelableCompat(listStateKey)?.let { layoutManager.onRestoreInstanceState(it) } with(statsListFragment) { @@ -153,16 +159,18 @@ class StatsViewAllFragment : DaggerFragment(R.layout.stats_view_all_fragment) { savedInstanceState: Bundle? ) { val nonNullIntent = checkNotNull(activity.intent) - val type = if (savedInstanceState == null) { - nonNullIntent.getSerializableExtra(ARGS_VIEW_TYPE) as StatsViewType - } else { - savedInstanceState.getSerializable(ARGS_VIEW_TYPE) as StatsViewType - } + val type = requireNotNull( + if (savedInstanceState == null) { + nonNullIntent.getSerializableExtraCompat(ARGS_VIEW_TYPE) + } else { + savedInstanceState.getSerializableCompat(ARGS_VIEW_TYPE) + } + ) - val granularity = if (savedInstanceState == null) { - nonNullIntent.getSerializableExtra(ARGS_TIMEFRAME) as StatsGranularity? + val granularity: StatsGranularity? = if (savedInstanceState == null) { + nonNullIntent.getSerializableExtraCompat(ARGS_TIMEFRAME) } else { - savedInstanceState.getSerializable(ARGS_TIMEFRAME) as StatsGranularity? + savedInstanceState.getSerializableCompat(ARGS_TIMEFRAME) } val siteId = savedInstanceState?.getInt(WordPress.LOCAL_SITE_ID, 0) @@ -172,10 +180,10 @@ class StatsViewAllFragment : DaggerFragment(R.layout.stats_view_all_fragment) { val viewModelFactory = viewModelFactoryBuilder.build(type, granularity) viewModel = ViewModelProvider(activity, viewModelFactory).get(StatsViewAllViewModel::class.java) - val selectedDate = if (savedInstanceState == null) { - nonNullIntent.getParcelableExtra(ARGS_SELECTED_DATE) as SelectedDate? + val selectedDate: SelectedDate? = if (savedInstanceState == null) { + nonNullIntent.getParcelableExtraCompat(ARGS_SELECTED_DATE) } else { - savedInstanceState.getParcelable(ARGS_SELECTED_DATE) as SelectedDate? + savedInstanceState.getParcelableCompat(ARGS_SELECTED_DATE) } setupObservers(activity) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewModel.kt index fb785a27f043..9ebc14e5b248 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/StatsViewModel.kt @@ -53,6 +53,7 @@ import org.wordpress.android.util.JetpackBrandingUtils import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.config.MySiteDashboardTodaysStatsCardFeatureConfig +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.util.mapNullable import org.wordpress.android.util.mergeNotNull import org.wordpress.android.viewmodel.Event @@ -114,10 +115,10 @@ class StatsViewModel fun start(intent: Intent, restart: Boolean = false) { val localSiteId = intent.getIntExtra(WordPress.LOCAL_SITE_ID, 0) - val launchedFrom = intent.getSerializableExtra(StatsActivity.ARG_LAUNCHED_FROM) + val launchedFrom = intent.getSerializableExtraCompat(StatsActivity.ARG_LAUNCHED_FROM) val initialTimeFrame = getInitialTimeFrame(intent) val initialSelectedPeriod = intent.getStringExtra(StatsActivity.INITIAL_SELECTED_PERIOD_KEY) - val notificationType = intent.getSerializableExtra(ARG_NOTIFICATION_TYPE) as? NotificationType + val notificationType = intent.getSerializableExtraCompat(ARG_NOTIFICATION_TYPE) start(localSiteId, launchedFrom, initialTimeFrame, initialSelectedPeriod, restart, notificationType) } @@ -135,7 +136,7 @@ class StatsViewModel } private fun getInitialTimeFrame(intent: Intent): StatsSection? { - return when (intent.getSerializableExtra(StatsActivity.ARG_DESIRED_TIMEFRAME)) { + return when (intent.getSerializableExtraCompat(StatsActivity.ARG_DESIRED_TIMEFRAME)) { StatsTimeframe.INSIGHTS -> StatsSection.INSIGHTS DAY -> StatsSection.DAYS WEEK -> StatsSection.WEEKS diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt index 5cec079d6b34..4d48b4e45eed 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/StatsListFragment.kt @@ -13,8 +13,8 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import androidx.recyclerview.widget.RecyclerView.LayoutManager import androidx.recyclerview.widget.StaggeredGridLayoutManager +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R -import org.wordpress.android.WordPress import org.wordpress.android.databinding.StatsListFragmentBinding import org.wordpress.android.ui.ViewPagerFragment import org.wordpress.android.ui.stats.refresh.StatsViewModel.DateSelectorUiModel @@ -27,11 +27,15 @@ import org.wordpress.android.ui.stats.refresh.lists.detail.DetailListViewModel import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter import org.wordpress.android.ui.stats.refresh.utils.StatsNavigator import org.wordpress.android.ui.stats.refresh.utils.drawDateSelector +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.util.extensions.setVisible import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject +@AndroidEntryPoint class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -68,19 +72,18 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - statsSection = arguments?.getSerializable(LIST_TYPE) as? StatsSection - ?: activity?.intent?.getSerializableExtra(LIST_TYPE) as? StatsSection + statsSection = arguments?.getSerializableCompat(LIST_TYPE) + ?: activity?.intent?.getSerializableExtraCompat(LIST_TYPE) ?: StatsSection.INSIGHTS setHasOptionsMenu(statsSection == StatsSection.INSIGHTS) - (requireActivity().application as WordPress).component().inject(this) } override fun onSaveInstanceState(outState: Bundle) { layoutManager?.let { outState.putParcelable(listStateKey, it.onSaveInstanceState()) } - (activity?.intent?.getSerializableExtra(LIST_TYPE) as? StatsSection)?.let { sectionFromIntent -> + (activity?.intent?.getSerializableExtraCompat(LIST_TYPE))?.let { sectionFromIntent -> outState.putSerializable(LIST_TYPE, sectionFromIntent) } super.onSaveInstanceState(outState) @@ -110,7 +113,7 @@ class StatsListFragment : ViewPagerFragment(R.layout.stats_list_fragment) { } else { StaggeredGridLayoutManager(columns, StaggeredGridLayoutManager.VERTICAL) } - savedInstanceState?.getParcelable(listStateKey)?.let { + savedInstanceState?.getParcelableCompat(listStateKey)?.let { layoutManager.onRestoreInstanceState(it) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/InsightsDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/InsightsDetailFragment.kt index 6a360420730e..45f709eef9d6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/InsightsDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/InsightsDetailFragment.kt @@ -13,7 +13,9 @@ import org.wordpress.android.databinding.StatsDetailFragmentBinding import org.wordpress.android.ui.stats.refresh.lists.StatsListFragment import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection import org.wordpress.android.util.WPSwipeToRefreshHelper +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.helpers.SwipeToRefreshHelper +import java.io.Serializable @AndroidEntryPoint class InsightsDetailFragment : Fragment(R.layout.stats_detail_fragment) { @@ -29,7 +31,9 @@ class InsightsDetailFragment : Fragment(R.layout.stats_detail_fragment) { super.onViewCreated(view, savedInstanceState) val nonNullActivity = requireActivity() - val listType = nonNullActivity.intent.extras?.get(StatsListFragment.LIST_TYPE) as StatsSection + val listType = requireNotNull( + nonNullActivity.intent.extras?.getSerializableCompat(StatsListFragment.LIST_TYPE) + ) with(StatsDetailFragmentBinding.bind(view)) { with(nonNullActivity as AppCompatActivity) { setSupportActionBar(toolbar) @@ -52,7 +56,7 @@ class InsightsDetailFragment : Fragment(R.layout.stats_detail_fragment) { private fun initializeViewModels(activity: FragmentActivity) { val siteId = activity.intent?.getIntExtra(WordPress.LOCAL_SITE_ID, 0) ?: 0 - val listType = activity.intent.extras?.get(StatsListFragment.LIST_TYPE) + val listType = activity.intent.extras?.getSerializableCompat(StatsListFragment.LIST_TYPE) viewModel = when (listType) { StatsSection.INSIGHT_DETAIL -> viewsVisitorsDetailViewModel diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/StatsDetailActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/StatsDetailActivity.kt index 6dbacf1e9eeb..0391ba1e0474 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/StatsDetailActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/StatsDetailActivity.kt @@ -20,6 +20,8 @@ import org.wordpress.android.ui.stats.refresh.lists.StatsListFragment import org.wordpress.android.ui.stats.refresh.lists.StatsListViewModel.StatsSection import org.wordpress.android.ui.stats.refresh.lists.sections.granular.SelectedDateProvider.SelectedDate import org.wordpress.android.util.analytics.AnalyticsUtils +import org.wordpress.android.util.extensions.getSerializableCompat +import java.io.Serializable const val POST_ID = "POST_ID" const val POST_TYPE = "POST_TYPE" @@ -33,7 +35,7 @@ class StatsDetailActivity : LocaleAwareActivity() { val binding = StatsDetailActivityBinding.inflate(layoutInflater) setContentView(binding.root) - val listType = intent.extras?.get(StatsListFragment.LIST_TYPE) + val listType = intent.extras?.getSerializableCompat(StatsListFragment.LIST_TYPE) if (savedInstanceState == null) { supportFragmentManager.commit { @@ -51,7 +53,7 @@ class StatsDetailActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/StatsDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/StatsDetailFragment.kt index 28995aa27790..6c53b40058e7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/StatsDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/detail/StatsDetailFragment.kt @@ -3,10 +3,11 @@ package org.wordpress.android.ui.stats.refresh.lists.detail import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView -import dagger.android.support.DaggerFragment +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.databinding.StatsDetailFragmentBinding @@ -23,7 +24,8 @@ import org.wordpress.android.util.helpers.SwipeToRefreshHelper import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject -class StatsDetailFragment : DaggerFragment(R.layout.stats_detail_fragment) { +@AndroidEntryPoint +class StatsDetailFragment : Fragment(R.layout.stats_detail_fragment) { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @@ -94,9 +96,9 @@ class StatsDetailFragment : DaggerFragment(R.layout.stats_detail_fragment) { statsSiteProvider.start(siteId) val postId = activity.intent?.getLongExtra(POST_ID, 0L) - val postType = activity.intent?.getSerializableExtra(POST_TYPE) as String? - val postTitle = activity.intent?.getSerializableExtra(POST_TITLE) as String? - val postUrl = activity.intent?.getSerializableExtra(POST_URL) as String? + val postType = activity.intent?.getStringExtra(POST_TYPE) + val postTitle = activity.intent?.getStringExtra(POST_TITLE) + val postUrl = activity.intent?.getStringExtra(POST_URL) viewModel = ViewModelProvider(this, viewModelFactory) .get(StatsSection.DETAIL.name, StatsDetailViewModel::class.java) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt index f77b236a5ce8..6cc52b2e5572 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/granular/SelectedDateProvider.kt @@ -20,6 +20,8 @@ import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter import org.wordpress.android.ui.stats.refresh.utils.toStatsSection import org.wordpress.android.ui.stats.refresh.utils.trackWithSection import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.util.extensions.readListCompat +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.filter import java.util.Date import javax.inject.Inject @@ -172,7 +174,7 @@ class SelectedDateProvider fun onRestoreInstanceState(savedState: Bundle) { for (period in listOf(DAYS, WEEKS, MONTHS, YEARS)) { - val selectedDate: SelectedDate? = savedState.getParcelable(buildStateKey(period)) as SelectedDate? + val selectedDate = savedState.getParcelableCompat(buildStateKey(period)) if (selectedDate != null) { mutableDates[period] = selectedDate } @@ -223,7 +225,7 @@ class SelectedDateProvider null } val availableTimeStamps = mutableListOf() - parcel.readList(availableTimeStamps, null) + parcel.readListCompat(availableTimeStamps, null) val availableDates = availableTimeStamps.map { Date(it as Long) } val loading = parcel.readValue(null) as Boolean val error = parcel.readValue(null) as Boolean diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/management/InsightsManagementActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/management/InsightsManagementActivity.kt index fbb3d52ec2a1..46e55cc85466 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/management/InsightsManagementActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/management/InsightsManagementActivity.kt @@ -2,10 +2,12 @@ package org.wordpress.android.ui.stats.refresh.lists.sections.insights.managemen import android.os.Bundle import android.view.MenuItem +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.databinding.InsightsManagementActivityBinding import org.wordpress.android.ui.LocaleAwareActivity +@AndroidEntryPoint class InsightsManagementActivity : LocaleAwareActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -24,7 +26,7 @@ class InsightsManagementActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/management/InsightsManagementFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/management/InsightsManagementFragment.kt index 100a95759b20..13d26f30b166 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/management/InsightsManagementFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/sections/insights/management/InsightsManagementFragment.kt @@ -5,20 +5,22 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import dagger.android.support.DaggerFragment +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.databinding.InsightsManagementFragmentBinding import org.wordpress.android.ui.stats.refresh.lists.sections.insights.management.InsightsManagementViewModel.InsightListItem import javax.inject.Inject -class InsightsManagementFragment : DaggerFragment(R.layout.insights_management_fragment), MenuProvider { +@AndroidEntryPoint +class InsightsManagementFragment : Fragment(R.layout.insights_management_fragment), MenuProvider { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory private lateinit var viewModel: InsightsManagementViewModel @@ -33,11 +35,7 @@ class InsightsManagementFragment : DaggerFragment(R.layout.insights_management_f initializeViews() initializeViewModels(requireActivity(), siteId) } - activity?.onBackPressedDispatcher?.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - viewModel.onBackPressed() - } - }) + requireActivity().onBackPressedDispatcher.addCallback(this) { viewModel.onBackPressed() } } override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/StatsAllTimeWidgetConfigureActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/StatsAllTimeWidgetConfigureActivity.kt index 2d8364d7ce9a..71a7e7a57fae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/StatsAllTimeWidgetConfigureActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/alltime/StatsAllTimeWidgetConfigureActivity.kt @@ -2,9 +2,11 @@ package org.wordpress.android.ui.stats.refresh.lists.widget.alltime import android.os.Bundle import android.view.MenuItem +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.databinding.StatsAllTimeWidgetConfigureActivityBinding import org.wordpress.android.ui.LocaleAwareActivity +@AndroidEntryPoint class StatsAllTimeWidgetConfigureActivity : LocaleAwareActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -21,7 +23,7 @@ class StatsAllTimeWidgetConfigureActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetColorSelectionDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetColorSelectionDialogFragment.kt index aabe54354731..ee7af87c9ac7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetColorSelectionDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetColorSelectionDialogFragment.kt @@ -1,14 +1,13 @@ package org.wordpress.android.ui.stats.refresh.lists.widget.configuration import android.app.Dialog -import android.content.Context import android.os.Bundle import android.widget.RadioGroup import androidx.appcompat.app.AppCompatDialogFragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.android.support.AndroidSupportInjection +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsColorSelectionViewModel.Color import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsColorSelectionViewModel.Color.DARK @@ -16,6 +15,7 @@ import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsCo import org.wordpress.android.util.image.ImageManager import javax.inject.Inject +@AndroidEntryPoint class StatsWidgetColorSelectionDialogFragment : AppCompatDialogFragment() { @Inject lateinit var imageManager: ImageManager @@ -61,9 +61,4 @@ class StatsWidgetColorSelectionDialogFragment : AppCompatDialogFragment() { else -> null } } - - override fun onAttach(context: Context) { - super.onAttach(context) - AndroidSupportInjection.inject(this) - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetConfigureFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetConfigureFragment.kt index c583695ba633..5dfa47a95023 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetConfigureFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetConfigureFragment.kt @@ -10,8 +10,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import dagger.android.support.DaggerFragment +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker.Stat.STATS_WIDGET_ADDED import org.wordpress.android.databinding.StatsWidgetConfigureFragmentBinding @@ -33,7 +34,8 @@ import org.wordpress.android.util.merge import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject -class StatsWidgetConfigureFragment : DaggerFragment() { +@AndroidEntryPoint +class StatsWidgetConfigureFragment : Fragment() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetDataTypeSelectionDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetDataTypeSelectionDialogFragment.kt index a3a0c3bb096d..a99aefd49061 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetDataTypeSelectionDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetDataTypeSelectionDialogFragment.kt @@ -1,14 +1,13 @@ package org.wordpress.android.ui.stats.refresh.lists.widget.configuration import android.app.Dialog -import android.content.Context import android.os.Bundle import android.widget.RadioGroup import androidx.appcompat.app.AppCompatDialogFragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.android.support.AndroidSupportInjection +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsDataTypeSelectionViewModel.DataType import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsDataTypeSelectionViewModel.DataType.COMMENTS @@ -18,6 +17,7 @@ import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsDa import org.wordpress.android.util.image.ImageManager import javax.inject.Inject +@AndroidEntryPoint class StatsWidgetDataTypeSelectionDialogFragment : AppCompatDialogFragment() { @Inject lateinit var imageManager: ImageManager @@ -68,9 +68,4 @@ class StatsWidgetDataTypeSelectionDialogFragment : AppCompatDialogFragment() { else -> null } } - - override fun onAttach(context: Context) { - super.onAttach(context) - AndroidSupportInjection.inject(this) - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetSiteSelectionDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetSiteSelectionDialogFragment.kt index 683bd63bf31f..378ee6587d40 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetSiteSelectionDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/configuration/StatsWidgetSiteSelectionDialogFragment.kt @@ -1,7 +1,6 @@ package org.wordpress.android.ui.stats.refresh.lists.widget.configuration import android.app.Dialog -import android.content.Context import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatDialogFragment @@ -9,13 +8,14 @@ import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dagger.android.support.AndroidSupportInjection +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.databinding.StatsWidgetSiteSelectorBinding import org.wordpress.android.util.image.ImageManager import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject +@AndroidEntryPoint class StatsWidgetSiteSelectionDialogFragment : AppCompatDialogFragment() { @Inject lateinit var imageManager: ImageManager @@ -54,9 +54,4 @@ class StatsWidgetSiteSelectionDialogFragment : AppCompatDialogFragment() { viewModel.loadSites() return alertDialogBuilder.create() } - - override fun onAttach(context: Context) { - super.onAttach(context) - AndroidSupportInjection.inject(this) - } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/minified/StatsMinifiedWidgetConfigureActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/minified/StatsMinifiedWidgetConfigureActivity.kt index 4402a1769655..aec0870fb72d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/minified/StatsMinifiedWidgetConfigureActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/minified/StatsMinifiedWidgetConfigureActivity.kt @@ -2,9 +2,11 @@ package org.wordpress.android.ui.stats.refresh.lists.widget.minified import android.os.Bundle import android.view.MenuItem +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.databinding.StatsMinifiedWidgetConfigureActivityBinding import org.wordpress.android.ui.LocaleAwareActivity +@AndroidEntryPoint class StatsMinifiedWidgetConfigureActivity : LocaleAwareActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -21,7 +23,7 @@ class StatsMinifiedWidgetConfigureActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/minified/StatsMinifiedWidgetConfigureFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/minified/StatsMinifiedWidgetConfigureFragment.kt index 5e0e7cb82713..c8680aaa25cb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/minified/StatsMinifiedWidgetConfigureFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/minified/StatsMinifiedWidgetConfigureFragment.kt @@ -6,8 +6,9 @@ import android.content.Intent import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import dagger.android.support.DaggerFragment +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker.Stat.STATS_WIDGET_ADDED import org.wordpress.android.databinding.StatsWidgetConfigureFragmentBinding @@ -27,7 +28,8 @@ import org.wordpress.android.util.mergeNotNull import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject -class StatsMinifiedWidgetConfigureFragment : DaggerFragment(R.layout.stats_widget_configure_fragment) { +@AndroidEntryPoint +class StatsMinifiedWidgetConfigureFragment : Fragment(R.layout.stats_widget_configure_fragment) { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/StatsTodayWidgetConfigureActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/StatsTodayWidgetConfigureActivity.kt index 6f8cadff4fb2..0d014d4bac76 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/StatsTodayWidgetConfigureActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/today/StatsTodayWidgetConfigureActivity.kt @@ -2,9 +2,11 @@ package org.wordpress.android.ui.stats.refresh.lists.widget.today import android.os.Bundle import android.view.MenuItem +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.databinding.StatsTodayWidgetConfigureActivityBinding import org.wordpress.android.ui.LocaleAwareActivity +@AndroidEntryPoint class StatsTodayWidgetConfigureActivity : LocaleAwareActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -21,7 +23,7 @@ class StatsTodayWidgetConfigureActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/StatsViewsWidgetConfigureActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/StatsViewsWidgetConfigureActivity.kt index 057ea12aaa37..0a93c370c9ae 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/StatsViewsWidgetConfigureActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/views/StatsViewsWidgetConfigureActivity.kt @@ -2,9 +2,11 @@ package org.wordpress.android.ui.stats.refresh.lists.widget.views import android.os.Bundle import android.view.MenuItem +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.databinding.StatsViewsWidgetConfigureActivityBinding import org.wordpress.android.ui.LocaleAwareActivity +@AndroidEntryPoint class StatsViewsWidgetConfigureActivity : LocaleAwareActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -21,7 +23,7 @@ class StatsViewsWidgetConfigureActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/StatsWeekWidgetConfigureActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/StatsWeekWidgetConfigureActivity.kt index f6fe3e596baa..82f9c663d5f2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/StatsWeekWidgetConfigureActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/weeks/StatsWeekWidgetConfigureActivity.kt @@ -2,9 +2,11 @@ package org.wordpress.android.ui.stats.refresh.lists.widget.weeks import android.os.Bundle import android.view.MenuItem +import dagger.hilt.android.AndroidEntryPoint import org.wordpress.android.databinding.StatsWeekViewsWidgetConfigureActivityBinding import org.wordpress.android.ui.LocaleAwareActivity +@AndroidEntryPoint class StatsWeekWidgetConfigureActivity : LocaleAwareActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -21,7 +23,7 @@ class StatsWeekWidgetConfigureActivity : LocaleAwareActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() return true } return super.onOptionsItemSelected(item) 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 1c8d265a4170..fd6b57d8626b 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 @@ -16,6 +16,7 @@ import org.wordpress.android.ui.mysite.SiteNavigationAction.AddNewStoryWithMedia 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.util.extensions.getSerializableExtraCompat import org.wordpress.android.viewmodel.Event import javax.inject.Inject @@ -109,8 +110,10 @@ class StoriesMediaPickerResultHandler private fun isWPStoriesMediaBrowserTypeResult(data: Intent): Boolean { if (data.hasExtra(MediaBrowserActivity.ARG_BROWSER_TYPE)) { - val browserType = data.getSerializableExtra(MediaBrowserActivity.ARG_BROWSER_TYPE) - return (browserType as MediaBrowserType).isWPStoriesPicker + val browserType = requireNotNull( + data.getSerializableExtraCompat(MediaBrowserActivity.ARG_BROWSER_TYPE) + ) + return browserType.isWPStoriesPicker } return false } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stories/StoriesTrackerHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/stories/StoriesTrackerHelper.kt index a71e8bae6a26..76771fed8ad1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stories/StoriesTrackerHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stories/StoriesTrackerHelper.kt @@ -6,6 +6,7 @@ import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.util.analytics.AnalyticsUtils +import org.wordpress.android.util.extensions.getSerializableCompat import javax.inject.Inject class StoriesTrackerHelper @Inject constructor() { @@ -33,7 +34,7 @@ class StoriesTrackerHelper @Inject constructor() { val properties = getCommonProperties(event) var siteModel: SiteModel? = null event.metadata?.let { - siteModel = it.getSerializable(WordPress.SITE) as SiteModel + siteModel = it.getSerializableCompat(WordPress.SITE) } siteModel?.let { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stories/StoryComposerActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/stories/StoryComposerActivity.kt index 6e377a93d654..e4105c4014e2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stories/StoryComposerActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stories/StoryComposerActivity.kt @@ -86,6 +86,10 @@ import org.wordpress.android.util.WPMediaUtils import org.wordpress.android.util.WPPermissionUtils import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.analytics.AnalyticsUtilsWrapper +import org.wordpress.android.util.extensions.getParcelableCompat +import org.wordpress.android.util.extensions.getParcelableExtraCompat +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.extensions.getSerializableExtraCompat import org.wordpress.android.util.helpers.MediaFile import org.wordpress.android.viewmodel.observeEvent import org.wordpress.android.widgets.WPSnackbar @@ -197,10 +201,10 @@ class StoryComposerActivity : ComposeLoopFrameActivity(), } private fun initSite(savedInstanceState: Bundle?) { - if (savedInstanceState == null) { - site = intent.getSerializableExtra(WordPress.SITE) as SiteModel + site = if (savedInstanceState == null) { + intent.getSerializableExtraCompat(WordPress.SITE) } else { - site = savedInstanceState.getSerializable(WordPress.SITE) as SiteModel + savedInstanceState.getSerializableCompat(WordPress.SITE) } } @@ -211,10 +215,10 @@ class StoryComposerActivity : ComposeLoopFrameActivity(), if (savedInstanceState == null) { localPostId = getBackingPostIdFromIntent() - originalStorySaveResult = intent.getParcelableExtra(KEY_STORY_SAVE_RESULT) as StorySaveResult? + originalStorySaveResult = intent.getParcelableExtraCompat(KEY_STORY_SAVE_RESULT) if (intent.hasExtra(ARG_NOTIFICATION_TYPE)) { - notificationType = intent.getSerializableExtra(ARG_NOTIFICATION_TYPE) as NotificationType + notificationType = intent.getSerializableExtraCompat(ARG_NOTIFICATION_TYPE) } } else { if (savedInstanceState.containsKey(STATE_KEY_POST_LOCAL_ID)) { @@ -222,7 +226,7 @@ class StoryComposerActivity : ComposeLoopFrameActivity(), } if (savedInstanceState.containsKey(STATE_KEY_ORIGINAL_STORY_SAVE_RESULT)) { originalStorySaveResult = - savedInstanceState.getParcelable(STATE_KEY_ORIGINAL_STORY_SAVE_RESULT) as StorySaveResult? + savedInstanceState.getParcelableCompat(STATE_KEY_ORIGINAL_STORY_SAVE_RESULT) } } @@ -230,8 +234,7 @@ class StoryComposerActivity : ComposeLoopFrameActivity(), PostEditorAnalyticsSession.fromBundle(bundle, STATE_KEY_EDITOR_SESSION_DATA, analyticsTrackerWrapper) } - viewModel = ViewModelProvider(this, viewModelFactory) - .get(StoryComposerViewModel::class.java) + viewModel = ViewModelProvider(this, viewModelFactory)[StoryComposerViewModel::class.java] site?.let { val postInitialized = viewModel.start( @@ -370,8 +373,7 @@ class StoryComposerActivity : ComposeLoopFrameActivity(), var localPostId = intent.getIntExtra(KEY_POST_LOCAL_ID, 0) if (localPostId == 0) { if (intent.hasExtra(KEY_STORY_SAVE_RESULT)) { - val storySaveResult = - intent.getParcelableExtra(KEY_STORY_SAVE_RESULT) as StorySaveResult? + val storySaveResult = intent.getParcelableExtraCompat(KEY_STORY_SAVE_RESULT) storySaveResult?.let { localPostId = it.metadata?.getInt(KEY_POST_LOCAL_ID, 0) ?: 0 } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stories/intro/StoriesIntroDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/stories/intro/StoriesIntroDialogFragment.kt index e33508182de2..9067133bfc45 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stories/intro/StoriesIntroDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stories/intro/StoriesIntroDialogFragment.kt @@ -7,7 +7,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import org.wordpress.android.R import org.wordpress.android.WordPress @@ -15,6 +14,7 @@ import org.wordpress.android.databinding.StoriesIntroDialogFragmentBinding import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.photopicker.MediaPickerLauncher +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.extensions.setStatusBarAsSurfaceColor import javax.inject.Inject @@ -59,7 +59,7 @@ class StoriesIntroDialogFragment : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val site = requireArguments().getSerializable(WordPress.SITE) as SiteModel + val site = requireArguments().getSerializableCompat(WordPress.SITE) with(StoriesIntroDialogFragmentBinding.bind(view)) { createStoryIntroButton.setOnClickListener { viewModel.onCreateStoryButtonPressed() } storiesIntroBackButton.setOnClickListener { viewModel.onBackButtonPressed() } @@ -67,20 +67,20 @@ class StoriesIntroDialogFragment : DialogFragment() { storyImageFirst.setOnClickListener { viewModel.onStoryPreviewTapped1() } storyImageSecond.setOnClickListener { viewModel.onStoryPreviewTapped2() } } - viewModel.onCreateButtonClicked.observe(this, Observer { + viewModel.onCreateButtonClicked.observe(this) { activity?.let { mediaPickerLauncher.showStoriesPhotoPickerForResultAndTrack(it, site) } dismiss() - }) + } - viewModel.onDialogClosed.observe(this, Observer { + viewModel.onDialogClosed.observe(this) { dismiss() - }) + } - viewModel.onStoryOpenRequested.observe(this, Observer { storyUrl -> + viewModel.onStoryOpenRequested.observe(this) { storyUrl -> ActivityLauncher.openUrlExternal(context, storyUrl) - }) + } viewModel.start() } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stories/media/StoryMediaSaveUploadBridge.kt b/WordPress/src/main/java/org/wordpress/android/ui/stories/media/StoryMediaSaveUploadBridge.kt index 72c008aaddc4..5fa9fa173fd8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stories/media/StoryMediaSaveUploadBridge.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stories/media/StoryMediaSaveUploadBridge.kt @@ -36,6 +36,7 @@ import org.wordpress.android.util.EventBusWrapper import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.ToastUtils import org.wordpress.android.util.ToastUtils.Duration.LONG +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.helpers.MediaFile import javax.inject.Inject import javax.inject.Named @@ -239,7 +240,7 @@ class StoryMediaSaveUploadBridge @Inject constructor( storiesTrackerHelper.trackStorySaveResultEvent(event) event.metadata?.let { - val site = it.getSerializable(WordPress.SITE) as SiteModel + val site = requireNotNull(it.getSerializableCompat(WordPress.SITE)) val story = storyRepositoryWrapper.getStoryAtIndex(event.storyIndex) saveStoryGutenbergBlockUseCase.saveNewLocalFilesToStoriesPrefsTempSlides( site, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionActivity.kt index b39947890415..9e160c569cdb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/suggestion/SuggestionActivity.kt @@ -9,6 +9,7 @@ import android.text.Editable import android.text.TextWatcher import android.view.KeyEvent import android.view.inputmethod.EditorInfo +import androidx.activity.addCallback import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.wordpress.android.R @@ -23,6 +24,8 @@ import org.wordpress.android.ui.suggestion.adapters.SuggestionAdapter import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T import org.wordpress.android.util.ToastUtils +import org.wordpress.android.util.extensions.getSerializableExtraCompat +import org.wordpress.android.util.extensions.onBackPressedCompat import org.wordpress.android.widgets.SuggestionAutoCompleteText import javax.inject.Inject @@ -42,8 +45,13 @@ class SuggestionActivity : LocaleAwareActivity() { binding = this } - val siteModel = intent.getSerializableExtra(INTENT_KEY_SITE_MODEL) as? SiteModel - val suggestionType = intent.getSerializableExtra(INTENT_KEY_SUGGESTION_TYPE) as? SuggestionType + onBackPressedDispatcher.addCallback(this) { + viewModel.trackExit(false) + onBackPressedDispatcher.onBackPressedCompat(this) + } + + val siteModel = intent.getSerializableExtraCompat(INTENT_KEY_SITE_MODEL) + val suggestionType = intent.getSerializableExtraCompat(INTENT_KEY_SUGGESTION_TYPE) when { siteModel == null -> abortDueToMissingIntentExtra(INTENT_KEY_SITE_MODEL) suggestionType == null -> abortDueToMissingIntentExtra(INTENT_KEY_SUGGESTION_TYPE) @@ -57,11 +65,6 @@ class SuggestionActivity : LocaleAwareActivity() { finish() } - override fun onBackPressed() { - viewModel.trackExit(false) - super.onBackPressed() - } - private fun initializeActivity(siteModel: SiteModel, suggestionType: SuggestionType) { siteId = siteModel.siteId viewModel.init(suggestionType, siteModel).let { supportsSuggestions -> diff --git a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserActivity.java index 2c6f5026b560..e5429f3a0fba 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/themes/ThemeBrowserActivity.java @@ -8,6 +8,7 @@ import android.view.View.OnScrollChangeListener; import android.widget.TextView; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; @@ -47,6 +48,7 @@ import org.wordpress.android.util.JetpackBrandingUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.analytics.AnalyticsUtils; +import org.wordpress.android.util.extensions.CompatExtensionsKt; import org.wordpress.android.widgets.HeaderGridView; import java.util.HashMap; @@ -93,6 +95,19 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.theme_browser_activity); + OnBackPressedCallback callback = new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + FragmentManager fm = getSupportFragmentManager(); + if (fm.getBackStackEntryCount() > 0) { + fm.popBackStack(); + } else { + CompatExtensionsKt.onBackPressedCompat(getOnBackPressedDispatcher(), this); + } + } + }; + getOnBackPressedDispatcher().addCallback(this, callback); + if (savedInstanceState == null) { addBrowserFragment(); fetchInstalledThemesIfJetpackSite(); @@ -129,23 +144,13 @@ protected void onSaveInstanceState(@NotNull Bundle outState) { public boolean onOptionsItemSelected(MenuItem item) { int i = item.getItemId(); if (i == android.R.id.home) { - onBackPressed(); + getOnBackPressedDispatcher().onBackPressed(); return true; } return super.onOptionsItemSelected(item); } - @Override - public void onBackPressed() { - FragmentManager fm = getSupportFragmentManager(); - if (fm.getBackStackEntryCount() > 0) { - fm.popBackStack(); - } else { - super.onBackPressed(); - } - } - @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/uploads/M4mVideoOptimizer.java b/WordPress/src/main/java/org/wordpress/android/ui/uploads/M4mVideoOptimizer.java deleted file mode 100644 index da09ed327ece..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/uploads/M4mVideoOptimizer.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.wordpress.android.ui.uploads; - -import androidx.annotation.NonNull; - -import org.m4m.MediaComposer; -import org.wordpress.android.analytics.AnalyticsTracker; -import org.wordpress.android.fluxc.model.MediaModel; -import org.wordpress.android.ui.prefs.AppPrefs; -import org.wordpress.android.util.AppLog; -import org.wordpress.android.util.WPVideoUtils; -import org.wordpress.android.util.analytics.AnalyticsUtils; - -import java.util.Map; - -import static org.wordpress.android.analytics.AnalyticsTracker.Stat.MEDIA_VIDEO_CANT_OPTIMIZE; - -public class M4mVideoOptimizer extends VideoOptimizerBase implements org.m4m.IProgressListener { - public M4mVideoOptimizer( - @NonNull MediaModel media, - @NonNull VideoOptimizationListener listener) { - super(media, listener); - } - - /* - * IProgressListener handlers - */ - @Override - public void onMediaStart() { - mStartTimeMS = System.currentTimeMillis(); - } - - @Override - public void onMediaProgress(float progress) { - sendProgressIfNeeded(progress); - } - - @Override - public void onMediaDone() { - trackVideoProcessingEvents(false, null); - selectMediaAndSendCompletionToListener(); - } - - @Override - public void onMediaPause() { - AppLog.d(AppLog.T.MEDIA, "VideoOptimizer > paused"); - } - - @Override - public void onMediaStop() { - // This seems to be called called in 2 cases. Do not use to check if we've manually stopped the composer. - // 1. When the encoding is done without errors, before onMediaDone - // 2. When we call 'stop' on the media composer - AppLog.d(AppLog.T.MEDIA, "VideoOptimizer > stopped"); - } - - @Override - public void onError(Exception e) { - AppLog.e(AppLog.T.MEDIA, "VideoOptimizer > Can't optimize the video", e); - trackVideoProcessingEvents(true, e); - mListener.onVideoOptimizationCompleted(mMedia); - } - - @Override - public void start() { - if (!arePathsValidated()) return; - - MediaComposer mediaComposer = null; - boolean wasNpeDetected = false; - - try { - mediaComposer = WPVideoUtils.getVideoOptimizationComposer( - getContext(), - mInputPath, - mOutputPath, - this, - AppPrefs.getVideoOptimizeWidth(), - AppPrefs.getVideoOptimizeQuality()); - } catch (NullPointerException npe) { - AppLog.w( - AppLog.T.MEDIA, - "VideoOptimizer > NullPointerException while getting composer " + npe.getMessage() - ); - wasNpeDetected = true; - } - - if (mediaComposer == null) { - AppLog.w(AppLog.T.MEDIA, "VideoOptimizer > null composer"); - Map properties = AnalyticsUtils.getMediaProperties(getContext(), true, - null, mInputPath); - properties.put("was_npe_detected", wasNpeDetected); - properties.put("optimizer_lib", "m4m"); - AnalyticsTracker.track(MEDIA_VIDEO_CANT_OPTIMIZE, properties); - mListener.onVideoOptimizationCompleted(mMedia); - return; - } - - // setup done. We're ready to optimize! - try { - mediaComposer.start(); - AppLog.d(AppLog.T.MEDIA, "VideoOptimizer > composer started"); - } catch (IllegalStateException e) { - AppLog.e(AppLog.T.MEDIA, "VideoOptimizer > failed to start composer", e); - mListener.onVideoOptimizationCompleted(mMedia); - } - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/utils/UiHelpers.kt b/WordPress/src/main/java/org/wordpress/android/ui/utils/UiHelpers.kt index 3b0bba6dc73f..fce9cf4cb452 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/utils/UiHelpers.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/utils/UiHelpers.kt @@ -58,9 +58,11 @@ class UiHelpers @Inject constructor() { view.visibility = if (visible) View.VISIBLE else View.GONE } - fun setTextOrHide(view: TextView, uiString: UiString?) { - val text = uiString?.let { getTextOfUiString(view.context, uiString) } - setTextOrHide(view, text) + fun setTextOrHide(view: TextView?, uiString: UiString?) { + view?.let { + val text = uiString?.let { getTextOfUiString(view.context, uiString) } + setTextOrHide(view, text) + } } fun setTextOrHide(view: TextView, @StringRes resId: Int?) { diff --git a/WordPress/src/main/java/org/wordpress/android/util/JetpackBrandingUtils.kt b/WordPress/src/main/java/org/wordpress/android/util/JetpackBrandingUtils.kt index 1656fdc4957e..1063f9aa8e56 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/JetpackBrandingUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/JetpackBrandingUtils.kt @@ -29,6 +29,11 @@ class JetpackBrandingUtils @Inject constructor( && !jetpackFeatureRemovalBrandingUtil.isInRemovalPhase() } + fun shouldShowJetpackBrandingInDashboard(): Boolean { + return isWpComSite() && jetpackPoweredFeatureConfig.isEnabled() && !buildConfigWrapper.isJetpackApp + && jetpackFeatureRemovalBrandingUtil.shouldShowBrandingInDashboard() + } + fun shouldShowJetpackBrandingForPhaseOne(): Boolean { return shouldShowJetpackBranding() && jetpackFeatureRemovalBrandingUtil.shouldShowPhaseOneBranding() } diff --git a/WordPress/src/main/java/org/wordpress/android/util/LocaleManager.java b/WordPress/src/main/java/org/wordpress/android/util/LocaleManager.java index 01d1d411d1e2..c3c59d7171cd 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/LocaleManager.java +++ b/WordPress/src/main/java/org/wordpress/android/util/LocaleManager.java @@ -53,6 +53,7 @@ public static Context setLocale(Context context) { * @param context current context used to access Shared Preferences. * @param configuration configuration that the locale should be applied to. */ + @SuppressLint("AppBundleLocaleChanges") public static Configuration updatedConfigLocale(Context context, Configuration configuration) { Locale locale = languageLocale(getLanguage(context)); Locale.setDefault(locale); @@ -154,6 +155,7 @@ private static void saveLanguageToPref(Context context, String language) { * @param language The 2-letter language code (example "en") * @return The modified context containing the updated localized resources */ + @SuppressLint("AppBundleLocaleChanges") private static Context updateResources(Context context, String language) { Locale locale = languageLocale(language); Locale.setDefault(locale); diff --git a/WordPress/src/main/java/org/wordpress/android/util/PackageManagerWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/PackageManagerWrapper.kt index b2aceb345ad5..8b5c8f2932c6 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/PackageManagerWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/PackageManagerWrapper.kt @@ -4,6 +4,7 @@ import android.content.ComponentName import android.content.Intent import android.content.pm.PackageManager import org.wordpress.android.util.AppLog.T +import org.wordpress.android.util.extensions.getActivityInfoCompat import org.wordpress.android.viewmodel.ContextProvider import javax.inject.Inject import javax.inject.Singleton @@ -38,7 +39,7 @@ class PackageManagerWrapper @Inject constructor( intent.component?.let { try { val context = contextProvider.getContext() - val activityInfo = context.packageManager.getActivityInfo(it, PackageManager.GET_META_DATA) + val activityInfo = context.packageManager.getActivityInfoCompat(it, PackageManager.GET_META_DATA) return activityInfo.labelRes } catch (ex: PackageManager.NameNotFoundException) { AppLog.e(T.UTILS, "Unable to extract label res from activity info") diff --git a/WordPress/src/main/java/org/wordpress/android/util/SiteUtils.java b/WordPress/src/main/java/org/wordpress/android/util/SiteUtils.java index 0e97693cae5c..aecc0f59775f 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/SiteUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/util/SiteUtils.java @@ -332,8 +332,8 @@ public static boolean checkMinimalWordPressVersion(SiteModel site, String minVer } public static boolean supportsStoriesFeature(SiteModel site, JetpackFeatureRemovalPhaseHelper helper) { - return site != null && (site.isWPCom() || checkMinimalJetpackVersion(site, WP_STORIES_JETPACK_VERSION)) - && !helper.shouldRemoveJetpackFeatures(); + return site != null && (site.isWPCom() || checkMinimalJetpackVersion(site, WP_STORIES_JETPACK_VERSION)) + && helper.shouldShowStoryPost(); } public static boolean supportsContactInfoFeature(SiteModel site) { 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 5a4df6403e6e..9a44fe0ec0d3 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/SiteUtilsWrapper.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.annotation.DimenRes import dagger.Reusable import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper import org.wordpress.android.ui.reader.utils.SiteAccessibilityInfo import javax.inject.Inject @@ -27,4 +28,7 @@ class SiteUtilsWrapper @Inject constructor(private val appContext: Context) { fun getSiteIconUrlOfResourceSize(site: SiteModel, @DimenRes sizeRes: Int): String { return SiteUtils.getSiteIconUrl(site, appContext.resources.getDimensionPixelSize(sizeRes)) } + fun supportsStoriesFeature(site: SiteModel?, helper: JetpackFeatureRemovalPhaseHelper): Boolean { + return SiteUtils.supportsStoriesFeature(site, helper) + } } diff --git a/WordPress/src/main/java/org/wordpress/android/util/UriWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/UriWrapper.kt index 943e8c68eb3e..8d5fbb09d316 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/UriWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/UriWrapper.kt @@ -20,3 +20,15 @@ data class UriWrapper(val uri: Uri) { return this.copy(uri = newUri) } } + +/** + * Note, java.net.URLEncoder is not currently a suitable alternative to using Uri.encode in the main codebase. + * This is because java.net.URLEncoder requires API 33 or later (we support down to API 24 at the time of writing). + * However, java.net.URLEncoder can be used in unit tests to avoid fully mocking the encode function. + * e.g. whenever(uriWrapper.encode(value)).thenReturn(URLEncoder.encode(value, StandardCharsets.UTF_8)) + */ +class UriEncoder { + fun encode(input: String): String { + return Uri.encode(input) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPMediaUtils.java b/WordPress/src/main/java/org/wordpress/android/util/WPMediaUtils.java index 40bcf329a9b1..87408847a19c 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/WPMediaUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/util/WPMediaUtils.java @@ -9,6 +9,7 @@ import android.content.pm.PackageManager; import android.media.MediaScannerConnection; import android.net.Uri; +import android.os.Build; import android.os.Environment; import android.provider.MediaStore; import android.view.ViewConfiguration; @@ -121,8 +122,9 @@ public static boolean shouldAdvertiseImageOptimization(final Context context) { } // Check we can access storage before asking for optimizing image - boolean hasStoreAccess = ContextCompat.checkSelfPermission( - context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + boolean hasStoreAccess = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + || ContextCompat.checkSelfPermission(context, + android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; if (!hasStoreAccess) { return false; } diff --git a/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java b/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java index 99544ddb1f6c..ca49f13b91b2 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/util/WPPermissionUtils.java @@ -7,6 +7,8 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.provider.Settings; import androidx.annotation.NonNull; @@ -30,13 +32,15 @@ public class WPPermissionUtils { public static final int SHARE_MEDIA_PERMISSION_REQUEST_CODE = 10; public static final int MEDIA_BROWSER_PERMISSION_REQUEST_CODE = 20; public static final int MEDIA_PREVIEW_PERMISSION_REQUEST_CODE = 30; - public static final int PHOTO_PICKER_STORAGE_PERMISSION_REQUEST_CODE = 40; + public static final int PHOTO_PICKER_MEDIA_PERMISSION_REQUEST_CODE = 40; public static final int PHOTO_PICKER_CAMERA_PERMISSION_REQUEST_CODE = 41; public static final int EDITOR_LOCATION_PERMISSION_REQUEST_CODE = 50; public static final int EDITOR_MEDIA_PERMISSION_REQUEST_CODE = 60; public static final int EDITOR_DRAG_DROP_PERMISSION_REQUEST_CODE = 70; public static final int READER_FILE_DOWNLOAD_PERMISSION_REQUEST_CODE = 80; + public static final int NOTIFICATIONS_PERMISSION_REQUEST_CODE = 90; + /** * called by the onRequestPermissionsResult() of various activities and fragments - tracks * the permission results, remembers that the permissions have been asked for, and optionally @@ -144,8 +148,16 @@ private static AppPrefs.PrefKey getPermissionAskedKey(@NonNull String permission return AppPrefs.UndeletablePrefKey.ASKED_PERMISSION_STORAGE_WRITE; case android.Manifest.permission.READ_EXTERNAL_STORAGE: return AppPrefs.UndeletablePrefKey.ASKED_PERMISSION_STORAGE_READ; + case android.Manifest.permission.READ_MEDIA_IMAGES: + return AppPrefs.UndeletablePrefKey.ASKED_PERMISSION_IMAGES_READ; + case android.Manifest.permission.READ_MEDIA_VIDEO: + return AppPrefs.UndeletablePrefKey.ASKED_PERMISSION_VIDEO_READ; + case android.Manifest.permission.READ_MEDIA_AUDIO: + return AppPrefs.UndeletablePrefKey.ASKED_PERMISSION_AUDIO_READ; case android.Manifest.permission.CAMERA: return AppPrefs.UndeletablePrefKey.ASKED_PERMISSION_CAMERA; + case Manifest.permission.POST_NOTIFICATIONS: + return AppPrefs.UndeletablePrefKey.ASKED_PERMISSION_NOTIFICATIONS; default: AppLog.w(AppLog.T.UTILS, "No key for requested permission"); return null; @@ -160,6 +172,12 @@ public static String getPermissionName(@NonNull Context context, @NonNull String case android.Manifest.permission.WRITE_EXTERNAL_STORAGE: case android.Manifest.permission.READ_EXTERNAL_STORAGE: return context.getString(R.string.permission_storage); + case android.Manifest.permission.READ_MEDIA_IMAGES: + return context.getString(R.string.permission_images); + case android.Manifest.permission.READ_MEDIA_VIDEO: + return context.getString(R.string.permission_video); + case android.Manifest.permission.READ_MEDIA_AUDIO: + return context.getString(R.string.permission_audio); case android.Manifest.permission.CAMERA: return context.getString(R.string.permission_camera); case Manifest.permission.RECORD_AUDIO: @@ -178,6 +196,12 @@ public static String getPermissionName(@NonNull ResourceProvider resourceProvide case android.Manifest.permission.WRITE_EXTERNAL_STORAGE: case android.Manifest.permission.READ_EXTERNAL_STORAGE: return resourceProvider.getString(R.string.permission_storage); + case android.Manifest.permission.READ_MEDIA_IMAGES: + return resourceProvider.getString(R.string.permission_images); + case android.Manifest.permission.READ_MEDIA_VIDEO: + return resourceProvider.getString(R.string.permission_video); + case android.Manifest.permission.READ_MEDIA_AUDIO: + return resourceProvider.getString(R.string.permission_audio); case android.Manifest.permission.CAMERA: return resourceProvider.getString(R.string.permission_camera); case Manifest.permission.RECORD_AUDIO: @@ -221,4 +245,20 @@ public static void showAppSettings(@NonNull Context context) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } + + /* + * open the device's notification settings page for this app so the user can edit permissions + */ + public static void showNotificationsSettings(@NonNull Context context) { + if (VERSION.SDK_INT >= VERSION_CODES.O) { + Intent intent = new Intent(); + intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } else { + // We can't open notifications settings screen directly. Instead, open the app settings. + showAppSettings(context); + } + } } diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/BlazeRemoteFields.kt b/WordPress/src/main/java/org/wordpress/android/util/config/BlazeRemoteFields.kt index e7596187fa3e..fdfddd740482 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/config/BlazeRemoteFields.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/config/BlazeRemoteFields.kt @@ -15,8 +15,7 @@ const val BLAZE_NON_DISMISSABLE_HASH_DEFAULT = "step-4" class BlazeNonDismissableHashConfig @Inject constructor(appConfig: AppConfig) : RemoteConfigField( appConfig, - BLAZE_NON_DISMISSABLE_HASH_REMOTE_FIELD, - BLAZE_NON_DISMISSABLE_HASH_DEFAULT + BLAZE_NON_DISMISSABLE_HASH_REMOTE_FIELD ) @@ -30,6 +29,5 @@ const val BLAZE_COMPLETED_STEP_HASH_DEFAULT = "step-5" class BlazeCompletedStepHashConfig @Inject constructor(appConfig: AppConfig) : RemoteConfigField( appConfig, - BLAZE_COMPLETED_STEP_HASH_REMOTE_FIELD, - BLAZE_COMPLETED_STEP_HASH_DEFAULT + BLAZE_COMPLETED_STEP_HASH_REMOTE_FIELD ) diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/BloggingPromptsSocialFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/BloggingPromptsSocialFeatureConfig.kt index 8cdb7ca9709a..b835550fba88 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/config/BloggingPromptsSocialFeatureConfig.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/config/BloggingPromptsSocialFeatureConfig.kt @@ -1,14 +1,17 @@ package org.wordpress.android.util.config import org.wordpress.android.BuildConfig -import org.wordpress.android.annotation.FeatureInDevelopment +import org.wordpress.android.annotation.Feature import javax.inject.Inject -@FeatureInDevelopment +private const val BLOGGING_PROMPTS_SOCIAL_REMOTE_FIELD = "blogging_prompts_social_enabled" + +@Feature(BLOGGING_PROMPTS_SOCIAL_REMOTE_FIELD, true) class BloggingPromptsSocialFeatureConfig @Inject constructor(appConfig: AppConfig) : FeatureConfig( appConfig, BuildConfig.BLOGGING_PROMPTS_SOCIAL, + BLOGGING_PROMPTS_SOCIAL_REMOTE_FIELD, ) { override fun isEnabled(): Boolean { return super.isEnabled() && BuildConfig.IS_JETPACK_APP diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/DasboardCardDomainFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/DasboardCardDomainFeatureConfig.kt new file mode 100644 index 000000000000..d685aacfed93 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/DasboardCardDomainFeatureConfig.kt @@ -0,0 +1,16 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.BuildConfig +import org.wordpress.android.annotation.Feature +import javax.inject.Inject + +private const val DASHBOARD_CARD_DOMAIN_REMOTE_FIELD = "dashboard_card_domain" + +@Feature(DASHBOARD_CARD_DOMAIN_REMOTE_FIELD, false) +class DashboardCardDomainFeatureConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.DASHBOARD_CARD_DOMAIN, + DASHBOARD_CARD_DOMAIN_REMOTE_FIELD +) diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/DashboardCardActvitiyLogConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/DashboardCardActvitiyLogConfig.kt new file mode 100644 index 000000000000..1c22bc7be68f --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/DashboardCardActvitiyLogConfig.kt @@ -0,0 +1,16 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.BuildConfig +import org.wordpress.android.annotation.Feature +import javax.inject.Inject + +const val DASHBOARD_CARD_ACTIVITY_LOG_REMOTE_FIELD = "dashboard_card_activity_log" + +@Feature(DASHBOARD_CARD_ACTIVITY_LOG_REMOTE_FIELD, false) +class DashboardCardActivityLogConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.DASHBOARD_CARD_ACTIVITY_LOG, + DASHBOARD_CARD_ACTIVITY_LOG_REMOTE_FIELD +) diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/DashboardCardPagesConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/DashboardCardPagesConfig.kt new file mode 100644 index 000000000000..9bd9c3eab743 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/DashboardCardPagesConfig.kt @@ -0,0 +1,16 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.BuildConfig +import org.wordpress.android.annotation.Feature +import javax.inject.Inject + +const val DASHBOARD_CARD_PAGES_REMOTE_FIELD = "dashboard_card_pages" + +@Feature(DASHBOARD_CARD_PAGES_REMOTE_FIELD, false) +class DashboardCardPagesConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.DASHBOARD_CARD_PAGES, + DASHBOARD_CARD_PAGES_REMOTE_FIELD +) diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/JetpackFeatureRemovalRemoteFields.kt b/WordPress/src/main/java/org/wordpress/android/util/config/JetpackFeatureRemovalRemoteFields.kt index d6d3899e5478..a119a4b194bf 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/config/JetpackFeatureRemovalRemoteFields.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/config/JetpackFeatureRemovalRemoteFields.kt @@ -10,8 +10,7 @@ const val JP_DEADLINE_DEFAULT = "" class JPDeadlineConfig @Inject constructor(appConfig: AppConfig) : RemoteConfigField( appConfig, - JP_DEADLINE_REMOTE_FIELD, - JP_DEADLINE_DEFAULT + JP_DEADLINE_REMOTE_FIELD ) const val PHASE_TWO_BLOG_POST_REMOTE_FIELD = "phase_two_blog_post" @@ -21,8 +20,7 @@ const val PHASE_TWO_BLOG_POST_DEFAULT = "" class PhaseTwoBlogPostLinkConfig @Inject constructor(appConfig: AppConfig) : RemoteConfigField( appConfig, - PHASE_TWO_BLOG_POST_REMOTE_FIELD, - PHASE_TWO_BLOG_POST_DEFAULT + PHASE_TWO_BLOG_POST_REMOTE_FIELD ) const val PHASE_THREE_BLOG_POST_LINK_REMOTE_FIELD = "phase_three_blog_post" @@ -35,8 +33,7 @@ const val PHASE_THREE_BLOG_POST_LINK_DEFAULT_VALUE = "" class PhaseThreeBlogPostLinkConfig @Inject constructor(appConfig: AppConfig) : RemoteConfigField( appConfig, - PHASE_THREE_BLOG_POST_LINK_REMOTE_FIELD, - PHASE_THREE_BLOG_POST_LINK_DEFAULT_VALUE + PHASE_THREE_BLOG_POST_LINK_REMOTE_FIELD ) const val PHASE_FOUR_BLOG_POST_LINK_REMOTE_FIELD = "phase_four_blog_post" @@ -49,8 +46,7 @@ const val PHASE_FOUR_BLOG_POST_LINK_DEFAULT_VALUE = "" class PhaseFourBlogPostLinkConfig @Inject constructor(appConfig: AppConfig) : RemoteConfigField( appConfig, - PHASE_FOUR_BLOG_POST_LINK_REMOTE_FIELD, - PHASE_FOUR_BLOG_POST_LINK_DEFAULT_VALUE + PHASE_FOUR_BLOG_POST_LINK_REMOTE_FIELD ) const val PHASE_NEW_USERS_BLOG_POST_LINK = "phase_new_users_blog_post" @@ -63,8 +59,7 @@ const val PHASE_NEW_USERS_BLOG_POST_LINK_DEFAULT_VALUE = "" class PhaseNewUsersBlogPostLinkConfig @Inject constructor(appConfig: AppConfig) : RemoteConfigField( appConfig, - PHASE_NEW_USERS_BLOG_POST_LINK, - PHASE_NEW_USERS_BLOG_POST_LINK_DEFAULT_VALUE + PHASE_NEW_USERS_BLOG_POST_LINK ) const val PHASE_SELF_HOSTED_BLOG_POST_LINK_REMOTE_FIELD = "phase_self_hosted_blog_post" @@ -77,6 +72,5 @@ const val PHASE_SELF_HOSTED_BLOG_POST_LINK_DEFAULT_VALUE = "" class PhaseSelfHostedPostLinkConfig @Inject constructor(appConfig: AppConfig) : RemoteConfigField( appConfig, - PHASE_SELF_HOSTED_BLOG_POST_LINK_REMOTE_FIELD, - PHASE_SELF_HOSTED_BLOG_POST_LINK_DEFAULT_VALUE + PHASE_SELF_HOSTED_BLOG_POST_LINK_REMOTE_FIELD ) diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/JetpackFeatureRemovalStaticPostersConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/JetpackFeatureRemovalStaticPostersConfig.kt new file mode 100644 index 000000000000..ceb3ad5b5f7b --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/JetpackFeatureRemovalStaticPostersConfig.kt @@ -0,0 +1,22 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.BuildConfig +import org.wordpress.android.annotation.Feature +import org.wordpress.android.util.config.JetpackFeatureRemovalStaticPostersConfig.Companion.JETPACK_FEATURE_REMOVAL_STATIC_POSTERS_REMOTE_FIELD +import javax.inject.Inject + +/** + * Configuration for Jetpack feature removal phase new users + */ +@Feature(JETPACK_FEATURE_REMOVAL_STATIC_POSTERS_REMOTE_FIELD, false) +class JetpackFeatureRemovalStaticPostersConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.JETPACK_FEATURE_REMOVAL_STATIC_POSTERS, + JETPACK_FEATURE_REMOVAL_STATIC_POSTERS_REMOTE_FIELD +) { + companion object { + const val JETPACK_FEATURE_REMOVAL_STATIC_POSTERS_REMOTE_FIELD = "jp_removal_static_posters" + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/RemoteConfigField.kt b/WordPress/src/main/java/org/wordpress/android/util/config/RemoteConfigField.kt index 9f55bfb27710..dfba647f0a01 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/config/RemoteConfigField.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/config/RemoteConfigField.kt @@ -1,13 +1,13 @@ package org.wordpress.android.util.config -open class RemoteConfigField(val appConfig: AppConfig, val remoteField: String, val defaultValue: T) { +open class RemoteConfigField(val appConfig: AppConfig, val remoteField: String) { @Suppress("UseCheckOrError") - inline fun getValue(): T { + inline fun getValue(): R { val remoteFieldValue = appConfig.getRemoteFieldConfigValue(remoteField) - return when (T::class) { - Int::class -> remoteFieldValue.toInt() as T - String::class -> remoteFieldValue as T - Long::class -> remoteFieldValue.toLong() as T + return when (R::class) { + Int::class -> remoteFieldValue.toInt() as R + String::class -> remoteFieldValue as R + Long::class -> remoteFieldValue.toLong() as R // add other types here if need else -> throw IllegalStateException("Unknown Generic Type") } diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/RemoteFieldConfigRepository.kt b/WordPress/src/main/java/org/wordpress/android/util/config/RemoteFieldConfigRepository.kt index f1918f6f9daa..59dc9abe497f 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/config/RemoteFieldConfigRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/config/RemoteFieldConfigRepository.kt @@ -31,16 +31,23 @@ class RemoteFieldConfigRepository // If the flags are empty, then this means that the // that the app is launched for the first time and we need to // store the default in the database - if (remoteFields.isEmpty()) { - insertRemoteConfigDefaultsInDatabase() + if (!remoteFields.containsAllFields()) { + insertMissingRemoteConfigDefaultsInDatabase(remoteFields.map { it.key }) refresh() } } } - private fun insertRemoteConfigDefaultsInDatabase() { - RemoteFieldConfigDefaults.remoteFieldConfigDefaults.mapNotNull { remoteField -> - remoteField.let { + private fun List.containsAllFields(): Boolean { + val defaults = RemoteFieldConfigDefaults.remoteFieldConfigDefaults + return defaults.all { default -> + this.any { it.key == default.key } + } + } + + private fun insertMissingRemoteConfigDefaultsInDatabase(existingKeys: List) { + RemoteFieldConfigDefaults.remoteFieldConfigDefaults.forEach { remoteField -> + if (remoteField.key !in existingKeys) { remoteConfigStore.insertRemoteConfig( remoteField.key, remoteField.value.toString() @@ -64,6 +71,10 @@ class RemoteFieldConfigRepository Stat.REMOTE_FIELD_CONFIG_SYNCED_STATE, configValues ) + + // re-insert the defaults in case they were removed from the remote config and also to make sure latest + // version defaults overwrite the old ones that might already be in the database + insertMissingRemoteConfigDefaultsInDatabase(configValues.map { it.key }) } if (response.isError) { AppLog.e(UTILS, "Remote field config values sync failed") @@ -71,6 +82,9 @@ class RemoteFieldConfigRepository } fun getValue(field: String): String { - return remoteFields.find { it.key == field }?.value ?: "" + // search the remote fields (from local database) then in-memory defaults, and return "" as fallback + return remoteFields.find { it.key == field }?.value + ?: RemoteFieldConfigDefaults.remoteFieldConfigDefaults[field]?.toString() + ?: "" } } diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/WPIndividualPluginOverlayFeatureConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/WPIndividualPluginOverlayFeatureConfig.kt new file mode 100644 index 000000000000..2da8f4e2af9c --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/WPIndividualPluginOverlayFeatureConfig.kt @@ -0,0 +1,20 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.BuildConfig +import org.wordpress.android.annotation.Feature +import javax.inject.Inject + +private const val WP_INDIVIDUAL_PLUGIN_OVERLAY_REMOTE_FIELD = "wp_individual_plugin_overlay" + +@Feature(WP_INDIVIDUAL_PLUGIN_OVERLAY_REMOTE_FIELD, true) +class WPIndividualPluginOverlayFeatureConfig @Inject constructor( + appConfig: AppConfig +) : FeatureConfig( + appConfig, + BuildConfig.WP_INDIVIDUAL_PLUGIN_OVERLAY, + WP_INDIVIDUAL_PLUGIN_OVERLAY_REMOTE_FIELD +) { + override fun isEnabled(): Boolean { + return super.isEnabled() && !BuildConfig.IS_JETPACK_APP + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/util/config/WPIndividualPluginOverlayMaxShownConfig.kt b/WordPress/src/main/java/org/wordpress/android/util/config/WPIndividualPluginOverlayMaxShownConfig.kt new file mode 100644 index 000000000000..12191c82d5d5 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/config/WPIndividualPluginOverlayMaxShownConfig.kt @@ -0,0 +1,18 @@ +package org.wordpress.android.util.config + +import org.wordpress.android.annotation.RemoteFieldDefaultGenerater +import javax.inject.Inject + +private const val WP_INDIVIDUAL_PLUGIN_OVERLAY_MAX_SHOWN_REMOTE_FIELD = "wp_plugin_overlay_max_shown" +private const val WP_INDIVIDUAL_PLUGIN_OVERLAY_MAX_SHOWN_DEFAULT = 3 + +@RemoteFieldDefaultGenerater( + remoteField = WP_INDIVIDUAL_PLUGIN_OVERLAY_MAX_SHOWN_REMOTE_FIELD, + defaultValue = WP_INDIVIDUAL_PLUGIN_OVERLAY_MAX_SHOWN_DEFAULT.toString() +) +class WPIndividualPluginOverlayMaxShownConfig @Inject constructor( + appConfig: AppConfig, +) : RemoteConfigField( + appConfig = appConfig, + remoteField = WP_INDIVIDUAL_PLUGIN_OVERLAY_MAX_SHOWN_REMOTE_FIELD +) diff --git a/WordPress/src/main/java/org/wordpress/android/util/extensions/CompatExtensions.kt b/WordPress/src/main/java/org/wordpress/android/util/extensions/CompatExtensions.kt new file mode 100644 index 000000000000..cc0cb8349331 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/extensions/CompatExtensions.kt @@ -0,0 +1,130 @@ +package org.wordpress.android.util.extensions + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import androidx.activity.OnBackPressedCallback +import androidx.activity.OnBackPressedDispatcher +import java.io.Serializable + +/** + * This is a temporary workaround for the issue described here: https://issuetracker.google.com/issues/247982487 + * This function temporary disables the callback to allow the system to handle the back pressed event. + * + * TODO: Replace this temporary workaround before enabling predictive back gesture on this project + * (android:enableOnBackInvokedCallback="true"). + * + * Related Issue: https://github.com/wordpress-mobile/WordPress-Android/issues/18053 + */ +@Suppress("ForbiddenComment") +fun OnBackPressedDispatcher.onBackPressedCompat(onBackPressedCallback: OnBackPressedCallback) { + onBackPressedCallback.isEnabled = false + onBackPressed() + onBackPressedCallback.isEnabled = true +} + +/** + * TODO: Remove this when stable androidx.core 1.10 is released. Use IntentCompat instead. + */ +@Suppress("ForbiddenComment") +inline fun Intent.getParcelableExtraCompat(key: String): T? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelableExtra(key, T::class.java) + } else { + @Suppress("DEPRECATION") + getParcelableExtra(key) as T? + } + +/** + * This is an Android 13 compatibility function that is not included in IntentCompat. + */ +inline fun Intent.getSerializableExtraCompat(key: String): T? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getSerializableExtra(key, T::class.java) + } else { + @Suppress("DEPRECATION") + getSerializableExtra(key) as T? + } + +/** + * TODO: Remove this when stable androidx.core 1.10 is released. Use BundleCompat instead. + */ +@Suppress("ForbiddenComment") +inline fun Bundle.getParcelableCompat(key: String): T? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelable(key, T::class.java) + } else { + @Suppress("DEPRECATION") + getParcelable(key) + } + +/** + * TODO: Remove this when stable androidx.core 1.10 is released. Use BundleCompat instead. + */ +@Suppress("ForbiddenComment") +inline fun Bundle.getParcelableArrayListCompat(key: String): ArrayList? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelableArrayList(key, T::class.java) + } else { + @Suppress("DEPRECATION") + getParcelableArrayList(key) + } + +/** + * This is an Android 13 compatibility function that is not included in BundleCompat. + */ +inline fun Bundle.getSerializableCompat(key: String): T? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getSerializable(key, T::class.java) + } else { + @Suppress("DEPRECATION") + getSerializable(key) as T? + } + +/** + * TODO: Remove this when upgrading to androidx.core 1.9.0. Use ParcelCompat instead. + */ +@Suppress("ForbiddenComment") +inline fun Parcel.readParcelableCompat(loader: ClassLoader?): T? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + readParcelable(loader, T::class.java) + } else { + @Suppress("DEPRECATION") + readParcelable(loader) + } +} + +/** + * TODO: Remove this when upgrading to androidx.core 1.9.0. Use ParcelCompat instead. + */ +@Suppress("ForbiddenComment") +inline fun Parcel.readListCompat(outVal: MutableList, loader: ClassLoader?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + readList(outVal, loader, T::class.java) + } else { + @Suppress("DEPRECATION") + readList(outVal, loader) + } +} + +fun PackageManager.getActivityInfoCompat(componentName: ComponentName, flags: Int): ActivityInfo = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getActivityInfo(componentName, PackageManager.ComponentInfoFlags.of(flags.toLong())) + } else { + @Suppress("DEPRECATION") + getActivityInfo(componentName, flags) + } + +fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int): PackageInfo? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong())) + } else { + @Suppress("DEPRECATION") + getPackageInfo(packageName, flags) + } diff --git a/WordPress/src/main/java/org/wordpress/android/util/extensions/DialogExtensions.kt b/WordPress/src/main/java/org/wordpress/android/util/extensions/DialogExtensions.kt index 64fc71f67118..93056c4071e7 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/extensions/DialogExtensions.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/extensions/DialogExtensions.kt @@ -2,6 +2,11 @@ package org.wordpress.android.util.extensions import android.app.Dialog import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.FrameLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog import org.wordpress.android.R import org.wordpress.android.R.attr @@ -26,3 +31,22 @@ fun Dialog.setStatusBarAsSurfaceColor() { } } } + +fun BottomSheetDialog.fillScreen(isDraggable: Boolean = false) { + setOnShowListener { + val bottomSheet: FrameLayout = findViewById( + com.google.android.material.R.id.design_bottom_sheet + ) ?: return@setOnShowListener + + val bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet) + bottomSheetBehavior.maxWidth = ViewGroup.LayoutParams.MATCH_PARENT + bottomSheetBehavior.isDraggable = isDraggable + bottomSheetBehavior.skipCollapsed = true + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + + bottomSheet.layoutParams?.let { layoutParams -> + layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT + bottomSheet.layoutParams = layoutParams + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/util/extensions/IndividualJetpackPluginExtensions.kt b/WordPress/src/main/java/org/wordpress/android/util/extensions/IndividualJetpackPluginExtensions.kt new file mode 100644 index 000000000000..d89fbb3e2415 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/util/extensions/IndividualJetpackPluginExtensions.kt @@ -0,0 +1,52 @@ +package org.wordpress.android.util.extensions + +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.persistence.JetpackCPConnectedSiteModel + +/** + * @return a List of the active individual Jetpack plugins names + * (e.g. [Jetpack Search, Jetpack VaultPress Backup]) or null if there are no active Jetpack connection plugins. + */ +fun SiteModel.activeIndividualJetpackPluginNames(): List? = + activeJetpackConnectionPluginValues()?.filterMapJetpackIndividualPluginValuesToNames() + +/** + * @return a List of the active individual Jetpack plugins names + * (e.g. [Jetpack Search, Jetpack VaultPress Backup]) or null if there are no active Jetpack connection plugins. + */ +fun JetpackCPConnectedSiteModel.activeIndividualJetpackPluginNames(): List = + activeJetpackConnectionPlugins.filterMapJetpackIndividualPluginValuesToNames() + +/** + * @return true if the site has active Jetpack individual plugins connected and does not have the full Jetpack plugin + */ +fun SiteModel.isJetpackIndividualPluginConnectedWithoutFullPlugin(): Boolean = + activeJetpackConnectionPluginValues()?.run { + isNotEmpty() && none { it.isJetpackFullPlugin() } && any { it.isJetpackIndividualPlugin() } + } ?: false + +/** + * @return true if the site has active Jetpack individual plugins connected and does not have the full Jetpack plugin + */ +fun JetpackCPConnectedSiteModel.isJetpackIndividualPluginConnectedWithoutFullPlugin(): Boolean = + activeJetpackConnectionPlugins.run { + isNotEmpty() && none { it.isJetpackFullPlugin() } && any { it.isJetpackIndividualPlugin() } + } + +private fun List.filterMapJetpackIndividualPluginValuesToNames(): List = this + .filter { it.isJetpackIndividualPlugin() } + .mapNotNull { + when (it) { + "jetpack-search" -> "Jetpack Search" + "jetpack-backup" -> "Jetpack VaultPress Backup" + "jetpack-protect" -> "Jetpack Protect" + "jetpack-videopress" -> "Jetpack VideoPress" + "jetpack-social" -> "Jetpack Social" + "jetpack-boost" -> "Jetpack Boost" + else -> null + } + } + +private fun String.isJetpackIndividualPlugin(): Boolean = this.startsWith("jetpack-") + +private fun String.isJetpackFullPlugin(): Boolean = this == "jetpack" diff --git a/WordPress/src/main/java/org/wordpress/android/util/extensions/ProductExtensions.kt b/WordPress/src/main/java/org/wordpress/android/util/extensions/ProductExtensions.kt index ab8fd89691ca..39861b80ed88 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/extensions/ProductExtensions.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/extensions/ProductExtensions.kt @@ -1,8 +1,5 @@ package org.wordpress.android.util.extensions import org.wordpress.android.fluxc.model.products.Product -import java.util.Currency fun Product?.isOnSale(): Boolean = this?.saleCost?.let { it > 0.0 } == true -fun Product?.saleCostForDisplay() = - this?.run { Currency.getInstance(currencyCode).symbol + "%.2f".format(saleCost) } ?: "" diff --git a/WordPress/src/main/java/org/wordpress/android/util/extensions/SiteModelExtensions.kt b/WordPress/src/main/java/org/wordpress/android/util/extensions/SiteModelExtensions.kt index cc9dbf5278ad..67609c27b161 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/extensions/SiteModelExtensions.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/extensions/SiteModelExtensions.kt @@ -31,28 +31,3 @@ val SiteModel.stateLogInformation: String */ fun SiteModel.activeJetpackConnectionPluginValues(): List? = activeJetpackConnectionPlugins?.split(",") - -/** - * @return a List of the active Jetpack connection plugins names - * (e.g. [Jetpack Search, Jetpack VaultPress Backup]) or null if there are no active Jetpack connection plugins. - */ -fun SiteModel.activeJetpackConnectionPluginNames(): List? = - activeJetpackConnectionPluginValues()?.filter { it.startsWith("jetpack-") } - ?.map { - when (it) { - "jetpack-search" -> "Jetpack Search" - "jetpack-backup" -> "Jetpack VaultPress Backup" - "jetpack-protect" -> "Jetpack Protect" - "jetpack-videopress" -> "Jetpack VideoPress" - "jetpack-social" -> "Jetpack Social" - "jetpack-boost" -> "Jetpack Boost" - else -> "" - } - } - ?.filter { it.isNotEmpty() } - - -fun SiteModel.isJetpackConnectedWithoutFullPlugin(): Boolean = - activeJetpackConnectionPluginValues()?.run { - isNotEmpty() && !contains("jetpack") && firstOrNull { it.startsWith("jetpack-") } != null - } ?: false diff --git a/WordPress/src/main/java/org/wordpress/android/util/publicdata/PackageManagerWrapper.kt b/WordPress/src/main/java/org/wordpress/android/util/publicdata/PackageManagerWrapper.kt index a996ccbca90a..abaa20bce5d7 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/publicdata/PackageManagerWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/publicdata/PackageManagerWrapper.kt @@ -1,10 +1,11 @@ package org.wordpress.android.util.publicdata import android.content.pm.PackageInfo +import org.wordpress.android.util.extensions.getPackageInfoCompat import org.wordpress.android.viewmodel.ContextProvider import javax.inject.Inject class PackageManagerWrapper @Inject constructor(private val contextProvider: ContextProvider) { fun getPackageInfo(packageName: String, flags: Int = 0): PackageInfo? = - contextProvider.getContext().packageManager.getPackageInfo(packageName, flags) + contextProvider.getContext().packageManager.getPackageInfoCompat(packageName, flags) } diff --git a/WordPress/src/main/java/org/wordpress/android/util/signature/SignatureUtils.kt b/WordPress/src/main/java/org/wordpress/android/util/signature/SignatureUtils.kt index f693c397b394..2095bf4aedc8 100644 --- a/WordPress/src/main/java/org/wordpress/android/util/signature/SignatureUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/util/signature/SignatureUtils.kt @@ -4,6 +4,7 @@ import android.annotation.TargetApi import android.content.pm.PackageManager import android.os.Build.VERSION import android.os.Build.VERSION_CODES +import org.wordpress.android.util.extensions.getPackageInfoCompat import org.wordpress.android.viewmodel.ContextProvider import java.security.MessageDigest import javax.inject.Inject @@ -28,8 +29,11 @@ class SignatureUtils @Inject constructor( trustedPackageId: String, trustedSignatureHash: String ): Boolean = try { - val signingInfo = contextProvider.getContext().packageManager.getPackageInfo( - trustedPackageId, PackageManager.GET_SIGNING_CERTIFICATES + val signingInfo = requireNotNull( + contextProvider.getContext().packageManager.getPackageInfoCompat( + trustedPackageId, + PackageManager.GET_SIGNING_CERTIFICATES + ) ).signingInfo if (signingInfo.hasMultipleSigners()) { throw SignatureNotFoundException() diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/accounts/PostSignupInterstitialViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/accounts/PostSignupInterstitialViewModel.kt index 3bed4a03c1ab..203416e5d8c0 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/accounts/PostSignupInterstitialViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/accounts/PostSignupInterstitialViewModel.kt @@ -1,6 +1,9 @@ package org.wordpress.android.viewmodel.accounts import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.wordpress.android.analytics.AnalyticsTracker.Stat.WELCOME_NO_SITES_INTERSTITIAL_ADD_SELF_HOSTED_SITE_TAPPED import org.wordpress.android.analytics.AnalyticsTracker.Stat.WELCOME_NO_SITES_INTERSTITIAL_CREATE_NEW_SITE_TAPPED import org.wordpress.android.analytics.AnalyticsTracker.Stat.WELCOME_NO_SITES_INTERSTITIAL_DISMISSED @@ -8,10 +11,12 @@ import org.wordpress.android.analytics.AnalyticsTracker.Stat.WELCOME_NO_SITES_IN import org.wordpress.android.ui.accounts.UnifiedLoginTracker import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Click import org.wordpress.android.ui.accounts.UnifiedLoginTracker.Step.SUCCESS +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginHelper import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.viewmodel.SingleLiveEvent import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel.NavigationAction.DISMISS +import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel.NavigationAction.SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel.NavigationAction.START_SITE_CONNECTION_FLOW import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel.NavigationAction.START_SITE_CREATION_FLOW import javax.inject.Inject @@ -20,7 +25,8 @@ class PostSignupInterstitialViewModel @Inject constructor( private val appPrefs: AppPrefsWrapper, private val unifiedLoginTracker: UnifiedLoginTracker, - private val analyticsTracker: AnalyticsTrackerWrapper + private val analyticsTracker: AnalyticsTrackerWrapper, + private val wpJetpackIndividualPluginHelper: WPJetpackIndividualPluginHelper, ) : ViewModel() { val navigationAction: SingleLiveEvent = SingleLiveEvent() @@ -28,6 +34,7 @@ class PostSignupInterstitialViewModel analyticsTracker.track(WELCOME_NO_SITES_INTERSTITIAL_SHOWN) unifiedLoginTracker.track(step = SUCCESS) appPrefs.shouldShowPostSignupInterstitial = false + checkJetpackIndividualPluginOverlayShouldShow() } fun onCreateNewSiteButtonPressed() { @@ -52,5 +59,27 @@ class PostSignupInterstitialViewModel navigationAction.value = DISMISS } - enum class NavigationAction { START_SITE_CREATION_FLOW, START_SITE_CONNECTION_FLOW, DISMISS } + private fun checkJetpackIndividualPluginOverlayShouldShow() { + // don't check if already shown + if (navigationAction.value == SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY) return + + viewModelScope.launch { + val showOverlay = wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay() + if (showOverlay) { + delay(DELAY_BEFORE_SHOWING_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY) + navigationAction.postValue(SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY) + } + } + } + + enum class NavigationAction { + START_SITE_CREATION_FLOW, + START_SITE_CONNECTION_FLOW, + DISMISS, + SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY + } + + companion object { + private const val DELAY_BEFORE_SHOWING_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY = 500L + } } diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt index 3881a6a835f0..db17828d2fe1 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt @@ -6,14 +6,20 @@ import android.text.style.ClickableSpan import android.view.View import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.map import org.wordpress.android.R import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.activity.ActivityLogModel import org.wordpress.android.fluxc.model.activity.ActivityLogModel.ActivityActor +import org.wordpress.android.fluxc.model.dashboard.CardModel import org.wordpress.android.fluxc.store.ActivityLogStore +import org.wordpress.android.fluxc.store.dashboard.CardsStore import org.wordpress.android.fluxc.tools.FormattableRange +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.activitylog.detail.ActivityLogDetailModel import org.wordpress.android.ui.activitylog.detail.ActivityLogDetailNavigationEvents import org.wordpress.android.ui.activitylog.detail.ActivityLogDetailNavigationEvents.ShowDocumentationPage @@ -24,24 +30,31 @@ import org.wordpress.android.util.extensions.toFormattedDateString import org.wordpress.android.util.extensions.toFormattedTimeString import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ResourceProvider +import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent import javax.inject.Inject +import javax.inject.Named const val ACTIVITY_LOG_ID_KEY: String = "activity_log_id_key" const val ACTIVITY_LOG_ARE_BUTTONS_VISIBLE_KEY: String = "activity_log_are_buttons_visible_key" const val ACTIVITY_LOG_IS_RESTORE_HIDDEN_KEY: String = "activity_log_is_restore_hidden_key" +const val ACTIVITY_LOG_IS_DASHBOARD_CARD_ENTRY_KEY: String = "activity_log_is_dashboard_card_entry_key" @HiltViewModel class ActivityLogDetailViewModel @Inject constructor( val dispatcher: Dispatcher, private val activityLogStore: ActivityLogStore, private val resourceProvider: ResourceProvider, - private val htmlMessageUtils: HtmlMessageUtils -) : ViewModel() { + private val htmlMessageUtils: HtmlMessageUtils, + private val cardsStore: CardsStore, + @param:Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, + @param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher +) : ScopedViewModel(mainDispatcher) { lateinit var site: SiteModel lateinit var activityLogId: String var areButtonsVisible = false var isRestoreHidden = false + var isDashboardCardEntry = false private val _navigationEvents = MutableLiveData>() val navigationEvents: LiveData> @@ -51,8 +64,8 @@ class ActivityLogDetailViewModel @Inject constructor( val handleFormattableRangeClick: LiveData get() = _handleFormattableRangeClick - private val _item = MutableLiveData() - val activityLogItem: LiveData + private val _item = MutableLiveData() + val activityLogItem: LiveData get() = _item private val _restoreVisible = MutableLiveData() @@ -74,39 +87,72 @@ class ActivityLogDetailViewModel @Inject constructor( site: SiteModel, activityLogId: String, areButtonsVisible: Boolean, - isRestoreHidden: Boolean + isRestoreHidden: Boolean, + isDashboardCardEntry: Boolean ) { this.site = site this.activityLogId = activityLogId this.areButtonsVisible = areButtonsVisible this.isRestoreHidden = isRestoreHidden + this.isDashboardCardEntry = isDashboardCardEntry _restoreVisible.value = areButtonsVisible && !isRestoreHidden _downloadBackupVisible.value = areButtonsVisible _multisiteVisible.value = if (isRestoreHidden) Pair(true, getMultisiteMessage()) else Pair(false, null) - if (activityLogId != _item.value?.activityID) { - _item.value = activityLogStore - .getActivityLogForSite(site) - .find { it.activityID == activityLogId } - ?.let { - ActivityLogDetailModel( - activityID = it.activityID, - rewindId = it.rewindID, - actorIconUrl = it.actor?.avatarURL, - showJetpackIcon = it.actor?.showJetpackIcon(), - isRewindButtonVisible = it.rewindable ?: false, - actorName = it.actor?.displayName, - actorRole = it.actor?.role, - content = it.content, - summary = it.summary, - createdDate = it.published.toFormattedDateString(), - createdTime = it.published.toFormattedTimeString() - ) + activityLogId.takeIf { it != _item.value?.activityID }?.let { + findAndPostActivityLogItemDetail() + } + } + + + private fun findAndPostActivityLogItemDetail() { + activityLogStore + .getActivityLogForSite(site) + .find { it.activityID == activityLogId } + ?.toActivityLogDetailModel() + ?.let { + _item.value = it + }?: findAndPostActivityLogItemDetailViaDashboardCardsIfNeeded() + } + + private fun findAndPostActivityLogItemDetailViaDashboardCardsIfNeeded() { + if (!isDashboardCardEntry) { + _item.value = null + return + } + + launch(bgDispatcher) { + cardsStore.getCards(site, listOf(CardModel.Type.ACTIVITY)) + .map { it.model } + .collect { result -> + _item.postValue( + result.takeIf { !it.isNullOrEmpty() } + ?.let { activityCardModelList -> + (activityCardModelList[0] as CardModel.ActivityCardModel) + .activities + .find { it.activityID == activityLogId } + ?.toActivityLogDetailModel() + }) } } } + private fun ActivityLogModel.toActivityLogDetailModel() = + ActivityLogDetailModel( + activityID = activityID, + rewindId = rewindID, + actorIconUrl = actor?.avatarURL, + showJetpackIcon = actor?.showJetpackIcon(), + isRewindButtonVisible = rewindable ?: false, + actorName = actor?.displayName, + actorRole = actor?.role, + content = content, + summary = summary, + createdDate = published.toFormattedDateString(), + createdTime = published.toFormattedTimeString() + ) + private fun getMultisiteMessage(): SpannableString { val clickableText = resourceProvider.getString(R.string.activity_log_visit_our_documentation_page) val multisiteMessage = htmlMessageUtils.getHtmlMessageFromStringFormatResId( diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/SitePickerViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/SitePickerViewModel.kt index 08b0b9ccbcd6..44aa163e863e 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/SitePickerViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/SitePickerViewModel.kt @@ -3,47 +3,32 @@ package org.wordpress.android.viewmodel.main import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginHelper import org.wordpress.android.ui.main.SitePickerAdapter.SiteRecord import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.main.SitePickerViewModel.Action.AskForSiteSelection import org.wordpress.android.viewmodel.main.SitePickerViewModel.Action.ContinueReblogTo import org.wordpress.android.viewmodel.main.SitePickerViewModel.Action.NavigateToState +import org.wordpress.android.viewmodel.main.SitePickerViewModel.Action.ShowJetpackIndividualPluginOverlay import org.wordpress.android.viewmodel.main.SitePickerViewModel.ActionType.ASK_FOR_SITE_SELECTION import org.wordpress.android.viewmodel.main.SitePickerViewModel.ActionType.CONTINUE_REBLOG_TO import org.wordpress.android.viewmodel.main.SitePickerViewModel.ActionType.NAVIGATE_TO_STATE +import org.wordpress.android.viewmodel.main.SitePickerViewModel.ActionType.SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY import org.wordpress.android.viewmodel.main.SitePickerViewModel.NavigateState.TO_NO_SITE_SELECTED import org.wordpress.android.viewmodel.main.SitePickerViewModel.NavigateState.TO_SITE_SELECTED import javax.inject.Inject -class SitePickerViewModel @Inject constructor() : ViewModel() { +class SitePickerViewModel @Inject constructor( + private val wpJetpackIndividualPluginHelper: WPJetpackIndividualPluginHelper, +) : ViewModel() { private val _onActionTriggered = MutableLiveData>() val onActionTriggered: LiveData> = _onActionTriggered private var siteForReblog: SiteRecord? = null - enum class ActionType { - NAVIGATE_TO_STATE, - CONTINUE_REBLOG_TO, - ASK_FOR_SITE_SELECTION - } - - enum class NavigateState { - TO_SITE_SELECTED, - TO_NO_SITE_SELECTED - } - - sealed class Action(val actionType: ActionType) { - data class NavigateToState(val navigateState: NavigateState, val siteForReblog: SiteRecord? = null) : Action( - NAVIGATE_TO_STATE - ) - - data class ContinueReblogTo(val siteForReblog: SiteRecord?) : Action( - CONTINUE_REBLOG_TO - ) - - object AskForSiteSelection : Action(ASK_FOR_SITE_SELECTION) - } - fun onSiteForReblogSelected(siteRecord: SiteRecord) { selectSite(siteRecord) } @@ -72,4 +57,47 @@ class SitePickerViewModel @Inject constructor() : ViewModel() { siteForReblog = siteRecord _onActionTriggered.value = Event(NavigateToState(TO_SITE_SELECTED, siteRecord)) } + + fun onSiteListLoaded() { + // don't check if already shown + if (_onActionTriggered.value?.peekContent() == ShowJetpackIndividualPluginOverlay) return + + viewModelScope.launch { + val showOverlay = wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay() + if (showOverlay) { + delay(DELAY_BEFORE_SHOWING_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY) + _onActionTriggered.postValue(Event(ShowJetpackIndividualPluginOverlay)) + } + } + } + + enum class ActionType { + NAVIGATE_TO_STATE, + CONTINUE_REBLOG_TO, + ASK_FOR_SITE_SELECTION, + SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY + } + + enum class NavigateState { + TO_SITE_SELECTED, + TO_NO_SITE_SELECTED + } + + sealed class Action(val actionType: ActionType) { + data class NavigateToState(val navigateState: NavigateState, val siteForReblog: SiteRecord? = null) : Action( + NAVIGATE_TO_STATE + ) + + data class ContinueReblogTo(val siteForReblog: SiteRecord?) : Action( + CONTINUE_REBLOG_TO + ) + + object AskForSiteSelection : Action(ASK_FOR_SITE_SELECTION) + + object ShowJetpackIndividualPluginOverlay : Action(SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY) + } + + companion object { + private const val DELAY_BEFORE_SHOWING_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY = 500L + } } diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt index c065be764f6a..ce94b71ba180 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModel.kt @@ -41,8 +41,8 @@ import org.wordpress.android.ui.utils.UiString.UiStringText import org.wordpress.android.ui.whatsnew.FeatureAnnouncementProvider import org.wordpress.android.util.BuildConfigWrapper import org.wordpress.android.util.FluxCUtils -import org.wordpress.android.util.SiteUtils import org.wordpress.android.util.SiteUtils.hasFullAccessToContent +import org.wordpress.android.util.SiteUtilsWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.map import org.wordpress.android.util.mapNullable @@ -73,7 +73,8 @@ class WPMainActivityViewModel @Inject constructor( @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper, private val blazeFeatureUtils: BlazeFeatureUtils, - private val blazeStore: BlazeStore + private val blazeStore: BlazeStore, + private val siteUtilsWrapper: SiteUtilsWrapper ) : ScopedViewModel(mainDispatcher) { private var isStarted = false @@ -186,7 +187,7 @@ class WPMainActivityViewModel @Inject constructor( onClickAction = null ) ) - if (SiteUtils.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper)) { + if (siteUtilsWrapper.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper)) { actionsList.add( CreateAction( actionType = CREATE_NEW_STORY, @@ -269,7 +270,11 @@ class WPMainActivityViewModel @Inject constructor( _showQuickStarInBottomSheet.postValue(quickStartRepository.activeTask.value == PUBLISH_POST) - if (SiteUtils.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper) || hasFullAccessToContent(site)) { + if (siteUtilsWrapper.supportsStoriesFeature( + site, + jetpackFeatureRemovalPhaseHelper) || + hasFullAccessToContent(site) + ) { launch { // The user has at least two create options available for this site (pages and/or story posts), // so we should show a bottom sheet. @@ -355,7 +360,7 @@ class WPMainActivityViewModel @Inject constructor( } fun getCreateContentMessageId(site: SiteModel?): Int { - return if (SiteUtils.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper)) { + return if (siteUtilsWrapper.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper)) { getCreateContentMessageIdStoriesFlagOn(hasFullAccessToContent(site)) } else { getCreateContentMessageIdStoriesFlagOff(hasFullAccessToContent(site)) diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/plugins/PluginBrowserViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/plugins/PluginBrowserViewModel.kt index be200abd07fc..8290e4ad2561 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/plugins/PluginBrowserViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/plugins/PluginBrowserViewModel.kt @@ -30,6 +30,7 @@ import org.wordpress.android.ui.ListDiffCallback import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T import org.wordpress.android.util.SiteUtils +import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.viewmodel.plugins.PluginBrowserViewModel.PluginListType.FEATURED import org.wordpress.android.viewmodel.plugins.PluginBrowserViewModel.PluginListType.NEW import org.wordpress.android.viewmodel.plugins.PluginBrowserViewModel.PluginListType.POPULAR @@ -138,7 +139,7 @@ class PluginBrowserViewModel @Inject constructor( // read from the bundle return } - site = savedInstanceState.getSerializable(WordPress.SITE) as SiteModel + site = requireNotNull(savedInstanceState.getSerializableCompat(WordPress.SITE)) searchQuery = requireNotNull(savedInstanceState.getString(KEY_SEARCH_QUERY)) setTitle(savedInstanceState.getString(KEY_TITLE)) } diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListCreateMenuViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListCreateMenuViewModel.kt index 3f2f71800a46..404c79b81c5d 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListCreateMenuViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListCreateMenuViewModel.kt @@ -15,7 +15,7 @@ import org.wordpress.android.ui.main.MainActionListItem.ActionType.NO_ACTION import org.wordpress.android.ui.main.MainActionListItem.CreateAction import org.wordpress.android.ui.main.MainFabUiState import org.wordpress.android.ui.prefs.AppPrefsWrapper -import org.wordpress.android.util.SiteUtils +import org.wordpress.android.util.SiteUtilsWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.SingleLiveEvent @@ -25,7 +25,8 @@ import javax.inject.Inject class PostListCreateMenuViewModel @Inject constructor( private val appPrefsWrapper: AppPrefsWrapper, private val analyticsTracker: AnalyticsTrackerWrapper, - private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper + private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper, + private val siteUtilsWrapper: SiteUtilsWrapper ) : ViewModel() { private var isStarted = false private lateinit var site: SiteModel @@ -67,15 +68,17 @@ class PostListCreateMenuViewModel @Inject constructor( onClickAction = null ) ) - actionsList.add( - CreateAction( - actionType = CREATE_NEW_STORY, - iconRes = R.drawable.ic_story_icon_24dp, - labelRes = R.string.my_site_bottom_sheet_add_story, - onClickAction = ::onCreateActionClicked - + if (siteUtilsWrapper.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper)) { + actionsList.add( + CreateAction( + actionType = CREATE_NEW_STORY, + iconRes = R.drawable.ic_story_icon_24dp, + labelRes = R.string.my_site_bottom_sheet_add_story, + onClickAction = ::onCreateActionClicked + + ) ) - ) + } actionsList.add( CreateAction( actionType = CREATE_NEW_POST, @@ -145,7 +148,7 @@ class PostListCreateMenuViewModel @Inject constructor( } private fun getCreateContentMessageId(): Int { - return if (SiteUtils.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper)) { + return if (siteUtilsWrapper.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper)) { R.string.create_post_story_fab_tooltip } else { R.string.create_post_fab_tooltip 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 c16a0fcffce6..26b325e3aff3 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 @@ -107,7 +107,8 @@ class PostListItemUiStateHelper @Inject constructor( uploadUiState = uploadUiState, siteHasCapabilitiesToPublish = capabilitiesToPublish, statsSupported = statsSupported, - shouldRemoveJetpackFeatures = jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures(), + shouldShowStatsInJetpackRemovalPhase = + jetpackFeatureRemovalPhaseHelper.shouldShowPublishedPostStatsButton(), shouldShowPromoteWithBlaze = isSiteBlazeEligible && blazeFeatureUtils.isPostBlazeEligible( postStatus, post @@ -381,7 +382,7 @@ class PostListItemUiStateHelper @Inject constructor( uploadUiState: PostUploadUiState, siteHasCapabilitiesToPublish: Boolean, statsSupported: Boolean, - shouldRemoveJetpackFeatures: Boolean, + shouldShowStatsInJetpackRemovalPhase: Boolean, shouldShowPromoteWithBlaze: Boolean ): List { val canRetryUpload = uploadUiState is UploadFailed @@ -395,7 +396,7 @@ class PostListItemUiStateHelper @Inject constructor( postStatus == PUBLISHED && !isLocalDraft && !isLocallyChanged && - !shouldRemoveJetpackFeatures + shouldShowStatsInJetpackRemovalPhase val canShowCopy = postStatus == PUBLISHED || postStatus == DRAFT val canShowCopyUrlButton = !isLocalDraft && postStatus != TRASHED val canShowViewButton = !canRetryUpload && postStatus != TRASHED diff --git a/WordPress/src/main/res/drawable-ldrtl/ic_wordpress_jetpack_appicon.xml b/WordPress/src/main/res/drawable-ldrtl/ic_wordpress_jetpack_appicon.xml new file mode 100644 index 000000000000..c4aba7a10bbf --- /dev/null +++ b/WordPress/src/main/res/drawable-ldrtl/ic_wordpress_jetpack_appicon.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/drawable-night/browser_address_bar.xml b/WordPress/src/main/res/drawable-night/browser_address_bar.xml new file mode 100644 index 000000000000..3d0f1e47a96b --- /dev/null +++ b/WordPress/src/main/res/drawable-night/browser_address_bar.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/drawable/bg_oval_white_24dp.xml b/WordPress/src/main/res/drawable/bg_oval_white_24dp.xml deleted file mode 100644 index f76c64025d50..000000000000 --- a/WordPress/src/main/res/drawable/bg_oval_white_24dp.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/WordPress/src/main/res/drawable/browser_address_bar.xml b/WordPress/src/main/res/drawable/browser_address_bar.xml new file mode 100644 index 000000000000..ddce9484a0a4 --- /dev/null +++ b/WordPress/src/main/res/drawable/browser_address_bar.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/drawable/ic_external_v2.xml b/WordPress/src/main/res/drawable/ic_external_v2.xml new file mode 100644 index 000000000000..2c3fa058a7bf --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_external_v2.xml @@ -0,0 +1,9 @@ + + + diff --git a/WordPress/src/main/res/drawable/ic_wordpress_jetpack_appicon.xml b/WordPress/src/main/res/drawable/ic_wordpress_jetpack_appicon.xml new file mode 100644 index 000000000000..5170f147493b --- /dev/null +++ b/WordPress/src/main/res/drawable/ic_wordpress_jetpack_appicon.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout-land/site_creation_preview_screen_default.xml b/WordPress/src/main/res/layout-land/site_creation_preview_screen_default.xml index 58d67c08010f..ddea8eb76cbb 100644 --- a/WordPress/src/main/res/layout-land/site_creation_preview_screen_default.xml +++ b/WordPress/src/main/res/layout-land/site_creation_preview_screen_default.xml @@ -1,61 +1,54 @@ - - - - - - - - - - - - + + + + - + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/margin_extra_large" + android:text="@string/dialog_button_ok" + tools:ignore="InconsistentLayout" /> + + + diff --git a/WordPress/src/main/res/layout/account_settings_activity.xml b/WordPress/src/main/res/layout/account_settings_activity.xml index 990c9f094dc6..b082269a7457 100644 --- a/WordPress/src/main/res/layout/account_settings_activity.xml +++ b/WordPress/src/main/res/layout/account_settings_activity.xml @@ -1,6 +1,7 @@ @@ -24,6 +25,7 @@ android:name="org.wordpress.android.ui.prefs.accountsettings.AccountSettingsFragment" android:layout_width="match_parent" android:layout_height="match_parent" - app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:ignore="FragmentTagUsage" /> diff --git a/WordPress/src/main/res/layout/activity_log_list_activity.xml b/WordPress/src/main/res/layout/activity_log_list_activity.xml index 31a992714e5a..f2c6be50ffa1 100644 --- a/WordPress/src/main/res/layout/activity_log_list_activity.xml +++ b/WordPress/src/main/res/layout/activity_log_list_activity.xml @@ -75,7 +75,7 @@ - @@ -24,6 +25,7 @@ android:name="org.wordpress.android.ui.prefs.AppSettingsFragment" android:layout_width="match_parent" android:layout_height="match_parent" - app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:ignore="FragmentTagUsage" /> diff --git a/WordPress/src/main/res/layout/backup_download_activity.xml b/WordPress/src/main/res/layout/backup_download_activity.xml index fada92bc499d..95f209573e0d 100644 --- a/WordPress/src/main/res/layout/backup_download_activity.xml +++ b/WordPress/src/main/res/layout/backup_download_activity.xml @@ -6,13 +6,12 @@ android:layout_height="match_parent" android:orientation="vertical"> - - + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/debug_settings_activity.xml b/WordPress/src/main/res/layout/debug_settings_activity.xml index 42a3b48ebc41..7844ad35576a 100644 --- a/WordPress/src/main/res/layout/debug_settings_activity.xml +++ b/WordPress/src/main/res/layout/debug_settings_activity.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - @@ -11,18 +12,40 @@ android:gravity="start" android:layout_width="0dp" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@id/feature_enabled" + app:layout_constraintEnd_toStartOf="@id/preview_icon" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:text="Feature Title"/> + + + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible"/> + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> diff --git a/WordPress/src/main/res/layout/fragment_jetpack_security_settings.xml b/WordPress/src/main/res/layout/fragment_jetpack_security_settings.xml index 8096621df632..0adff8377655 100644 --- a/WordPress/src/main/res/layout/fragment_jetpack_security_settings.xml +++ b/WordPress/src/main/res/layout/fragment_jetpack_security_settings.xml @@ -2,6 +2,7 @@ @@ -25,6 +26,7 @@ android:id="@+id/jetpack_security_fragment" android:layout_below="@+id/appbar" android:layout_width="match_parent" - android:layout_height="match_parent" /> + android:layout_height="match_parent" + tools:ignore="FragmentTagUsage" /> diff --git a/WordPress/src/main/res/layout/insights_management_activity.xml b/WordPress/src/main/res/layout/insights_management_activity.xml index c6bb5788a170..e73fcf1e8ef0 100644 --- a/WordPress/src/main/res/layout/insights_management_activity.xml +++ b/WordPress/src/main/res/layout/insights_management_activity.xml @@ -19,7 +19,7 @@ - - - - - - - - diff --git a/WordPress/src/main/res/layout/jetpack_remote_install_fragment.xml b/WordPress/src/main/res/layout/jetpack_remote_install_fragment.xml deleted file mode 100644 index bad5e74ce1a0..000000000000 --- a/WordPress/src/main/res/layout/jetpack_remote_install_fragment.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/layout/media_picker_fragment.xml b/WordPress/src/main/res/layout/media_picker_fragment.xml index 08c3d58c0458..af4c35e2c082 100644 --- a/WordPress/src/main/res/layout/media_picker_fragment.xml +++ b/WordPress/src/main/res/layout/media_picker_fragment.xml @@ -77,7 +77,7 @@ android:visibility="gone" app:aevButton="@string/photo_picker_soft_ask_allow" app:aevImage="@drawable/img_illustration_add_media_150dp" - app:aevTitle="@string/photo_picker_soft_ask_label" + app:aevTitle="@string/photo_picker_soft_ask_photos_label" tools:visibility="visible" /> diff --git a/WordPress/src/main/res/layout/my_profile_activity.xml b/WordPress/src/main/res/layout/my_profile_activity.xml index 0e67129003de..25501df9f67e 100644 --- a/WordPress/src/main/res/layout/my_profile_activity.xml +++ b/WordPress/src/main/res/layout/my_profile_activity.xml @@ -19,7 +19,7 @@ - + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/my_site_activity_card_with_activity_items.xml b/WordPress/src/main/res/layout/my_site_activity_card_with_activity_items.xml new file mode 100644 index 000000000000..54311e525aff --- /dev/null +++ b/WordPress/src/main/res/layout/my_site_activity_card_with_activity_items.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/my_site_pages_card_footer_link.xml b/WordPress/src/main/res/layout/my_site_pages_card_footer_link.xml new file mode 100644 index 000000000000..62a0dca71d85 --- /dev/null +++ b/WordPress/src/main/res/layout/my_site_pages_card_footer_link.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/WordPress/src/main/res/layout/my_site_pages_card_with_page_items.xml b/WordPress/src/main/res/layout/my_site_pages_card_with_page_items.xml new file mode 100644 index 000000000000..455f1a9ced66 --- /dev/null +++ b/WordPress/src/main/res/layout/my_site_pages_card_with_page_items.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/my_site_single_action_card_item.xml b/WordPress/src/main/res/layout/my_site_single_action_card_item.xml index 37389c1b54ef..d6bdb59edc29 100644 --- a/WordPress/src/main/res/layout/my_site_single_action_card_item.xml +++ b/WordPress/src/main/res/layout/my_site_single_action_card_item.xml @@ -19,7 +19,6 @@ android:layout_marginStart="@dimen/margin_extra_large" android:layout_marginTop="@dimen/margin_extra_large" android:contentDescription="@null" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:src="@drawable/ic_wordpress_blue_32dp" /> @@ -30,14 +29,31 @@ android:layout_height="wrap_content" android:layout_marginEnd="@dimen/single_action_card_padding" android:layout_marginStart="@dimen/single_action_card_padding" + android:layout_marginTop="@dimen/margin_extra_large" android:textColor="?attr/colorOnSurface" android:textSize="@dimen/text_sz_large" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/learn_more" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/single_action_card_image" app:layout_constraintTop_toTopOf="parent" tools:text="@string/jp_migration_success_card_message" /> + + - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/pages_item.xml b/WordPress/src/main/res/layout/pages_item.xml new file mode 100644 index 000000000000..0b002a57377f --- /dev/null +++ b/WordPress/src/main/res/layout/pages_item.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/WordPress/src/main/res/layout/pages_parent_activity.xml b/WordPress/src/main/res/layout/pages_parent_activity.xml index c30ca54440bd..edabf6d60ccd 100644 --- a/WordPress/src/main/res/layout/pages_parent_activity.xml +++ b/WordPress/src/main/res/layout/pages_parent_activity.xml @@ -7,7 +7,7 @@ - - diff --git a/WordPress/src/main/res/layout/publicize_webview_fragment.xml b/WordPress/src/main/res/layout/publicize_webview_fragment.xml index e90669d4d793..58b7849cd32b 100644 --- a/WordPress/src/main/res/layout/publicize_webview_fragment.xml +++ b/WordPress/src/main/res/layout/publicize_webview_fragment.xml @@ -1,15 +1,11 @@ - - - - @@ -23,5 +19,4 @@ android:visibility="gone" tools:visibility="visible" /> - - + diff --git a/WordPress/src/main/res/layout/reader_interests_activity.xml b/WordPress/src/main/res/layout/reader_interests_activity.xml index 9e509548397f..92a505ba1917 100644 --- a/WordPress/src/main/res/layout/reader_interests_activity.xml +++ b/WordPress/src/main/res/layout/reader_interests_activity.xml @@ -20,7 +20,7 @@ - - - + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - - + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/site_creation_header_v2.xml b/WordPress/src/main/res/layout/site_creation_header_v2.xml index 9c11635d10a1..9fe0d4499cd1 100644 --- a/WordPress/src/main/res/layout/site_creation_header_v2.xml +++ b/WordPress/src/main/res/layout/site_creation_header_v2.xml @@ -9,8 +9,6 @@ diff --git a/WordPress/src/main/res/layout/site_creation_preview_header_item.xml b/WordPress/src/main/res/layout/site_creation_preview_header_item.xml index a7ff0f72a336..4c7fb21ea0db 100644 --- a/WordPress/src/main/res/layout/site_creation_preview_header_item.xml +++ b/WordPress/src/main/res/layout/site_creation_preview_header_item.xml @@ -6,11 +6,17 @@ + style="@style/SiteCreationHeaderV2Title" + android:paddingBottom="@dimen/margin_large" + android:text="@string/new_site_creation_preview_title" + app:fixWidowWords="true" + app:autoSizeTextType="uniform" /> diff --git a/WordPress/src/main/res/layout/site_creation_preview_screen_default.xml b/WordPress/src/main/res/layout/site_creation_preview_screen_default.xml index 987ec9ebd96b..8216a70c0d9b 100644 --- a/WordPress/src/main/res/layout/site_creation_preview_screen_default.xml +++ b/WordPress/src/main/res/layout/site_creation_preview_screen_default.xml @@ -2,66 +2,57 @@ + android:id="@+id/site_creation_preview_header_item" + layout="@layout/site_creation_preview_header_item" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_marginHorizontal="@dimen/margin_extra_large" + android:layout_marginVertical="@dimen/margin_large" /> - - + android:layout_height="match_parent" + android:layout_above="@+id/sitePreviewCaption" + android:layout_below="@id/site_creation_preview_header_item" + android:layout_marginHorizontal="@dimen/margin_extra_large" /> - + - + - - - - - + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginEnd="@dimen/margin_extra_large" + android:layout_marginStart="@dimen/margin_extra_large" + android:text="@string/dialog_button_ok" /> + diff --git a/WordPress/src/main/res/layout/site_creation_preview_web_view_container.xml b/WordPress/src/main/res/layout/site_creation_preview_web_view_container.xml index 42d13e614ed6..2fd9458e67c0 100644 --- a/WordPress/src/main/res/layout/site_creation_preview_web_view_container.xml +++ b/WordPress/src/main/res/layout/site_creation_preview_web_view_container.xml @@ -6,8 +6,10 @@ style="@style/Widget.MaterialComponents.CardView" android:layout_width="match_parent" android:layout_height="match_parent" - app:cardCornerRadius="0dp" - app:cardElevation="@dimen/card_elevation"> + app:cardCornerRadius="@dimen/margin_large" + app:cardElevation="@dimen/card_elevation" + app:strokeColor="@color/site_creation_preview_card_border" + app:strokeWidth="@dimen/margin_extra_extra_small"> + + + + + + diff --git a/WordPress/src/main/res/layout/site_settings_categories_list_activity.xml b/WordPress/src/main/res/layout/site_settings_categories_list_activity.xml index 1f75eaea54f5..34228ae829b1 100644 --- a/WordPress/src/main/res/layout/site_settings_categories_list_activity.xml +++ b/WordPress/src/main/res/layout/site_settings_categories_list_activity.xml @@ -5,7 +5,7 @@ android:layout_height="match_parent" android:orientation="vertical"> - @@ -17,7 +18,8 @@ android:layout_height="match_parent" android:layout_below="@id/toolbar" app:layout_behavior="@string/appbar_scrolling_view_behavior" - app:viewType="allTime" /> + app:viewType="allTime" + tools:ignore="FragmentTagUsage" /> diff --git a/WordPress/src/main/res/layout/stats_minified_widget_configure_activity.xml b/WordPress/src/main/res/layout/stats_minified_widget_configure_activity.xml index 2b833d70c3c0..bc4f6a3414f7 100644 --- a/WordPress/src/main/res/layout/stats_minified_widget_configure_activity.xml +++ b/WordPress/src/main/res/layout/stats_minified_widget_configure_activity.xml @@ -10,7 +10,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - @@ -17,7 +18,8 @@ android:layout_height="match_parent" android:layout_below="@id/toolbar" app:layout_behavior="@string/appbar_scrolling_view_behavior" - app:viewType="today" /> + app:viewType="today" + tools:ignore="FragmentTagUsage" /> diff --git a/WordPress/src/main/res/layout/stats_views_widget_configure_activity.xml b/WordPress/src/main/res/layout/stats_views_widget_configure_activity.xml index f4084046260f..f7f818595a5b 100644 --- a/WordPress/src/main/res/layout/stats_views_widget_configure_activity.xml +++ b/WordPress/src/main/res/layout/stats_views_widget_configure_activity.xml @@ -1,6 +1,7 @@ @@ -17,7 +18,8 @@ android:layout_height="match_parent" android:layout_below="@id/toolbar" app:layout_behavior="@string/appbar_scrolling_view_behavior" - app:viewType="views" /> + app:viewType="views" + tools:ignore="FragmentTagUsage" /> diff --git a/WordPress/src/main/res/layout/stats_week_views_widget_configure_activity.xml b/WordPress/src/main/res/layout/stats_week_views_widget_configure_activity.xml index ed412c0d808a..8afecb9857f3 100644 --- a/WordPress/src/main/res/layout/stats_week_views_widget_configure_activity.xml +++ b/WordPress/src/main/res/layout/stats_week_views_widget_configure_activity.xml @@ -1,6 +1,7 @@ @@ -17,7 +18,8 @@ android:layout_height="match_parent" android:layout_below="@id/toolbar" app:layout_behavior="@string/appbar_scrolling_view_behavior" - app:viewType="week" /> + app:viewType="week" + tools:ignore="FragmentTagUsage" /> diff --git a/WordPress/src/main/res/layout/threat_details_activity.xml b/WordPress/src/main/res/layout/threat_details_activity.xml index 0f43db6e04b0..eaf849fac216 100644 --- a/WordPress/src/main/res/layout/threat_details_activity.xml +++ b/WordPress/src/main/res/layout/threat_details_activity.xml @@ -7,7 +7,7 @@ android:layout_height="match_parent" android:orientation="vertical"> - + + + diff --git a/WordPress/src/main/res/values-ar/strings.xml b/WordPress/src/main/res/values-ar/strings.xml index 058977079c6a..6b2161373a86 100644 --- a/WordPress/src/main/res/values-ar/strings.xml +++ b/WordPress/src/main/res/values-ar/strings.xml @@ -1,11 +1,89 @@ + إزالة المكوّنات + الخصوصية والتصنيف + إعدادات التشغيل + لون شريط التشغيل + يدوي + ديناميكية + صف غرض الصورة. اترك ذلك الحقل فارغًا إذا كان للزخرفة. + البدء بتخطيطات معدة حسب الطلب ومناسبة للهواتف المحمولة + إنشاء صفحة أخرى + إضافة صفحات إلى موقعك + إخفاء هذا + شارك مطالبتك في زاوية الويب مع عنوان الموقع الذي يسهل العثور عليه ومشاركته ومتابعته. + امتلاك هويتك على الإنترنت مع نطاق مخصص + لاستخدام تذكيرات التدوين، سيتعيَّن عليك تشغيل التنبيهات المؤقتة. + تشغيل التنبيهات المؤقتة + الاستمرار في النطاف الفرعي + شراء النطاق + الصور والفيديوهات والموسيقى والملفات الصوتية + الموسيقى والملفات الصوتية + الصور والفيديوهات + يحتاج %s إلى صلاحية للوصول إلى ملفاتك الصوتية + يحتاج %s إلى صلاحية للوصول إلى فيديوهاتك + يحتاج %s إلى صلاحية للوصول إلى صورك + يحتاج %s إلى صلاحية للوصول إلى صورك وفيديوهاتك + يحتاج %s إلى صلاحية للوصول إلى الموسيقى والملفات الصوتية والصور والفيديوهات الخاصة بك + تشغيل التنبيهات + انتقل إلى الإعدادات ← التنبيهات ← إعدادات التطبيق، وشغّل ⁦%1$s⁩ لتلقي الإخطارات على الفور. + إصلاح + سيتعين عليك فتح التطبيق للاطلاع على التنبيهات. + تم إيقاف تشغيل تنبيهات الدفع + تم إيقاف تشغيل تنبيهات الدفع. + تجاهَل تحذير صلاحية التنبيه. + تستخدم <b>%1$s</b> ⁦%2$s⁩ من إضافات Jetpack الفردية + تستخدم <b>%1$s</b> إضافة <b>%2$s</b> + المواقع ذات إضافات Jetpack الفردية غير مدعومة من تطبيق ووردبريس. + تستخدم <b>%1$s</b> إضافات Jetpack الفردية، غير المدعومة من تطبيق ووردبريس. + تستخدم <b>%1$s</b> إضافة <b>%2$s</b>، غير المدعومة من تطبيق ووردبريس. + يتعذر الوصول إلى بعض مواقعك + يتعذر الوصول إلى أحد مواقعك + يرجى التبديل إلى تطبيق Jetpack، حيث سنرشدك خلال الاتصال بإضافة Jetpack الكاملة لاستخدام هذا الموقع مع التطبيق. + التبديل إلى تطبيق Jetpack + يستخدم ⁦%1$s⁩ ⁦%2$s⁩، التي لا تدعم كل ميزات التطبيق حتى الآن.\n\nيرجى تثبيت ⁦%3$s⁩ لاستخدام التطبيق من خلال هذا الموقع. + هذا الموقع + يستخدم ⁦%1$s⁩ ⁦%2$s⁩، التي لا تدعم كل ميزات التطبيق حتى الآن. Please install the %3$s. + يستخدم ⁦%1$s⁩ ⁦%2$s⁩، التي لا تدعم كل ميزات التطبيق حتى الآن. Please install the %3$s. + Moving to the Jetpack app in a few days. + التبديل مجاني، ولا يستغرق سوى بضع دقائق. + تمت إزالة ميزات الإحصاءات والقارئ والتنبيهات وغيرها من الميزات التي يدعمها Jetpack من تطبيق ووردبريس. + معرفة المزيد على jetpack.com + التبديل إلى تطبيق Jetpack + %s have moved to the Jetpack app. + %s has moved to the Jetpack app. + مسؤول ووردبريس + إدارة المحتويات + حركة المرور + المحتوى + إعداد + تم + والآن بعد تثبيت Jetpack، ما علينا سوى مساعدتك على الإعداد. لن يستغرق هذا إلا دقيقة. + إبراز تدوينة الآن + إبراز هذه الصفحة + إبراز هذه التدوينة + تعقّب الأداء، وابدأ Blaze الخاصة بك وأوقفها في أي وقت. + سيظهر محتواك على ملايين من مواقع ووردبريس وTumblr. + روّج لأي تدوينة أو صفحة في غضون دقائق قليلة فقط مقابل بضعة دولارات فقط في اليوم. + جذب مزيد من حركة المرور إلى موقعك باستخدام Blaze + التوهج + هذا النطاق مسجّل بالفعل + تخفيض + موصى به + أفضل بديل + مساعدة + ارجع إلى قسم الأسئلة المتداولة لدينا للحصول على إجابات عن الأسئلة الشائعة التي قد تطرحها. + شكرًا لك للتبديل إلى تطبيق Jetpack! + السجلات + التذاكر + مجانًا + مساعدة قائمة المكوّنات إخفاء هذا اعرض عملك عبر ملايين المواقع. @@ -18,24 +96,23 @@ Language: ar إضافة Jetpack الكاملة إضافات Jetpack الفردية إضافة ⁦%1$s⁩ - يستخدم ⁦%1$s⁩ ⁦%2$s⁩، التي لا تدعم كل ميزات التطبيق حتى الآن.\n\nيرجى تثبيت ⁦%3$s⁩ لاستخدام التطبيق من خلال هذا الموقع. + يستخدم ⁦%1$s⁩ ⁦%2$s⁩، التي لا تدعم كل ميزات التطبيق حتى الآن.\n\nيرجى تثبيت ⁦%3$s⁩ لاستخدام التطبيق من خلال هذا الموقع. يرجى تثبيت إضافة Jetpack الكاملة لا يتوافر سوى موقع واحد، لذا يتعذر عليك تغيير موقعك الأساسي. - يستخدم هذا الموقع إضافة فردية، لا تدعم كل ميزات التطبيق حتى الآن. يرجى تثبيت إضافة Jetpack الكاملة. - الاتصال بالدعم - إعادة المحاولة - يتعذر تنصيب Jetpack في الوقت الحالي. - حدثت مشكلة - أيقونة الخطأ - تم - مستعد لاستخدام هذا الموقع من خلال التطبيق. - تم تثبيت Jetpack - تثبيت Jetpack على موقعك. قد يستغرق هذا الأمر بضع دقائق حتى يكتمل. - تثبيت Jetpack - متابعة - لن يتم تخزين بيانات اعتمادك وستُستخدم فقط بغرض تنصيب Jetpack. - أيقونة Jetpack + الاتصال بالدعم + إعادة المحاولة + يتعذر تنصيب Jetpack في الوقت الحالي. + حدثت مشكلة + أيقونة الخطأ + مستعد لاستخدام هذا الموقع من خلال التطبيق. + تم تثبيت Jetpack + تثبيت Jetpack على موقعك. قد يستغرق هذا الأمر بضع دقائق حتى يكتمل. + تثبيت Jetpack + متابعة + لن يتم تخزين بيانات اعتمادك وستُستخدم فقط بغرض تنصيب Jetpack. + أيقونة Jetpack الترويج مع الإبراز + تثبيت Jetpack افتح إمكانات موقعك بالكامل. احصل على ميزات الإحصاءات والتنبيهات والمزيد من خلال Jetpack. يتضمن موقعك إضافة Jetpack تم تصميم تطبيق Jetpack للهاتف المحمول للعمل إلى جانب إضافة Jetpack. قم بالتبديل الآن لكي تحظى بإمكانية الوصول إلى ميزات الإحصاءات والتنبيهات والقارئ والمزيد. @@ -63,6 +140,7 @@ Language: ar ستنتقل ⁦%1$s⁩ في غضون ⁦%2$s⁩ ستنتقل ⁦%1$s⁩ قريبًا سينتقل ⁦%1$s⁩ قريبًا + سينتقل ⁦%1$s⁩ في غضون ⁦%2$s⁩ تنزيل تطبيق Jetpack عرض كل الردود ⁦%1$s⁩ أقل من الأيام الـ 7 السابقة @@ -82,6 +160,7 @@ Language: ar معرفة المزيد على jetpack.com التبديل مجاني، ولا يستغرق سوى بضع دقائق. ستتم إزالة ميزات الإحصاءات والقارئ والتنبيهات وغيرها من الميزات التي يدعمها Jetpack من تطبيق ووردبريس قريبًا. + ستنتقل ميزات الإحصاءات والقارئ والتنبيهات وغيرها من الميزات قريبًا إلى تطبيق Jetpack للهاتف المحمول. ستتم إزالة ميزات الإحصاءات والقارئ والتنبيهات وغيرها من الميزات التي يدعمها Jetpack من تطبيق ووردبريس في %s. ستنتقل ميزات Jetpack قريبًا. ستنتقل ميزة التنبيهات إلى Jetpack @@ -129,9 +208,6 @@ Language: ar فتح الروابط في Jetpack هل تحتاج إلى مساعدة؟ فهمت - يرجى <b>حذف تطبيق ووردبريس</b> لتفادي تعارضات البيانات. - يبدو أن تطبيق ووردبريس لا يزال مثبتًا لديك. نوصي بأن تحذف تطبيق ووردبريس لتفادي تعارضات البيانات. - لن تحتاج إلى تطبيق ووردبريس بعد الآن يتعذر علينا نقل بياناتك وإعداداتك من دون اتصال الشبكة. يرجى التحقق للتأكد من أن اتصال الشبكة لديك قيد العمل، وحاول مجددًا. يتعذر الاتصال بالإنترنت. @@ -141,13 +217,11 @@ Language: ar المحاولة مجددًا إنهاء إزالة أيقونة تطبيق ووردبريس - يرجى <b>حذف تطبيق ووردبريس</b> لتفادي تعارضات البيانات. لقد نقلنا كل بياناتك وإعداداتك. يصبح كل شيء صحيحًا عندما تتركه. شكرًا على التبديل إلى Jetpack! سنقوم بإيقاف تشغيل التنبيهات من تطبيق ووردبريس. ستتلقى كل التنبيهات نفسها ولكنها ستأتي الآن من تطبيق Jetpack. تأتي التنبيهات الآن من Jetpack - يرجى حذف تطبيق ووردبريس مركز مساعدة ووردبريس دعم يسمح للتطبيق بتعطيل تنبيهات ووردبريس. @@ -727,6 +801,7 @@ Language: ar GIF واحد لا توجد معاينة متاحة + أضف عنواناً لون النص الهوامش الداخلية مميز @@ -734,6 +809,7 @@ Language: ar إنشاء تضمين رابط (URL) مخصّص عمود %d + المزيد قدّم وصفاً موجزًا للرابط لمساعدة مستخدم قارئ الشاشة إضافة مكوِّنات لم يتم العثور على أي مواقع Jetpack @@ -1150,7 +1226,6 @@ Language: ar مقدمة لمقالات القصص تم إنشاء صفحة فارغة تم إنشاء صفحة - ⁦%1$s⁩ تم رفض الوصول إلى صورك. لإصلاح هذا، حرِّر صلاحياتك وشغِّل ⁦%2$s⁩ و⁦%3$s⁩. فشل إدراج الوسائط. فشل إدراج الوسائط: %s الاختيار من مكتبة وسائط ووردبريس @@ -1630,6 +1705,7 @@ Language: ar إضافة مكوّن هنا حدث خطأ غير معروف. يُرجى المحاولة مرة أخرى. إضافة نصّ بديل للرابط + إضافة وصف \"تم تحميل القائمة بعناصر %1$d.\" أنقر فوق الزرّ \"حفظ المقالات\" لحفظ مقالةٍ في قائمتك. إيقاف التشغيل @@ -1831,7 +1907,6 @@ Language: ar اجتماعي إحصاءات الموقع السنوية تسجيل نطاق - والآن بعد تثبيت Jetpack، ما علينا سوى مساعدتك على الإعداد. لن يستغرق هذا إلا دقيقة. تعذّر تحميل اقتراحات النطاق كتابة كلمة مفتاحية للحصول على مزيد من الأفكار لم يتم العثور على أي اقتراحات @@ -2100,18 +2175,9 @@ Language: ar المزيد أضف موضوعًا هنا للعثور على مقالات حول موضوعاتك المفضلة لا توجد مواضيع تمت متابعتها - إعادة المحاولة - حدثت مشكلة Jetpack الأسئلة الشائعة عن Jetpack - تمّ تنصيب Jetpack - تنصيب Jetpack - تنصيب Jetpack سجّل الدخول إلى حساب WordPress.com الذي تستخدمه للاتصال بـ Jetpack. - يتعذر تنصيب Jetpack في الوقت الحالي. - تنصيب Jetpack على موقعك. قد يستغرق هذا الأمر وقتًا يصل إلى بضع دقائق لاكتماله. - لن يتم تخزين بيانات اعتمادك وستُستخدم فقط بغرض تنصيب Jetpack. - الإعداد لاستخدام الإحصاءات على موقع ووردبريس الخاص بك، سيتعين عليك تنصيب إضافة Jetpack. لا توجد سمات مطابقة لبحثك ما الذي ترغب في إيجاده؟ @@ -2663,7 +2729,7 @@ Language: ar المستندات الصور الكل - تم رفض وصول %1$s إلى صورك. لإصلاح هذا، حرر أذوناتك وشغّل %2$s. + تم رفض وصول ⁦%1$s⁩ إلى ملفات الوسائط الاجتماعية الخاصة بك. لإصلاح ذلك، حرّر صلاحياتك وشغِّل ⁦%2$s⁩. عرض التعليقات جودة الفيديوهات تُشير القيم الأعلى إلى الفيديوهات ذات الجودة الأفضل. تغيير حجم الفيديوهات في المقالات إلى هذا الحجم diff --git a/WordPress/src/main/res/values-bg/strings.xml b/WordPress/src/main/res/values-bg/strings.xml index cfe0c02f2d94..2006c2bbdd22 100644 --- a/WordPress/src/main/res/values-bg/strings.xml +++ b/WordPress/src/main/res/values-bg/strings.xml @@ -154,7 +154,6 @@ Language: bg Всички Аудио Видео файлове - Достъпът на %1$s до вашите снимки беше ограничен. За да поправите това редактирайте вашите правомощия и включете %2$s. Преглед на коментарите Променя размера на видеоклиповете в публикациите до този Включва преоразмеряването и компресията на видеото diff --git a/WordPress/src/main/res/values-cs/strings.xml b/WordPress/src/main/res/values-cs/strings.xml index 0a46ed1f7225..992c8e31b2e1 100644 --- a/WordPress/src/main/res/values-cs/strings.xml +++ b/WordPress/src/main/res/values-cs/strings.xml @@ -1,13 +1,105 @@ + <b>%1$s</b> používá %2$s jednotlivé pluginy Jetpack + <b>%1$s</b> používá plugin <b>%2$s</b> + Aplikace WordPress nepodporuje weby s jednotlivými pluginy Jetpack. + <b>%1$s</b> používá jednotlivé pluginy Jetpack, které aplikace WordPress nepodporuje. + <b>%1$s</b> používá plugin <b>%2$s</b>, který aplikace WordPress nepodporuje. + Nelze získat přístup k některým z vašich stránek + Nelze získat přístup k jednomu z vašich stránek + Přepněte prosím do aplikace Jetpack, kde vás provedeme připojením úplného pluginu Jetpack, abyste mohli používat tuto stránku s aplikací. + Přepněte do aplikace Jetpack + %1$s používá aplikaci %2$s, která zatím nepodporuje všechny funkce aplikace.\nChcete-li aplikaci používat s tímto webem, nainstalujte si prosím %3$s. + Tento web + %1$s používá aplikaci %2$s, která zatím nepodporuje všechny funkce aplikace. Nainstalujte prosím %3$s. + %1$s používá aplikaci %2$s, která zatím nepodporuje všechny funkce aplikace. Nainstalujte %3$s. + Přesun do aplikace Jetpack za pár dní. + Přepínání je zdarma a trvá jen minutu. + Statistiky, čtečka, upozornění a další funkce poháněné Jetpack byly z aplikace WordPress odstraněny a nyní je lze nalézt pouze v aplikaci Jetpack. + Více se dozvíte na Jetpack.com + Přepněte do aplikace Jetpack + %s se přesunuly do aplikace Jetpack. + %s se přesunul do aplikace Jetpack. + WP Admin + Správa + Návštěvnost + Obsah + Nastavit + Hotovo + Nyní, když je nainstalován Jetpack, musíme vás ho nechat nastavit. Bude to trvat jen minutu. + Nyní zvýrazněte příspěvek + Zvýrazněte tuto stránku + Zvýrazněte tento příspěvek + Sledujte výkon, spusťte a zastavte svůj Blaze kdykoli. + Váš obsah se objeví na milionech webů WordPress a Tumblr. + Propagujte jakýkoli příspěvek nebo stránku během několika minut za pouhých pár dolarů denně. + Přiveďte na svůj web větší návštěvnost se zvýrazněním + Zvýraznění + Tato doména je již registrována + Prodej + Doporučeno + Nejlepší alternativa + Nápověda + Odpovědi na běžné otázky, které můžete mít, najdete v našich FAQ. + Děkujeme, že jste přešli na aplikaci Jetpack! + Protokoly + Vstupenky + Zdarma + Nápověda Zavřít Kontaktovat podporu + Menu bloků + Skrýt toto + Zobrazte svou práci na milionech webů. + Propagujte svůj zvýrazněný obsah + Nainstalujte celý plugin + Pravidla a podmínky + Nastavením Jetpacku souhlasíte s našimi + plný plugin Jetpack + individuální Jetpack pluginy + plugin %1$s + %1$s používá aplikaci %2$s, která zatím nepodporuje všechny funkce aplikace.\nChcete-li aplikaci používat s tímto webem, nainstalujte si prosím %3$s. + Ikona chyby + Nainstalujte si celý Jetpack plugin + K dispozici je pouze jeden web, takže nemůžete změnit svůj primární web. + Kontaktujte podporu + Znovu + Jetpack v tuto chvíli nelze nainstalovat. + Vyskytl se problém + Připraveno k použití tohoto webu s aplikací. + Jetpack nainstalován + Instalace Jetpack na vaše stránky. To může trvat až několik minut. + Instalování Jetpacku + Pokračovat + Pověření na vašem webu nebudou uložena a budou použita pouze za účelem instalace Jetpack. + Instalovat Jetpack + Ikona Jetpack + Propagujte zvýrazněním + Odemkněte plný potenciál svého webu. Získejte statistiky, upozornění a další s Jetpack. + Vaše stránky mají plugin Jetpack + Mobilní aplikace Jetpack je navržena tak, aby fungovala společně s pluginem Jetpack. Přepněte nyní a získejte přístup ke statistikám, oznámením, čtečce a dalším. + Dostávejte upozornění na nové komentáře, hodnocení Líbí se mi, zhlédnutí a další. + Najděte a sledujte své oblíbené stránky a komunity a sdílejte svůj obsah. + Sledujte, jak vaše návštěvnost roste, pomocí užitečných statistik a komplexních statistik. + Statistiky a postřehy + Jetpack vám umožní udělat více s vaším webem WordPress. Přepínání je zdarma a trvá jen minutu. + Dejte WordPressu podporu s Jetpackem + Předvolby pro návrhy a připomenutí psaní blogu můžete kdykoli nastavit v části Můj web > Nastavení > Blogování + Oznámení bude obsahovat slovo nebo krátkou frázi pro inspiraci + Navštivte <b>nastavení webu</b> a znovu jej zapněte + Výzvy k blogování jsou skryté + Vypněte výzvy + Získejte pomoc od naší skupiny dobrovolníků. + Komunitní fóra + Připomenutí blogů + Zobrazit výzvy + Blogování Chcete-li získat aplikaci Jetpack, nainstalujte si obchod Google Play Odložit na později Přejděte na Jetpack @@ -37,6 +129,7 @@ Language: cs_CZ Přepínání je zdarma a trvá jen minutu. Statistiky, čtečka, upozornění a další funkce poháněné Jetpack budou z aplikace WordPress brzy odstraněny. Statistiky, čtečka, upozornění a další funkce poháněné Jetpack budou z aplikace WordPress odstraněny na %s. + Statistiky, čtečka, upozornění a další funkce se brzy přesunou do mobilní aplikace Jetpack. Funkce Jetpacku se brzy přesunou. Oznámení se přesouvají do Jetpacku Čtečka se přesouvá do aplikace Jetpack @@ -83,9 +176,6 @@ Language: cs_CZ Otevřít odkazy v Jetpacku Potřebujete pomoci? Mám to - <b>Smažte aplikaci WordPress</b>, abyste předešli konfliktům dat. - Už nepotřebujete WordPress aplikaci - Vypadá to, že máte stále nainstalovanou aplikaci WordPress. Doporučujeme smazat WordPress aplikaci, abyste předešli konfliktům dat. Bez připojení k síti nemůžeme přenést vaše data a nastavení. Zkontrolujte, zda vaše síťové připojení funguje, a zkuste to znovu. Nelze se připojit k internetu. @@ -95,7 +185,6 @@ Language: cs_CZ Zkuste to znovu Dokončit Odstranit ikonu aplikace WordPress - <b>Smažte aplikaci WordPress</b>, abyste předešli konfliktům dat. Přenesli jsme všechna vaše data a nastavení. Všechno je tam, kde jsi to nechal. Děkujeme, že jste přešli na Jetpack! Budete dostávat všechna stejná oznámení, ale nyní budou pocházet z aplikace Jetpack. @@ -105,7 +194,6 @@ Language: cs_CZ Podpora Umožňuje aplikaci zakázat WordPress oznámení. zakázat WordPress oznámení - Odstraňte WordPress aplikaci Potřebujete pomoci? Pokračovat Našli jsme vaše stránky. Pokračujte v přenosu všech dat a automaticky se přihlaste k Jetpacku. @@ -313,6 +401,8 @@ Language: cs_CZ Nejlepší způsob, jak se stát lepším spisovatelem, je vybudovat si návyk na psaní a sdílet ho s ostatními – to je místo, kde přicházejí výzvy! Nastavit připomenutí Publikování pravidelně přitahuje nové čtenáře. Řekněte nám, kdy chcete psát, a my vám zašleme připomenutí! + Představujeme\nVýzvy k blogování + Zahrňte výzvu k blogování Psaní poezie Cestování Technologie @@ -618,6 +708,7 @@ Language: cs_CZ Spravujte rubriky svého webu Rubriky Obsah stránky s nejnovějšími příspěvky je generován automaticky a nelze jej upravovat. + Připomenutí Nastavení okrajů Neptat se znovu Zobrazit úložiště @@ -685,6 +776,7 @@ Language: cs_CZ Vlastní URL Vytvořit vložení Sloupec %d + Více Stručně popište odkaz, abyste pomohli uživatelům s čtečkou obrazovky. Přidat bloky Nebyly nalezeny žádné stránky Jetpack @@ -937,8 +1029,8 @@ Language: cs_CZ Nejsou k dispozici žádné návrhy %s. Při načítání návrhů došlo k problému. Bez shody %s. - Jetpack Scan nemůže tuto hrozbu automaticky opravit.\n Doporučujeme hrozbu vyřešit ručně: ujistěte se, že WordPress, vaše téma a všechny vaše doplňky jsou aktuální, a odstraňte z webu nevhodný kód, téma nebo plugin.\n \n\n Pokud potřebujete další pomoc při řešení této hrozby, doporučujeme <b>Codeable+</b>, důvěryhodné tržiště nezávislých odborníků s vysoce prověřenými odborníky na WordPress.\n Identifikovali vybranou skupinu bezpečnostních odborníků, kteří s těmito projekty pomohli. Ceny se pohybují od 70–120 USD / hodinu a můžete získat bezplatný odhad bez povinnosti najmout. Jetpack Scan nahradí ovlivněný soubor nebo adresář. + Jetpack Scan nemůže tuto hrozbu automaticky opravit.\n Doporučujeme hrozbu vyřešit ručně: ujistěte se, že WordPress, vaše téma a všechny vaše doplňky jsou aktuální, a odstraňte z webu nevhodný kód, téma nebo plugin.\n \n\n Pokud potřebujete další pomoc při řešení této hrozby, doporučujeme <b>Codeable+</b>, důvěryhodné tržiště nezávislých odborníků s vysoce prověřenými odborníky na WordPress.\n Identifikovali vybranou skupinu bezpečnostních odborníků, kteří s těmito projekty pomohli. Ceny se pohybují od 70–120 USD / hodinu a můžete získat bezplatný odhad bez povinnosti najmout. Jaký byl problém? Technické podrobnosti V souboru nalezena hrozba: @@ -1096,7 +1188,6 @@ Language: cs_CZ Zkombinujte fotografie, videa a text a vytvořte poutavé příběhy příspěvků, na které lze klepnout, které se vašim návštěvníkům budou líbit. Příběhový příspěvek nezmizí Stránka byla vytvořena - %1$s byl odepřen přístup k vašim fotkám. Chcete-li to opravit, upravte svá oprávnění a zapněte %2$s a %3$s. Prázdná stránka vytvořena Představujeme příběhy Jak vytvořit příběhový příspěvek @@ -1581,6 +1672,7 @@ Language: cs_CZ Alternativní text Nastala neznámá chyba. Prosím zkuste to znovu. Přidejte alternativní text + Přidat popis \"Seznam má načteno %1$d položek.\" Klepnutím na tlačítko Přidat k uložení příspěvků uložíte příspěvek do svého seznamu. Vypnutím oznámení pro tento web zakážete zobrazování oznámení na kartě oznámení pro tento web. Po zapnutí oznámení pro tento web můžete doladit, jaký druh oznámení se vám zobrazí. @@ -1777,7 +1869,6 @@ Language: cs_CZ Nahrávám koncept Koncepty Při obnovování příspěvku došlo k chybě - Nyní, když je nainstalován Jetpack, musíme vás ho nechat nastavit. Bude to trvat jen minutu. Registrovat doménu Nebyly nalezeny žádné návrhy Napište klíčové slovo pro více nápadů @@ -2053,15 +2144,6 @@ Language: cs_CZ Nemáte žádné stránky Jetpack Jetpack FAQ - Instalovat Jetpack - Odpověď - Pokračovat - Instalování Jetpacku - Vyskytl se problém - Jetpack nainstalován - Pověření na vašem webu nebudou uložena a budou použita pouze za účelem instalace Jetpack. - Instalace Jetpack na vaše stránky. To může trvat až několik minut. - Jetpack v tuto chvíli nelze nainstalovat. Přihlaste se k účtu WordPress.com, který jste použili k připojení Jetpack. K zobrazení statistik je třeba mít nainstalovaný Jetpack plugin. Žádné šablony neodpovídají hledanému výrazu @@ -2614,7 +2696,7 @@ Language: cs_CZ Dokumenty Videa Obrázky - %1$s byl odepřen přístup k vašim fotografiím. Chcete-li to vyřešit, upravte oprávnění a zapněte %2$s. + %1$s byl odepřen přístup k vašim fotografiím. Chcete-li to vyřešit, upravte oprávnění a zapněte %2$s. Zobrazit komentáře Kvalita videí. Vyšší hodnoty znamenají lepší kvalitu videí. Změní velikost videí v příspěvcích na tuto velikost diff --git a/WordPress/src/main/res/values-de/strings.xml b/WordPress/src/main/res/values-de/strings.xml index 04cbf0b29d64..474bf3e18283 100644 --- a/WordPress/src/main/res/values-de/strings.xml +++ b/WordPress/src/main/res/values-de/strings.xml @@ -1,14 +1,89 @@ + Blöcke entfernen + Datenschutz und Bewertung + Dynamisch + Seiten zu deiner Website hinzufügen + Weitere Seite erstellen + Wiedergabeeinstellungen + Farbe der Wiedergabeleiste + Manuell + Beschreibe den Zweck des Bildes. Freilassen falls dekorativ. + Beginne mit maßgeschneiderten, für Mobilgeräte geeigneten Layouts + Ausblenden + Erobere dir dein eigenes Stück vom Internet mit einer Website-Adresse, die man leicht finden, teilen und abonnieren kann. + Personalisiere deine Online-Identität mit einer individuellen Domain + Aktiviere Push-Benachrichtigungen, um Blog-Erinnerungen zu erhalten. + Domain kaufen + Fotos und Videos + Mit Subdomain fortfahren + Musik und Audio + Fotos, Videos, Musik und Audio + %s benötigt Rechte, um auf deine Audios zuzugreifen + %s benötigt Rechte, um auf deine Videos zuzugreifen + %s benötigt Rechte, um auf deine Fotos zuzugreifen + %s benötigt Rechte, um auf deine Fotos und Videos zuzugreifen + %s benötigt Rechte, um auf deine Musik, Audios, Fotos und Videos zuzugreifen + Benachrichtigungen aktivieren + Push-Benachrichtigungen aktivieren + Gehe zu Benachrichtigungen → App-Einstellungen und aktiviere %1$s, um sofort benachrichtigt zu werden. + Du musst die App öffnen, um Benachrichtigungen anzuzeigen. + Berechtigungs-Warnhinweis verwerfen. + Push-Benachrichtigungen sind deaktivert + Push-Benachrichtigungen sind deaktiviert. + Beheben + <b>%1$s</b> benutzt %2$s individuelle Jetpack-Plugins + <b>%1$s</b> benutzt das <b>%2$s</b> Plugin + Websites mit individuellen Jetpack-Plugins werden von der WordPress-App nicht unterstützt. + <b>%1$s</b> benutzt individuelle Jetpack-Plugins, die von der WordPress-App nicht unterstützt werden. + <b>%1$s</b> benutzt das <b>%2$s</b> Plugin, welches nicht von der WordPress-App unterstützt wird. + Auf manche deiner Websites kann nicht zugegriffen werden + Bitte wechsle zu der Jetpack-App, wo wir dich durch die Anbindung des vollständigen Jetpack-Plugins führen, damit diese Website mit der App benutzt werden kann. + Auf eine deiner Websites kann nicht zugegriffen werden + Zur Jetpack-App wechseln + %1$s verwendet %2$s, das noch nicht alle Funktionen der App unterstützt.\n\nBitte installiere das %3$s, um die App mit dieser Website zu verwenden. + Diese Website + %1$s verwendet %2$s, das noch nicht alle Funktionen der App unterstützt. Bitte installiere das %3$s. + %1$s verwendet %2$s, das noch nicht alle Funktionen der App unterstützt. Bitte installiere das %3$s. + Wird in einigen Tagen in die Jetpack-App verschoben. + Der Wechsel ist kostenlos und dauert nur eine Minute. + Statistiken, der Reader, Benachrichtigungen und weitere von Jetpack unterstützte Funktionen wurden aus der WordPress-App entfernt und sind ab sofort ausschließlich in der Jetpack-App verfügbar. + Weitere Informationen auf Jetpack.com + Zur Jetpack-App wechseln + %s wurden in die Jetpack-App verschoben. + %s wurde in die Jetpack-App verschoben. + WP Admin + Verwalten + Traffic + Inhalt + Einrichten + Fertig + Jetpack ist installiert. Jetzt müssen wir nur noch die Einrichtung abschließen. Dies dauert nur eine Minute. + Beitrag jetzt mit Blaze bewerben + Diese Seite mit Blaze bewerben + Diesen Beitrag mit Blaze bewerben + Verfolge die Performance, starte und beende Blaze jederzeit. Hilfe Protokolle Hilfe + Deine Inhalte werden auf Millionen von WordPress- und Tumblr-Websites angezeigt. + Bewirb innerhalb von Minuten und für lediglich ein paar Dollar pro Tag jeden beliebigen Beitrag und jede Seite. + Leite mit Blaze mehr Traffic auf deine Website + Diese Domain ist bereits registriert + Angebot + Empfohlen + Beste Alternative + In unseren FAQ findest du Antworten auf häufige Fragen, die du dir vielleicht auch stellst. + Danke für deinen Wechsel zur Jetpack-App! + Kostenlos + Tickets + Blaze Block-Menü Ausblenden Schließen @@ -21,25 +96,23 @@ Language: de das %1$s-Plugin vollständige Jetpack-Plugin individuelle Jetpack-Plugins - %1$s verwendet %2$s, das noch nicht alle Funktionen der App unterstützt.\n\nBitte installiere das %3$s, um die App mit dieser Website zu verwenden. + %1$s verwendet %2$s, das noch nicht alle Funktionen der App unterstützt.\n\nBitte installiere %3$s, um die App mit dieser Website zu verwenden. Bitte installiere das vollständige Jetpack-Plugin Es ist nur eine Website verfügbar, du kannst deine Haupt-Website also nicht ändern. - Support kontaktieren - Erneut versuchen - Jetpack kann zurzeit nicht installiert werden. - Es ist ein Problem aufgetreten - Fehler-Icon - Diese Website verwendet ein individuelles Plugin, das noch nicht alle Funktionen der App unterstützt. Bitte installiere das vollständige Jetpack-Plugin. - Fertig - Jetpack installiert - Jetpack wird auf deiner Website installiert. Dieser Vorgang kann einige Minuten dauern. - Jetpack wird installiert - Weiter - Deine Website-Anmeldedaten werden nicht gespeichert und werden nur verwendet, um Jetpack zu installieren. - Jetpack installieren + Support kontaktieren + Erneut versuchen + Jetpack kann zurzeit nicht installiert werden. + Es ist ein Problem aufgetreten + Fehler-Icon + Jetpack installiert + Jetpack wird auf deiner Website installiert. Dieser Vorgang kann einige Minuten dauern. + Jetpack wird installiert + Weiter + Deine Website-Anmeldedaten werden nicht gespeichert und werden nur verwendet, um Jetpack zu installieren. + Jetpack installieren Mit Blaze bewerben - Diese Website kann jetzt mit der App verwendet werden. - Jetpack-Icon + Diese Website kann jetzt mit der App verwendet werden. + Jetpack-Icon Entfalte das volle Potenzial deiner Website. Erhalte Statistiken, Benachrichtigungen und vieles mehr mit Jetpack. Deine Website hat das Jetpack-Plugin Suche deine Lieblingswebsites und -communities, folge ihnen und teile deine Inhalte. @@ -135,9 +208,6 @@ Language: de Links in Jetpack öffnen Brauchst du Hilfe? Verstanden - Bitte <b>lösche die WordPress-App</b>, um Datenkonflikte zu vermeiden. - Offenbar hast du die WordPress-App noch installiert. Wir empfehlen dir, die WordPress-App zu löschen, um Datenkonflikte zu vermeiden. - Du brauchst die WordPress-App nicht mehr Wir können deine Daten und Einstellungen ohne Netzwerkverbindung nicht übertragen. Bitte vergewissere dich, dass eine Netzwerkverbindung besteht, und versuch es erneut. Es kann keine Internetverbindung hergestellt werden. @@ -147,13 +217,11 @@ Language: de Erneut versuchen Beenden Icon „WordPress-App entfernen“ - Bitte <b>lösche die WordPress-App</b>, um Datenkonflikte zu vermeiden. Wir haben all deine Daten und Einstellungen übertragen. Alles ist genauso wie vorher. Danke für deinen Wechsel zu Jetpack! Wir deaktivieren Benachrichtigungen von der WordPress App. Du erhältst dieselben Benachrichtigungen, sie kommen aber ab sofort von der Jetpack-App. Benachrichtigungen kommen ab jetzt von Jetpack - Bitte lösche die WordPress-App WordPress-Hilfe-Center Support Erlaubt der App, WordPress-Benachrichtigungen zu deaktivieren. @@ -733,6 +801,7 @@ Language: de GIF Eins Keine Vorschau verfügbar + Titel hinzufügen Textfarbe Innenabstand Beitragsbild @@ -740,6 +809,7 @@ Language: de Einbettung erstellen Individuelle URL Spalte %d + Mehr Gib eine kurze Beschreibung des Links für Benutzer von Screenreadern an Blöcke hinzufügen Keine Jetpack-Websites gefunden @@ -1156,7 +1226,6 @@ Language: de Neu: Story-Beiträge Leere Seite erstellt Seite erstellt - %1$s wurde der Zugriff auf deine Fotos verweigert. Um das zu beheben, bearbeite deine Berechtigungen und aktiviere %2$s und %3$s. Fehler beim Einfügen von Medien. Fehler beim Einfügen von Medien: %s Wähle aus der WordPress-Mediathek @@ -1636,6 +1705,7 @@ Language: de BILD HINZUFÜGEN HIER BLOCK HINZUFÜGEN Alt-Text hinzufügen + Beschreibung hinzufügen „Die Liste wurde mit %1$d Einträgen geladen.“ Tippe auf den Button „Hinzufügen, um Beiträge zu speichern“, um einen Beitrag in deiner Liste zu speichern. Benachrichtigungen @@ -1841,7 +1911,6 @@ Language: de Gib ein Stichwort ein, um weitere Ideen zu erhalten Keine Vorschläge gefunden Domain registrieren - Jetpack ist installiert. Jetzt müssen wir nur noch die Einrichtung abschließen. Dies dauert nur eine Minute. Aus Einsichten entfernen Nach unten verschieben Nach oben verschieben @@ -2107,17 +2176,8 @@ Language: de Keine abonnierten Themen Füge hier Themen hinzu, um Beiträge zu deinen Lieblingsthemen zu finden Melde dich bei deinem WordPress.com-Konto an, das du für die Jetpack-Verbindung verwendet hast. - Erneut versuchen - Jetpack kann zurzeit nicht installiert werden - Es ist ein Problem aufgetreten - Jetpack installiert - Jetpack wird auf deiner Website installiert. Dieser Vorgang kann einige Minuten dauern - Jetpack installieren - Deine Website-Anmeldedaten werden nicht gespeichert und werden verwendet, um Jetpack zu installieren - Jetpack installieren Jetpack Häufige Fragen zu Jetpack - Einrichten Wenn du Statistiken zu deiner WordPress-Website erhalten möchtest, musst du das Jetpack-Plugin installieren. Keine Themes entsprechen deiner Suche Was möchtest du finden? @@ -2669,7 +2729,7 @@ Language: de Dokumente Bilder Alle - %1$s wurde der Zugriff auf deine Fotos verweigert. Um das zu korrigieren, bearbeite deine Berechtigungen und aktiviere %2$s. + %1$s wurde der Zugriff auf deine Mediendateien verweigert. Um das zu korrigieren, bearbeite deine Berechtigungen und aktiviere %2$s. Kommentare anzeigen Qualität der Videos. Höhere Werte bedeuten qualitativ hochwertigere Videos. Ändert die Größe von Videos in Beiträgen in diese Größe @@ -2828,7 +2888,7 @@ Language: de %s: Folge ich bereits %s: Benutzer nicht gefunden %s: Bereits ein Mitglied - Kommentar genehmigt! + Kommentar freigegeben! Like Jetzt Follower diff --git a/WordPress/src/main/res/values-el/strings.xml b/WordPress/src/main/res/values-el/strings.xml index 091169122a4e..b1f8eba45e7a 100644 --- a/WordPress/src/main/res/values-el/strings.xml +++ b/WordPress/src/main/res/values-el/strings.xml @@ -1,14 +1,33 @@ + Προσφορά + Προτεινόμενο + Καλύτερη Εναλλακτική + Βοήθεια + Βοήθεια + Δωρεάν + Εισιτήρια + Παρουσιάστηκε ένα πρόβλημα + Εικονίδιο σφάλματος + %d Απαντήσεις + 1 απάντηση + 0 απαντήσεις + Απαντήθηκε + Το\'πιασα + Χρειάζεστε βοήθεια; Αδυναμία σύνδεσης στο διαδίκτυο Δοκιμάστε ξανά Χμμ.. κάτι πήγε στραβά! + Καλώς ήρθατε στο Jetpack! + εικονίδιο + Γονική σελίδα + Χαρακτηριστικά σελίδας Ποια εφαρμογή διαχείρισης email χρησιμοποιείτε; Μετακίνηση εικόνας εμπρός Μετακίνηση εικόνας πίσω @@ -178,16 +197,9 @@ Language: el_GR Δεν έχετε ιστότοπους Περισσότερα Μη ακολουθούμενα θέματα - Εγκατάσταση Jetpack - Προσπαθήστε ξανά Jetpack Συχνές Ερωτήσεις για το Jetpack - Παρουσιάστηκε ένα πρόβλημα - Το Jetpack εγκαταστάθηκε - Εγκατάσταση Jetpack Συνδεθείτε στο WordPress.com λογαριασμό σας που χρησιμοποιείτε για να συνδεθείτε με το Jetpack. - Το Jetpack δεν ήταν δυνατό να εγκατασταθεί αυτή τη στιγμή. - Το Jetpack εγκαθίσταται στον ιστότοπο σας. Αυτό μπορεί να διαρκέσει μερικά λεπτά για να ολοκληρωθεί. Τι θα θέλατε να βρείτε; Δεν έχετε κάποια ετικέτα Δημιουργήστε μια ετικέτα @@ -1238,9 +1250,11 @@ Language: el_GR Διαχωρίστε τις ετικέτες με κόμματα Κάρτα SD Είναι Απαραίτητη Πολυμέσα + Επιτυχής ενημέρωση κατηγορίας Έγκριση Διαγραφή Κανένα + Η ενημέρωση της κατηγορίας απέτυχε Απάντηση Ναι Όχι diff --git a/WordPress/src/main/res/values-en-rAU/strings.xml b/WordPress/src/main/res/values-en-rAU/strings.xml index cd1274341d97..0517b9347d40 100644 --- a/WordPress/src/main/res/values-en-rAU/strings.xml +++ b/WordPress/src/main/res/values-en-rAU/strings.xml @@ -206,7 +206,6 @@ Language: en_AU Social Annual Site Stats Register Domain - Now that Jetpack is installed, we just need to get you set up. This will only take a minute. Domain suggestions couldn\'t be loaded Type a keyword for more ideas No suggestions found @@ -455,18 +454,9 @@ Language: en_AU Go Cancel More - There was a problem - Retry Log in to the WordPress.com account you used to connect Jetpack. - Jetpack installed Jetpack FAQ Jetpack - Installing Jetpack - Install Jetpack - Your website credentials will not be stored and are used only for the purpose of installing Jetpack. - Set up - Jetpack could not be installed at this time. - Installing Jetpack on your site. This can take up to a few minutes to complete. To use Stats on your WordPress site, you\'ll need to install the Jetpack plugin. No themes matching your search What would you like to find? @@ -997,7 +987,6 @@ Language: en_AU Documents Images All - %1$s was denied access to your photos. To fix this, edit your permissions and turn on %2$s. View comments Quality of videos. Higher values mean better quality videos. Resizes videos in posts to this size diff --git a/WordPress/src/main/res/values-en-rCA/strings.xml b/WordPress/src/main/res/values-en-rCA/strings.xml index 4c186dab3a85..ab944c3d1920 100644 --- a/WordPress/src/main/res/values-en-rCA/strings.xml +++ b/WordPress/src/main/res/values-en-rCA/strings.xml @@ -1,11 +1,26 @@ + Your site has the Jetpack plugin + Get notifications for new comments, likes, views, and more. + Watch your traffic grow with helpful insights and comprehensive stats. + Find and follow your favourite sites and communities, and share you content. + Stats & Insights + Blogging Prompts hidden + Give WordPress a boost with Jetpack + Visit <b>Site Settings</b> to turn back on + Notification will include a word or short phrase for inspiration + Blogging + Show prompts + Community forums + Turn off prompts + Blogging reminders + Get help from our group of volunteers. Please install Google Play Store to get the Jetpack app Stats, Reader, Notifications and other Jetpack powered features have been removed from the WordPress app. Jetpack features have moved. @@ -73,7 +88,6 @@ Language: en_CA Create a new WordPress site with the Jetpack app Switch to the Jetpack app to find, follow, and like all your favorite sites and posts with Reader. Switch to the Jetpack app to watch your site’s traffic grow with stats and insights. - Please <b>delete the WordPress app</b> to avoid data conflicts. Got it Need help? Open links in Jetpack @@ -82,14 +96,11 @@ Language: en_CA Get your notifications with the Jetpack app Follow any site with the Jetpack app Get your stats using the new Jetpack app - It looks like you still have the WordPress app installed. We recommend you delete the WordPress app to avoid data conflicts. - You no longer need the WordPress app Please contact support or try again later. We’re sorry but something didn’t go as planned. Your data is safe, but we’re unable to transfer it at this time. We are unable to transfer your data and settings without a network connection. Please check to make sure your network connection is working and try again. Unable to connect to the internet. - Please <b>delete the WordPress app</b> to avoid data conflicts. Finish Uh oh, something went wrong.. Remove WordPress App icon @@ -99,7 +110,6 @@ Language: en_CA We\'ll turn off notifications from the WordPress app. You’ll get all the same notifications but now they’ll come from the Jetpack app. Notifications now come from Jetpack - Please delete the WordPress app Allows the app to disable WordPress notifications. disable WordPress notifications Support @@ -1095,7 +1105,6 @@ Language: en_CA Combine photos, videos, and text to create engaging and tappable story posts that your visitors will love. Blank page created Page created - %1$s was denied access to your photos. To fix this, edit your permissions and turn on %2$s and %3$s. Now stories are for everyone Example story title How to create a story post @@ -1780,7 +1789,6 @@ Language: en_CA Social Annual Site Stats Register Domain - Now that Jetpack is installed, we just need to get you set up. This will only take a minute. Domain suggestions couldn\'t be loaded Type a keyword for more ideas No suggestions found @@ -2050,17 +2058,8 @@ Language: en_CA No followed topics Add topics here to find posts about your favourite topics Log in to the WordPress.com account you used to connect Jetpack. - Retry - There was a problem - Jetpack installed - Installing Jetpack - Install Jetpack Jetpack Jetpack FAQ - Set up - Jetpack could not be installed at this time. - Installing Jetpack on your site. This can take up to a few minutes to complete. - Your website credentials will not be stored and are used only for the purpose of installing Jetpack. To use Stats on your WordPress site, you\'ll need to install the Jetpack plugin. No themes matching your search What would you like to find? @@ -2612,7 +2611,6 @@ Language: en_CA Documents Images All - %1$s was denied access to your photos. To fix this, edit your permissions and turn on %2$s. View comments Quality of videos. Higher values mean better quality videos. Resizes videos in posts to this size diff --git a/WordPress/src/main/res/values-en-rGB/strings.xml b/WordPress/src/main/res/values-en-rGB/strings.xml index d1c8a18d73c1..cdea1b5391c5 100644 --- a/WordPress/src/main/res/values-en-rGB/strings.xml +++ b/WordPress/src/main/res/values-en-rGB/strings.xml @@ -1,11 +1,63 @@ + Blaze a Post now + Blaze this Page + Blaze this Post + Track performance, start, and stop your Blaze at any time. + Best Alternative + Blaze + Drive more traffic to your site with Blaze + Free + Help + Help + Logs + Promote any post or page in only a few minutes for just a few dollars a day. + Recommended + Sale + See our FAQ for answers to common questions you may have. + Thank you for switching to the Jetpack app! + This domain is already registered + Tickets + Your content will appear on millions of WordPress and Tumblr sites. + Blocks menu + By setting up Jetpack you agree to our + Close + Contact support + Display your work across millions of sites. + Hide this + Install the full plugin + Promote your content with Blaze + Terms and conditions + full Jetpack plugin + individual Jetpack plugins + the %1$s plugin + Only one site is available, so you can\'t change your primary site. + Please install the full Jetpack plugin + Promote with Blaze + Your site has the Jetpack plugin + Get notifications for new comments, likes, views, and more. + Find and follow your favourite sites and communities, and share you content. + Watch your traffic grow with helpful insights and comprehensive stats. + The Jetpack mobile app is designed to work in companion with the Jetpack plugin. Switch now to get access to stats, notifications, reader, and more. + Stats & Insights + Blogging Prompts hidden + Visit <b>Site Settings</b> to turn back on + Notification will include a word or short phrase for inspiration + Give WordPress a boost with Jetpack + You can control Blogging Prompts and Reminders at any time in My Site > Settings > Blogging + Jetpack lets you do more with your WordPress site. Switching is free and only takes a minute. + Turn off prompts + Get help from our group of volunteers. + Community forums + Blogging reminders + Show prompts + Blogging %1$s are moving in %2$s %1$s are moving soon %1$s is moving in %2$s @@ -35,6 +87,7 @@ Language: en_GB Stats, Reader, Notifications and other Jetpack-powered features will be removed from the WordPress app soon. Switch to the Jetpack app Switching is free and only takes a minute. + Stats, Reader, Notifications and other features will soon move to the Jetpack mobile app. %d answers 0 answers 1 answer @@ -81,19 +134,15 @@ Language: en_GB Open links in Jetpack Unable to disable open links in Jetpack Unable to enable open links in Jetpack - Please <b>delete the WordPress app</b> to avoid data conflicts. - It looks like you still have the WordPress app installed. We recommend you delete the WordPress app to avoid data conflicts. Please check to make sure your network connection is working and try again. Please contact support or try again later. Unable to connect to the internet. We are unable to transfer your data and settings without a network connection. We’re sorry but something didn’t go as planned. Your data is safe, but we’re unable to transfer it at this time. - You no longer need the WordPress app Finish Remove WordPress App icon Try again Uh oh, something went wrong.. - Please <b>delete the WordPress app</b> to avoid data conflicts. We’ve transferred all your data and settings. Everything is right where you left it. Thanks for switching to Jetpack! We\'ll turn off notifications from the WordPress app. @@ -101,7 +150,6 @@ Language: en_GB Allows the app to disable WordPress notifications. disable WordPress notifications Notifications now come from Jetpack - Please delete the WordPress app Support WordPress help centre Continue @@ -311,6 +359,8 @@ Language: en_GB The best way to become a better writer is to build a writing habit and share with others - that’s where Prompts come in! Posting regularly attracts new readers. Tell us when you want to write and we’ll send you a reminder! Set reminders + Include a Blogging Prompt + Introducing\nBlogging Prompts Become a better writer by building a habit Writing & Poetry Travel @@ -616,6 +666,7 @@ Language: en_GB Manage your site\'s categories The content of your latest posts page is automatically generated and cannot be edited. Categories + Reminders Border settings We need to save your content on the device before it can be published. Review your storage settings and remove files to free up space. View storage @@ -935,8 +986,8 @@ Language: en_GB No %s suggestions available. There was a problem loading suggestions. No matching %s. - Jetpack Scan will replace the affected file or directory. Jetpack Scan cannot automatically fix this threat.\n We suggest that you resolve the threat manually: ensure that WordPress, your theme, and all of your plugins are up to date, and remove the offending code, theme, or plugin from your site. \n \n\n If you need more help to resolve this threat, we recommend <b>Codeable</b>, a trusted freelancer marketplace of highly vetted WordPress experts.\n They have identified a select group of security experts to help with these projects. Pricing ranges from $70–120/hour, and you can get a free estimate with no obligation to hire.\n + Jetpack Scan will replace the affected file or directory. What was the problem? The technical details Threat found in file: @@ -1093,7 +1144,6 @@ Language: en_GB Help button Combine photos, videos, and text to create engaging and tappable story posts that your visitors will love. Story posts don\'t disappear - %1$s was denied access to your photos. To fix this, edit your permissions and turn on %2$s and %3$s. Page created Blank page created Introducing Story Posts @@ -1783,7 +1833,6 @@ Language: en_GB Type a keyword for more ideas No suggestions found Register Domain - Now that Jetpack is installed, we just need to get you set up. This will only take a minute. Follower totals Remove from insights Move down @@ -2049,18 +2098,9 @@ Language: en_GB More Add topics here to find posts about your favourite topics No followed topics - Install Jetpack - Installing Jetpack Jetpack Jetpack FAQ - Jetpack installed Log in to the WordPress.com account you used to connect Jetpack. - Retry - There was a problem - Set up - Jetpack could not be installed at this time. - Installing Jetpack on your site. This can take up to a few minutes to complete. - Your website credentials will not be stored and are used only for the purpose of installing Jetpack. To use Stats on your WordPress site, you\'ll need to install the Jetpack plugin. No themes matching your search What would you like to find? @@ -2612,7 +2652,6 @@ Language: en_GB Documents Images All - %1$s was denied access to your photos. To fix this, edit your permissions and turn on %2$s. View comments Quality of videos. Higher values mean better quality videos. Enable to resize and compress videos diff --git a/WordPress/src/main/res/values-es-rCL/strings.xml b/WordPress/src/main/res/values-es-rCL/strings.xml index 97d62bd529ac..577a9a86d579 100644 --- a/WordPress/src/main/res/values-es-rCL/strings.xml +++ b/WordPress/src/main/res/values-es-rCL/strings.xml @@ -235,11 +235,6 @@ Language: es_CL No tienes ningún sitio Más Inicia sesión en la cuenta WordPress.com que usaste para conectar jetpack. - Reintentar - Hubo un problema - Jetpack instalado - Instalación de Jetpack - Instalar Jetpack Jetpack Jetpack FAQ Para usar stats en tu sitio de WordPress, necesitarás instalar el plugin de Jetpack. @@ -768,7 +763,6 @@ Language: es_CL Imágenes Todo Audio - %1$s le negó el acceso a sus fotos. Para solucionarlo, edita tus permisos y activa %2$s. Ver comentarios Calidad de los vídeos. Los valores más altos significan una mejor calidad de video. Habilitar para cambiar el tamaño y comprimir vídeos diff --git a/WordPress/src/main/res/values-es-rCO/strings.xml b/WordPress/src/main/res/values-es-rCO/strings.xml index b1ad057f878e..4784228b651f 100644 --- a/WordPress/src/main/res/values-es-rCO/strings.xml +++ b/WordPress/src/main/res/values-es-rCO/strings.xml @@ -953,7 +953,6 @@ Language: es_CO Presentación de las entradas de historias Página en blanco creada Página creada - %1$s ha denegado el acceso a tus fotos. Para corregirlo, edita tus permisos y activa %2$s y %3$s. Inserción del medio fallida. Ha fallado la inserción del medio: %s Elige desde la biblioteca de medios de WordPress @@ -1635,7 +1634,6 @@ Language: es_CO Teclea una palabra clave para más ideas No se han encontrado sugerencias Registrar dominio - Ahora que está instalado Jetpack, solo necesitamos que lo configures. Esto solo te llevará un minuto. Quitar de los detalles Mover abajo Mover arriba @@ -1898,15 +1896,6 @@ Language: es_CO Temas no seguidos Añade aquí temas para descubrir entradas sobre tus temáticas favoritas Accede a la cuenta de WordPress.com que usaste para conectar Jetpack. - Reintentar - Configurar - Jetpack no se pudo instalar en este momento. - Hubo un problema - Jetpack instalado - Instalando Jetpack en tu sitio. Esto puede llevar unos minutos completarse. - Instalando Jetpack - Las credenciales de tu web no se almacenarán, y solo se utilizan para instalar Jetpack. - Instala Jetpack Jetpack FAQ de Jetpack Para usar las estadísticas en tu sitio WordPress necesitas instalar el plugin Jetpack. @@ -2459,7 +2448,6 @@ Language: es_CO Documentos Imágenes Todos - %1$s denegó el acceso a tus fotos. Para solucionar esto modifica tus permisos y activa %2$s. Ver comentarios Calidad de los videos. Valores más altos implican videos de mejor calidad. Redimensiona los videos en las entradas a este tamaño diff --git a/WordPress/src/main/res/values-es-rMX/strings.xml b/WordPress/src/main/res/values-es-rMX/strings.xml index 38651caba3c6..a93cfdc4d0d2 100644 --- a/WordPress/src/main/res/values-es-rMX/strings.xml +++ b/WordPress/src/main/res/values-es-rMX/strings.xml @@ -26,8 +26,6 @@ Language: es_MX Abrir las ligas en Jetpack. ¿Necesita ayuda? Entendido - Parece que todavía tiene instalada la aplicación WordPress. Recomendamos que la elimines para evitar conflictos de datos. - Ya no necesita la aplicación WordPress. No pudimos cambiar sus datos y configuraciones sin conexión con la red. Por favor, cheque para asegurar que su conexión de red está funcionando y inténtelo de nuevo. No se puede conectar al internet. @@ -194,7 +192,6 @@ Language: es_MX Botón de ayuda Combina fotos, vídeos y texto para crear entradas de historias atractivas y accesibles que les encantarán a tus visitantes. Las entradas de historias no desaparecen - %1$s ha denegado el acceso a tus fotos. Para corregirlo, edita tus permisos y activa %2$s y %3$s. Página creada Página en blanco creada Presentación de las entradas de historias @@ -869,7 +866,6 @@ Language: es_MX Estadísticas anuales del sitio Seguidores totales Registrar dominio - Ahora que está instalado Jetpack, solo necesitamos que lo configures. Esto solo te llevará un minuto. Mover abajo Mover arriba Ajustes de los parámetros de las estadísticas @@ -1120,17 +1116,8 @@ Language: es_MX Añade aquí temas para descubrir entradas sobre tus temáticas favoritas Temas no seguidos Accede a la cuenta de WordPress.com que usaste para conectar Jetpack. - Reintentar - Hubo un problema - Jetpack instalado - Instalando Jetpack - Instala Jetpack Jetpack FAQ de Jetpack - Configurar - Jetpack no se pudo instalar en este momento. - Instalando Jetpack en tu sitio. Esto puede llevar unos minutos completarse. - Las credenciales de tu web no se almacenarán, y solo se utilizan para instalar Jetpack. Para usar las estadísticas en tu sitio WordPress necesitas instalar el plugin Jetpack. No hay temas que coincidan con tu búsqueda ¿Que te gustaría encontrar? @@ -1675,7 +1662,6 @@ Language: es_MX Imágenes Todos Videos - %1$s denegó el acceso a tus fotos. Para solucionar esto modifica tus permisos y activa %2$s. Ver comentarios Calidad de los videos. Valores más altos implican videos de mejor calidad. Redimensiona los videos en las entradas a este tamaño diff --git a/WordPress/src/main/res/values-es-rVE/strings.xml b/WordPress/src/main/res/values-es-rVE/strings.xml index 8451d2238bae..5937f45983a3 100644 --- a/WordPress/src/main/res/values-es-rVE/strings.xml +++ b/WordPress/src/main/res/values-es-rVE/strings.xml @@ -973,7 +973,6 @@ Language: es_VE Presentación de las entradas de historias Página en blanco creada Página creada - %1$s ha denegado el acceso a tus fotos. Para corregirlo, edita tus permisos y activa %2$s y %3$s. Inserción del medio fallida. Ha fallado la inserción del medio: %s Elige desde la biblioteca de medios de WordPress @@ -1655,7 +1654,6 @@ Language: es_VE Teclea una palabra clave para más ideas No se han encontrado sugerencias Registrar dominio - Ahora que está instalado Jetpack, solo necesitamos que lo configures. Esto solo te llevará un minuto. Quitar de los detalles Mover abajo Mover arriba @@ -1918,15 +1916,6 @@ Language: es_VE Temas no seguidos Añade aquí temas para descubrir entradas sobre tus temáticas favoritas Accede a la cuenta de WordPress.com que usaste para conectar Jetpack. - Reintentar - Configurar - Jetpack no se pudo instalar en este momento. - Hubo un problema - Jetpack instalado - Instalando Jetpack en tu sitio. Esto puede llevar unos minutos completarse. - Instalando Jetpack - Las credenciales de tu web no se almacenarán, y solo se utilizan para instalar Jetpack. - Instala Jetpack Jetpack FAQ de Jetpack Para usar las estadísticas en tu sitio WordPress necesitas instalar el plugin Jetpack. @@ -2480,7 +2469,6 @@ Language: es_VE Documentos Imágenes Todos - %1$s denegó el acceso a tus fotos. Para solucionar esto modifica tus permisos y activa %2$s. Ver comentarios Calidad de los videos. Valores más altos implican videos de mejor calidad. Redimensiona los videos en las entradas a este tamaño diff --git a/WordPress/src/main/res/values-es/strings.xml b/WordPress/src/main/res/values-es/strings.xml index 8ab339c95294..00e8b35f8046 100644 --- a/WordPress/src/main/res/values-es/strings.xml +++ b/WordPress/src/main/res/values-es/strings.xml @@ -1,11 +1,94 @@ + Recomendamos <b>desinstalar la aplicación WordPress</b> en tu dispositivo para evitar conflictos de datos. + Parece que todavía tienes la aplicación WordPress instalada. + Ya no necesitas la aplicación WordPress en tu dispositivo + Recomendamos <b>desinstalar la aplicación WordPress</b> en tu dispositivo para evitar conflictos de datos. + Bienvenido a la aplicación Jetpack. Puedes desinstalar la aplicación WordPress. + Eliminar bloques + Privacidad y valoraciones + Ajustes de reproducción + Color de la barra de reproducción + Manual + Dinámica + Describe el propósito de la imagen. Déjalo vacío si la imagen es decorativa. + Comience con diseños personalizados y preparados para dispositivos móviles + Crear otra página + Añadir páginas a tu sitio + Ocultar esto + Hazte con tu rincón en Internet con una dirección web fácil de encontrar, de compartir y de seguir. + Sé el dueño de tu identidad online con un dominio propio + Para usar recordatorios para publicar tienes que activar los avisos instantáneos. + Activar los avisos instantáneos + Continuar con subdominio + Comprar dominio + Fotos y videos, Música y audio + Música y audio + Fotos y videos + %s necesita permisos para acceder a tus audios + %s necesita permisos para acceder a tus videos + %s necesita permisos para acceder a tus fotos + %s necesita permisos para acceder a tus fotos y videos + %s necesita permisos para acceder a tu música, audios, fotos y videos + Activar los avisoa + Ve a Ajustes &rarr; Notificaciones &rarr; Ajustes de la app, y activa %1$s para recibir notificaciones inmediatamente. + Corrección + Tendrás que abrir la aplicación para ver las notificaciones. + Las notificaciones push están desactivadas + Las notificaciones push están desactivadas. + Descarta el aviso del permiso de notificaciones. + <b>%1$s</b> está usando %2$s plugins individuales de Jetpack + <b>%1$s</b> está usando el plugin <b>%2$s</b> + La aplicación de WordPress no es compatible con los los plugins individuales de Jetpack. + <b>%1$s</b> está usando plugins individuales de Jetpack que no son compatibles con la aplicación de WordPress. + <b>%1$s</b> está usando el plugin <b>%2$s</b> que no es compatible con la aplicación de WordPress. + No se ha podido acceder a algunos de tus sitios + No se ha podido acceder a uno de tus sitios + Por favor, pásate a la aplicación Jetpack donde te guiaremos para que conectes el plugin Jetpack para usar este sitio con la aplicación. + Cambia a la aplicación Jetpack + %1$s usa %2$s, que todavía no es compatible con todas las funciones de la aplicación.\n\nInstala el %3$s para usar la aplicación con este sitio. + Este sitio + %1$s usa %2$s, que todavía no es compatible con todas las funciones de la aplicación. Instala el %3$s. + %1$s usa %2$s, que todavía no es compatible con todas las funciones de la aplicación. Instala el %3$s. + El cambio es gratuito y solo te llevará un minuto. + Pásate a la aplicación de Jetpack en pocos días. + Hemos eliminado algunas funciones (como Estadísticas, Lector o Notificaciones, entre otras) de la aplicación de WordPress y, ahora, solo están disponibles en la de Jetpack. + Tráfico + Contenido + Hecho + Encontrarás más información en Jetpack.com + Cambiar a la aplicación de Jetpack + %s se han trasladado a la aplicación de Jetpack. + %s se ha trasladado a la aplicación de Jetpack. + WP Admin + Configurar + Ahora que Jetpack está instalado, solo tenemos que configurarlo. Solo te llevará un minuto. + Gestionar + Promocionar esta página con Blaze + Promocionar esta entrada con Blaze + Promocionar una entrada con Blaze ahora + Inicia y detén la actividad promocional con Blaze y haz un seguimiento del rendimiento siempre que quieras. + Tu contenido aparecerá en millones de sitios web de WordPress y Tumblr. + Comienza a promocionar cualquier entrada o página en cuestión de minutos y a un precio muy asequible. + Genera más tráfico hacia tu sitio con Blaze + Este dominio ya está registrado + Oferta + Recomendado + Mejor alternativa + Ayuda + ¡Gracias por cambiar a la aplicación Jetpack! + Registros + Entradas + Gratis + Ayuda + Blaze + Con nuestras preguntas frecuentes podrás resolver algunas de tus dudas. Menú de bloques Ocultar esto Muestra tu trabajo en millones de sitios @@ -18,26 +101,24 @@ Language: es plugin completo de Jetpack plugins individuales de Jetpack el plugin %1$s - %1$s usa %2$s, que todavía no es compatible con todas las funciones de la aplicación.\n\nInstala el %3$s para usar la aplicación con este sitio. + %1$s usa %2$s, que todavía no es compatible con todas las funciones de la aplicación.\n\nInstala el %3$s para usar la aplicación con este sitio. Instala el plugin completo de Jetpack Solo hay un sitio disponible, por lo que no puedes cambiar tu sitio principal. - Este sitio usa un plugin individual que todavía no es compatible con todas las funciones de la aplicación. Instala el plugin completo de Jetpack. - Contactar con el soporte técnico - Reintentar - En estos momentos, no se puede instalar Jetpack. - Se ha producido un problema - Icono de error - Hecho - Todo listo para usar este sitio con la aplicación. - Se ha instalado Jetpack - Instala Jetpack en tu sitio. Esto puede llevar unos minutos completarse. - Instalando Jetpack - Continuar - Las credenciales de tu sitio web no se almacenan; solo se usan para poder instalar Jetpack de forma segura. - Instalar Jetpack - Icono de Jetpack + Reintentar + En estos momentos, no se puede instalar Jetpack. + Se ha producido un problema + Icono de error + Contactar con soporte Promocionar con Blaze - Libera todo el potencial de tu sitio Obtén estadísticas, notificaciones y más con Jetpack. + Instalar Jetpack + Todo listo para usar este sitio con la aplicación. + Jetpack instalado + Instala Jetpack en tu sitio. Esto puede llevar unos minutos completarse. + Instalando Jetpack + Continuar + Las credenciales de tu web no se almacenarán y solo se usarán para instalar Jetpack. + Icono de Jetpack + Libera todo el potencial de tu sitio. Obtén estadísticas, notificaciones y más con Jetpack. Tu sitio tiene el plugin de Jetpack La aplicación móvil Jetpack está diseñada para funcionar junto con el plugin de Jetpack. Haz el cambio ahora y obtén acceso a estadísticas, notificaciones o el lector, entre otras funciones. Recibe notificaciones por nuevos comentarios, Me gusta, visualizaciones, etc. @@ -71,8 +152,8 @@ Language: es %1$s es mayor que la semana anterior Tus visitantes en los últimos siete días son %1$s menos que en los siete días anteriores. Tus visitantes en los últimos siete días son %1$s más que en los siete días anteriores. - Tus visitas en los últimos siete días son %1$s menos que en los siete días anteriores. Tus visitas en los últimos siete días son %1$s más que en los siete días anteriores. + Tus visitas en los últimos siete días son %1$s menos que en los siete días anteriores. Siete días anteriores Últimos siete días %d semanas @@ -80,12 +161,12 @@ Language: es Desde el <b>DayOne</b> Ocultar esto Recuérdamelo más tarde - Algunas funciones, como estadísticas, lector o avisos, se trasladarán pronto a la aplicación móvil de Jetpack. Cambiar a la aplicación de Jetpack Más información en jetpack.com El cambio es gratuito y solo lleva un minuto. Pronto se van a retirar de la aplicación de WordPress las estadísticas, lectura, avisos y otras funcionalidades de Jetpack. Se van a retirar de la aplicación de WordPress las estadísticas, lectura, avisos y otras funcionalidades de Jetpack el %s. + Algunas funciones, como estadísticas, lector o avisos, se trasladarán pronto a la aplicación móvil de Jetpack. Las funciones de Jetpack se trasladarán pronto. Los avisos se están trasladando a la aplicación de Jetpack El lector se está trasladando a la aplicación de Jetpack @@ -110,8 +191,8 @@ Language: es Actualizando la categoría Actualizar categoría Las entradas de este usuario no volverán a mostrarse - Bloquear usuario Denunciar a este usuario + Bloquear usuario Abrir enlaces en WordPress Parece que tienes instalada la aplicación de Jetpack.\n\n¿Quieres abrir enlaces en la aplicación de Jetpack en el futuro?\n\nPuedes cambiar esta opción en cualquier momento desde Ajustes de la aplicación > Abrir enlaces en Jetpack. ¿Quieres abrir enlaces en Jetpack? @@ -132,9 +213,6 @@ Language: es Abrir enlaces en Jetpack ¿Necesitas ayuda? De acuerdo - Por favor, <b>borra la aplicación de WordPress</b> para evitar conflictos de datos. - Parece que tienes instalada la aplicación de WordPress. Te recomendamos que elimines la aplicación de WordPress para evitar conflictos de datos. - Ya no necesitas la aplicación de WordPress No podemos transferir tus datos y ajustes sin una conexión de red. Comprueba tu conexión de red para asegurarte de que funcione y vuelve a intentarlo. No se ha podido conectar a Internet. @@ -144,13 +222,11 @@ Language: es Volver a intentarlo Terminar Icono Quitar aplicación de WordPress - Por favor, <b>borra la aplicación de WordPress</b> para evitar conflictos de datos. Hemos transferido todos tus datos y ajustes. Todo está tal y como lo dejaste. ¡Gracias por cambiar a Jetpack! Desactivaremos las notificaciones de la aplicación de WordPress. Recibirás las mismas notificaciones, pero a partir de ahora desde la aplicación de Jetpack. Ahora las notificaciones llegan de Jetpack - Elimina la aplicación de WordPress Centro de Ayuda de WordPress Soporte Permite que la aplicación desactive las notificaciones de WordPress. @@ -186,8 +262,8 @@ Language: es Las estadísticas funcionan con Jetpack Encuentra, sigue y dale «Me gusta» a todos tus sitios y publicaciones favoritos con Reader, ahora disponible en la nueva aplicación Jetpack. Reader funciona con Jetpack - La nueva app de Jetpack tiene estadísticas, lector, avisos, y más para mejorar tu WordPress. WordPress es mejor con Jetpack + La nueva app de Jetpack tiene estadísticas, lector, avisos, y más para mejorar tu WordPress. Actualiza tu plan para usar fondos de vídeo Actualiza tu plan para subir audio Funciona gracias a Jetpack @@ -199,8 +275,8 @@ Language: es Prueba la nueva aplicación Jetpack Problema al mostrar el bloque. \nToca para intentar la recuperación del bloque. La semana pasada tuviste %1$s visitas y %2$s comentarios - La semana pasada tuviste %1$s visitas y %2$s Me gusta La semana pasada tuviste %1$s visitas. + La semana pasada tuviste %1$s visitas y %2$s Me gusta La semana pasada tuviste %1$s visitas, %2$s Me gusta y %3$s comentarios. ⭐️ Tu última entrada %1$s ha recibido %2$s Me gusta. Funciona gracias a Jetpack @@ -234,16 +310,16 @@ Language: es Se están cargando los estímulos para bloguear. Espera un momento e inténtalo de nuevo. ¿No puedes decidirte? Puedes cambiar el tema en cualquier momento. Bloguear - Elegido para ti - Ideal para %s - Vista previa del tema %s Elige un tema - Me he saltado el estímulo para bloguear de hoy - Más información Totales Otros Buscar WordPress + Elegido para ti + Ideal para %s + Vista previa del tema %s + Me he saltado el estímulo para bloguear de hoy + Más información Vistas Programar Programa tu entrada @@ -251,10 +327,10 @@ Language: es Configura tus recordatorios de blogueo Consulta el curso Haz crecer tu audiencia - También puedes reorganizar los bloques tocando un bloque y luego tocando las flechas arriba y abajo que aparecen en la parte inferior izquierda del bloque para moverlo encima o debajo de otros bloques. Archivo de imagen no encontrado. Arrastrar y soltar hace que reordenar bloques sea algo trivial. Presiona y sujeta un bloque, luego arrástralo a su nueva ubicación y suéltalo. Arrastrar y soltar + También puedes reorganizar los bloques tocando un bloque y luego tocando las flechas arriba y abajo que aparecen en la parte inferior izquierda del bloque para moverlo encima o debajo de otros bloques. Botones de flechas %1$s. Seleccionado actualmente: %2$s Todas las tareas están completas @@ -262,23 +338,25 @@ Language: es Explorar código de acceso ⭐️ Tu última entrada %1$s ha recibido %2$s me gusta. No hay suficiente actividad. ¡Vuelve a comprobarlo más tarde, cuando tu sitio haya tenido más visitas! - %1$s, %2$s%% del total de seguidores %1$s (%2$s%%) + %1$s, %2$s%% del total de seguidores Copiar enlace - ¡Enhorabuena! Ya sabes manejarte<br/> + Sube fotos o vídeos Conoce la aplicación + ¡Enhorabuena! Ya sabes manejarte<br/> Sube los medios directamente a tu sitio desde tu dispositivo o cámara - Sube fotos o vídeos Obtén actualizaciones en tiempo real desde tu bolsillo - Selecciona %1$s Medios %2$s para ver tu biblioteca actual. Obtén actualizaciones en tiempo real desde tu bolsillo. Comprueba tus avisos + Utiliza <b> Descubrir </b> para encontrar sitios y etiquetas. + Miniatura de vídeo + Selecciona %1$s Medios %2$s para ver tu biblioteca actual. Selecciona la %1$s Pestaña de avisos %2$s para recibir actualizaciones sobre la marcha. Selecciona %1$s Más %2$s para subir medios. Puedes añadirlos a tus entradas o páginas desde cualquier dispositivo. - Utiliza <b> Descubrir </b> para encontrar sitios y etiquetas. Utiliza <b> Descubrir </b> para encontrar sitios y etiquetas. Prueba a seleccionar %1$s Ajustes %2$s para añadir temáticas que te gusten. - Miniatura de vídeo Principales comentaristas + Total de seguidores + Total de comentarios Publicada hace %1$d años Publicada hace un año Publicada hace %1$d meses @@ -290,35 +368,33 @@ Language: es Publicada hace %1$d minutos Publicada hace un minuto Publicada hace unos segundos - Total de seguidores - Total de comentarios Total de «Me gusta» Descartar Responder - Estímulo diario Entendido Toca <b>%1$s</b> para ver tu sitio + Estímulo diario Selecciona el %1$s Lector %2$s para descubrir otros sitios. - Aprende más sobre los estímulos Vídeo no seleccionado Vídeo seleccionado + Aprende más sobre los estímulos Miniatura del medio 🔥 La hora más popular %1$s %2$s Visitar el escritorio Tu sitio ya está protegido con VaultPress. Más abajo, puedes encontrar un enlace a tu escritorio de VaultPress. - Tu sitio tiene VaultPress Idioma actual: + Tu sitio tiene VaultPress Crear sitio Añadir bloques Pantalla inicial Más información Conviértete en un mejor escritor creando un hábito de escritura. Toca para más información. + Nombre del sitio Nuevo en la aplicación móvil de WordPress: Mensajes Un buen nombre es corto y fácil de recordar.\nPuedes cambiarlo más adelante. - Dale un nombre a tu web %s - Nombre del sitio ¿Te interesa crear tu audiencia? Echa un vistazo a nuestros <a href=\"%1$s\">mejores consejos</a>. + Dale un nombre a tu web %s Vistas y visitantes Eliminada como imagen destacada Establecer como imagen destacada @@ -360,16 +436,16 @@ Language: es Nota: ¡Te mostraremos un nuevo estímulo cada día en tu escritorio para ayudarte a que fluyan esos fluidos creativos! El mejor modo de convertirte en un mejor escritor es crear un hábito de escritura y compartir con otros - ¡aquí es donde entran los estímulos! - Presentando\nEstímulos para bloguear Configurar recordatorios - Incluir el estímulo para bloquear Publicar con regularidad atrae nuevos lectores. ¡Cuéntanos cuándo quieres escribir y te enviaremos un recordatorio! - Conviértete en un mejor escritor creando un hábito + Presentando\nEstímulos para bloguear + Incluir el estímulo para bloquear Escritura y poesía Viajes Tecnología Deportes Inmobiliaria + Conviértete en un mejor escritor creando un hábito Política Fotografía Personal @@ -395,114 +471,114 @@ Language: es Belleza Automoción Arte - Ej.: Moda, poesía, política Temática del sitio + Ver más estímulos Toca <b>%1$s</b> para continuar. + Ej.: Moda, poesía, política Omitir por hoy - Ver más estímulos - %d respuestas Comparte el estímulo de bloguear ✓ Respondido - Responder estímulo + %d respuestas Estímulos Todos + Responder estímulo Esta combinación de color puede ser difícil de leer para la gente. Intenta usar un color de fondo más claro y/o un color de texto más oscuro. - Esta combinación de color puede ser difícil de leer para la gente. Intenta usar un color de fondo más oscuro y/o un color de texto más claro. Fallo al insertar los medios.\nToca para más información. - Elige una temática de las listadas a continuación o escribe la tuya propia. + Esta combinación de color puede ser difícil de leer para la gente. Intenta usar un color de fondo más oscuro y/o un color de texto más claro. ¿De qué trata tu web? - Resumen semanal - Inicio + Elige una temática de las listadas a continuación o escribe la tuya propia. Añadir categorías - ¿Qué aplicación de correo electrónico usas? + Inicio + Resumen semanal Ha habido un problema al comunicar con el sitio. Se ha devuelto un código de error HTTP 401. - Las llamadas XML-RPC parecen bloqueadas en este sitio (código de error 401). Si el intento de acceso falla, toca en el icono de ayuda para ver las FAQ. + ¿Qué aplicación de correo electrónico usas? No se ha podido leer el sitio WordPress en esa URL. Toca en el icono de ayuda para ver las FAQ. + Las llamadas XML-RPC parecen bloqueadas en este sitio (código de error 401). Si el intento de acceso falla, toca en el icono de ayuda para ver las FAQ. Los servicios XML-RPC están desactivados en este sitio. Menú Tu búsqueda incluye caracteres no compatibles en los dominios de WordPress.com. Se permiten los siguientes caracteres: A–Z, a–z, 0–9. - Comprueba tu conexión a Internet y actualiza la página. - Ir a las estadísticas - Estadísticas de hoy Ha ocurrido un error al actualizar el contenido del aviso + Estadísticas de hoy + Ir a las estadísticas + Comprueba tu conexión a Internet y actualiza la página. Editar - Fallo al moderar los comentarios - Mover a la papelera Marcar como spam + Mover a la papelera + Fallo al moderar los comentarios Rechazar - Ajustes de la galería de mosaico Navega a la pantalla de selección del diseño - Estilo de la galería - Ir a la web + Ajustes de la galería de mosaico Puedes conectar tu cuenta de Facebook en la web de WordPress.com. Cuando lo hayas hecho, vuelve a la aplicación de WordPress para cambiar tus ajustes para compartir. - Icono de la aplicación - Icono de volver - Logotipo de Automattic + Ir a la web + Estilo de la galería WordPress - WooCommerce - Tumblr - Simplenote - Pocket Casts - Jetpack - Day One - Código fuente - Política de privacidad - Términos del servicio + Logotipo de Automattic + Icono de volver + Icono de la aplicación Trabaja desde cualquier lugar - Trabaja con nosotros - Familia Automattic - Legal y otros - Twitter - Instagram - Valóranos - Compartir con amigos + Términos del servicio + Política de privacidad + Código fuente + Day One + Jetpack + Pocket Casts + Simplenote + Tumblr + WooCommerce Puedes editar este bloque usando la versión web del editor. - Abrir los ajustes de seguridad de Jetpack + Compartir con amigos + Valóranos + Instagram + Twitter + Legal y otros + Familia Automattic + Trabaja con nosotros Nota: Debes permitir el acceso desde WordPress.com para editar este bloque en el editor móvil. - Nota: El diseño puede variar entre temas y tamaños de pantalla - Ajustes de la dirección - AÑADIR MEDIOS + Abrir los ajustes de seguridad de Jetpack Estamos teniendo problemas en este momento para cargar los datos de tu sitio. - Algunos datos no se han cargado - El escritorio no está actualizado. Por favor, comprueba tu conexión y luego pulsa para refrescar. - No se ha podido actualizar el escritorio. + AÑADIR MEDIOS + Ajustes de la dirección + Nota: El diseño puede variar entre temas y tamaños de pantalla ¡Vídeo no subido! Para subir vídeos de más de 5 minutos es necesario un plan de pago. - Agradecimientos + No se ha podido actualizar el escritorio. + El escritorio no está actualizado. Por favor, comprueba tu conexión y luego pulsa para refrescar. + Algunos datos no se han cargado Aviso de privacidad de California - Versión %1$s - Agradecimientos - Legal y otros - Sobre %1$s - Blog - Lo básico - Seleccionado: Por defecto - Más opciones de soporte - Obtener soporte - Tamaño de la fuente + Agradecimientos Toca dos veces para seleccionar un tamaño de fuente - Toca dos veces para seleccionar el tamaño de fuente por defecto - Contactar con el soporte - %1$s (%2$s) - Seguir la conversación - Sé el primero en comentar - Ver todos los comentarios + Tamaño de la fuente + Obtener soporte + Más opciones de soporte + Seleccionado: Por defecto + Lo básico + Blog + Sobre %1$s + Legal y otros + Agradecimientos + Versión %1$s Ha habido un error al obtener los datos de la entrada - Ha habido un error al obtener los comentarios + Ver todos los comentarios + Sé el primero en comentar + Seguir la conversación + %1$s (%2$s) + Contactar con el soporte + Toca dos veces para seleccionar el tamaño de fuente por defecto Ajustes para seguir la conversación - Desde el portapapeles + Ha habido un error al obtener los comentarios + Ir a los borradores + Ir a las entradas programadas + Acerca de WordPress Imagen destacada Copiar la URL desde el portapapeles, %s - Acerca de WordPress - Ir a las entradas programadas - Ir a los borradores - Crear una entrada - ¡Publicar regularmente ayuda a crear tu audiencia! - Crear tu próxima entrada - Cambiado a modo visual - Cambiado a modo HTML - Enlace copiado al portapapeles - Autor + Desde el portapapeles Copiar el enlace + Autor + Enlace copiado al portapapeles + Cambiado a modo HTML + Cambiado a modo visual + Crear tu próxima entrada + ¡Publicar regularmente ayuda a crear tu audiencia! + Crear una entrada Añadir un dominio personalizado hace que sea más fácil para tus visitantes encontrar tu sitio Añade tu dominio Las entradas aparecen en la página de tu blog en orden cronológicamente inverso. ¡Es el momento de compartir tus ideas con el mundo! @@ -513,68 +589,68 @@ Language: es <span style=\"color:#008000;\">Gratis el primer año </span><span style=\"color:#50575e;\"><s>%s /año</s></span> Tus dominios redirigirán tu sitio a %s Crear un enlace - Tu nuevo dominio <b>%s</b> está siendo configurando. Tu dominio puede tardar hasta 30 minutos en empezar a funcionar. ¡Felicidades por tu compra! - Seleccionar el dominio - Dominios - Fija - Fijar la entrada en la portada - Marcar como fija - Dejar de seguir la conversación - Activar los avisos de la aplicación + Tu nuevo dominio <b>%s</b> está siendo configurando. Tu dominio puede tardar hasta 30 minutos en empezar a funcionar. Estás siguiendo esta conversación. Recibirás avisos por correo electrónico cuando se publiquen nuevos comentarios. - Gestionar las opciones para seguir la conversación, ventana emergente - No se han podido desactivar los avisos de la aplicación + Activar los avisos de la aplicación + Dejar de seguir la conversación + Marcar como fija + Fijar la entrada en la portada + Fija + Dominios + Seleccionar el dominio No se han podido activar los avisos de la aplicación - Desactivados los avisos de la aplicación - Activados los avisos de la aplicación - Cancelada la suscripción a esta conversación + No se han podido desactivar los avisos de la aplicación + Gestionar las opciones para seguir la conversación, ventana emergente Siguiendo esta conversación\n¿Activar los avisos de la aplicación? + Cancelada la suscripción a esta conversación + Activados los avisos de la aplicación + Desactivados los avisos de la aplicación + Con tu plan, tienes incluido el registro de dominio gratis durante un año Buscar un dominio Los dominios comprados en este sitio redirigirán a los visitantes a <b>%s</b> - Con tu plan, tienes incluido el registro de dominio gratis durante un año - Reclama tu dominio gratuito - Gestionar los dominios Añadir un dominio + Gestionar los dominios + Reclama tu dominio gratuito <span style=\"color:#d63638;\">Caduca el %s</span> - Caduca el %s - Dominios de tu sitio - Dirección principal del sitio Cambiar la dirección del sitio + Dirección principal del sitio + Dominios de tu sitio + Caduca el %s Tu dirección gratuita de WordPress.com es - <span style=\"color:#B26200;\">%1$s el primer año </span><span style=\"color:#50575e;\"><s>%2$s /año</s></span> %s<span style=\"color:#50575e;\"> /año</span> - ¿Quieres descartarlos? - Hay cambios sin guardar - El comentario no puede estar vacío - Correo electrónico del usuario no válido - Dirección web no válida - El nombre de usuario no puede estar vacío - Dirección de correo electrónico - Dirección web - Comentario - Nombre + <span style=\"color:#B26200;\">%1$s el primer año </span><span style=\"color:#50575e;\"><s>%2$s /año</s></span> Hecho - Pronto llegarán las vistas previas de los bloques incrustados - Resumen semanal - Opciones de incrustación - Doble toque para ver las opciones de incrustación. - ¡Sitio creado! Completa otra tarea. - <a href=\"\">A %1$s blogueros</a> les gusta. - <a href=\"\">A 1 bloguero</a> le gusta. - <a href=\"\">A ti y a %1$s blogueros</a> os gusta. + Nombre + Comentario + Dirección web + Dirección de correo electrónico + El nombre de usuario no puede estar vacío + Dirección web no válida + Correo electrónico del usuario no válido + El comentario no puede estar vacío + Hay cambios sin guardar + ¿Quieres descartarlos? + Pronto llegarán las vistas previas de los bloques incrustados + Resumen semanal + Doble toque para ver las opciones de incrustación. + Opciones de incrustación + ¡Sitio creado! Completa otra tarea. <a href=\"\">A ti y a 1 bloguero</a> os gusta. + <a href=\"\">A ti y a %1$s blogueros</a> os gusta. + <a href=\"\">A 1 bloguero</a> le gusta. + <a href=\"\">A %1$s blogueros</a> les gusta. <a href=\"\">A ti</a> te gusta. - Altura de la línea + Error desconocido al recuperar la plantilla recomendada de la aplicación Obtén tu dominio + Altura de la línea %s - Error desconocido al recuperar la plantilla recomendada de la aplicación - Respuesta recibida no válida + Dominios + Enlaces rápidos + Comparte WordPress con un amigo No se ha recibido ninguna respuesta + Respuesta recibida no válida Aplicaciones Automattic - Aplicaciones para cualquier pantalla - Comparte WordPress con un amigo - Enlaces rápidos - Dominios Repaso semanal: %s Hora del aviso Recibirás recordatorios para bloquear <b>todos los días</b> a las <b>%s</b>. @@ -590,86 +666,86 @@ Language: es Los cambios en la imagen destacada no se verán afectados por los botones de deshacer/rehacer. Aplica el ajuste Puedes reorganizar los bloques tocando un bloque y luego tocando las flechas arriba y abajo que aparecen en la parte inferior izquierda del bloque para moverlo encima o debajo de otros bloques. - Bienvenido al mundo de los bloques Para eliminar un bloque, selecciona el bloque y haz clic en los tres puntos de la parte inferior derecha del bloque para ver los ajustes. A partir de ahí, elige la opción para eliminar el bloque. - Algunos bloques tienen ajustes adicionales. Toca el icono de los ajustes en la parte inferior derecha del bloque para ver más opciones. + Bienvenido al mundo de los bloques Bloque %s, disponible nuevamente - Edición de texto enriquecido + Algunos bloques tienen ajustes adicionales. Toca el icono de los ajustes en la parte inferior derecha del bloque para ver más opciones. Una vez que te hayas familiarizado con los nombres de los diferentes bloques, puedes añadir un bloque escribiendo una barra inclinada seguida del nombre del bloque, por ejemplo, «/imagen» o «/encabezado». + Edición de texto enriquecido Haz que tu contenido destaque añadiendo imágenes, gifs, vídeos y medios incrustados a tus páginas. - ¡Pruébalo añadiendo unos cuantos bloques a tu entrada o página! Medio incrustado + ¡Pruébalo añadiendo unos cuantos bloques a tu entrada o página! Cada bloque tiene sus propios ajustes. Para encontrarlos, toca en un bloque. Sus ajustes aparecerán en la barra de herramientas de la parte inferior de la pantalla. - Crear diseños Los bloques son piezas de contenido que puedes insertar, reorganizar y dar estilo sin necesidad de saber programar. Los bloques son una forma fácil y moderna para que crees bonitos diseños. + Crear diseños Los bloques te permiten centrarte en la escritura de tu contenido, sabiendo que todas las herramientas de formato que necesitas están ahí para ayudarte a transmitir tu mensaje. Organiza tu contenido en columnas, añade botones de llamada a la acción y superpón imágenes con texto. - Añade un nuevo bloque en cualquier momento tocando el icono «+» en la barra de herramientas en la parte inferior izquierda. %1$s de %2$s completado - Aprende lo básico con un rápido recorrido. + Añade un nuevo bloque en cualquier momento tocando el icono «+» en la barra de herramientas en la parte inferior izquierda. Ha fallado la moderación de uno o más comentarios + Aprende lo básico con un rápido recorrido. Crear un sitio - Ten tu sitio activo y funcionando en solo unos rápidos pasos - Crea tu web WordPress - No se han podido activar las estadísticas del sitio - Activar las estadísticas del sitio Activa las estadísticas del sitio para ver información detallada sobre el tráfico, los «Me gusta», los comentarios y los suscriptores. - ¿Buscas las estadísticas? + Activar las estadísticas del sitio + No se han podido activar las estadísticas del sitio + Crea tu web WordPress + Ten tu sitio activo y funcionando en solo unos rápidos pasos ¿Qué es un bloque? + ¿Buscas las estadísticas? Estamos trabajando duro para añadir compatibilidad para vistas previas %s. Mientras tanto, puedes previsualizar el contenido incrustado en la entrada. Estamos trabajando duro para añadir compatibilidad para vistas previas %s. Mientras tanto, puedes previsualizar el contenido incrustado en la página. - No se ha podido incrustar el medio - Prueba otro término de búsqueda No se han encontrado bloques + Prueba otro término de búsqueda + No se ha podido incrustar el medio Todavía no están disponibles las vistas previas de %s Pronto llegarán las vistas previas del bloque incrustado %s - Toca dos veces para previsualizar la entrada. - Toca dos veces para previsualizar la página. Mostrado en la pestaña del navegador de tu visitante y en otros sitios online. + Toca dos veces para previsualizar la página. + Toca dos veces para previsualizar la entrada. Muéstrame el camino - ¿Quieres una pequeña ayuda para gestionar este sitio con la aplicación? - Crear un nuevo sitio - Puedes cambiar los sitios en cualquier momento. - Elige un sitio para abrir Lo sentimos, en este momento Jetpack Scan no es compatible con las instalaciones multisitio de WordPress. + Elige un sitio para abrir + Puedes cambiar los sitios en cualquier momento. + Crear un nuevo sitio + ¿Quieres una pequeña ayuda para gestionar este sitio con la aplicación? Los multisitios de WordPress no son compatibles URL no válida. Por favor, introduce una URL válida. - Leyenda incrustada. %s - Leyenda incrustada. Vacía - visita nuestra página de documentación Jetpack Backup para instalaciones multisitio proporciona copias de seguridad descargables, no restauraciones con un solo clic. Para más información, %1$s. + visita nuestra página de documentación + Leyenda incrustada. Vacía + Leyenda incrustada. %s Publicar regularmente puede ayudar a que tus lectores permanezcan implicados, y a atraer nuevos visitantes a tu sitio. Consejo Puedes actualizar esto en cualquier momento Selecciona los días en los que quieres bloguear Puedes actualizar esto en cualquier momento desde Mi sitio > Ajustes > Recordatorios de blogueo. No tienes configurado ningún recordatorio. - Recibirás recordatorios para bloguear %1$s a la semana el %2$s a las %3$s. ¡Recordatorios eliminados! ¡Todo configurado! + Recibirás recordatorios para bloguear %1$s a la semana el %2$s a las %3$s. Actualizar Nada configurado %s a la semana Configurar recordatorios - Configura recordatorios de blogueo los días que quieras publicar. Tu entrada se está publicando … mientras tanto puedes configurar recordatorios de blogueo los días que quiera publicar. + Configura recordatorios de blogueo los días que quieras publicar. Configura tus recordatorios de blogueo Este es tu recordatorio para crear algo hoy Es hora de bloguear en %s WordPress para iOS aún no es compatible con editar bloques reutilizables WordPress para Android aún no es compatible con editar bloques reutilizables Alternativamente, puedes separar y editar estos bloques por separado tocando en «Convertir en bloques normales». - Hecho Avísame + Hecho <a href=\"%1$s\">Introduce las credenciales de tu servidor</a> para activar las restauraciones del sitio con un clic de las copias de seguridad. - Establecer como imagen destacada - Eliminar como imagen destacada Crear una categoría Soporte de WordPress para Android + Eliminar como imagen destacada + Establecer como imagen destacada Gestiona las categorías de tu sitio + El contenido de la página de tus últimas entradas se genera automáticamente y no se puede editar. Categorías Recordatorios - El contenido de la página de tus últimas entradas se genera automáticamente y no se puede editar. Ajustes del borde No mostrar de nuevo Ver el almacenamiento @@ -692,8 +768,8 @@ Language: es Toca dos veces para abrir la hoja de acción para añadir imagen o vídeo La unidad actual es %s Entrada cruzada - %s convertido a bloque normal Ajustes de columnas + %s convertido a bloque normal Añadir enlace a %s Añadir texto del enlace Añadir una imagen o vídeo @@ -713,8 +789,8 @@ Language: es Mover la imagen hacia delante Mover la imagen hacia atrás Ajustes de anchura - «rel» del enlace Ajustes de columna + «rel» del enlace (Sin título) Sitio Información de hoja inferior del perfil de usuario @@ -724,19 +800,21 @@ Language: es Icono social %s Mención NUEVO - Previsualizar la entrada Previsualizar la página + Previsualizar la entrada Reintentar GIF Uno Vista previa no disponible + Añadir título Color del texto Relleno - Cuatro Destacado - URL personalizada + Cuatro Crear una incrustación + URL personalizada Columna %d + Más Describe brevemente el enlace para ayudar a los usuarios de lectores de pantalla Añadir bloques No se han encontrado sitios de Jetpack @@ -745,10 +823,10 @@ Language: es Transformar bloque… Fallo al insertar los medios. Fallo al insertar el archivo de audio. - Describe el propósito de la imagen. Déjalo vacío si la imagen es puramente decorativa. %1$s transformado a %2$s Error al cargar los datos de me gusta. %s %d me gusta + Describe el propósito de la imagen. Déjalo vacío si la imagen es puramente decorativa. 1 me gusta Sugerencia: Usar botón de icono @@ -756,13 +834,13 @@ Language: es Botón de búsqueda. El texto actual del botón es Bloques de búsqueda Etiqueta del bloque de búsqueda. El texto actual es - Exterior - No se ha establecido ningún marcador de posición personalizado Dentro + No se ha establecido ningún marcador de posición personalizado Ocultar el encabezado de búsqueda Doble toque para editar el texto del marcador de posición Doble toque para editar el texto de la etiqueta Doble toque para editar el texto del botón + Exterior doble toque para cambiar la unidad El texto de marcador de posición actual es Vaciar la búsqueda @@ -774,24 +852,24 @@ Language: es No hay ninguna red disponible. No hay ningún comentario sin responder Sin responder - AÑADIR ENLACE Ajustes de búsqueda - Direcciones IP permitidas siempre + AÑADIR ENLACE Comentarios no permitidos + Direcciones IP permitidas siempre Añadir el texto del botón Seguir temas - Una nueva forma de crear y publicar contenidos atrayentes en tu sitio. - Descartar Descargar + Descartar + Una nueva forma de crear y publicar contenidos atrayentes en tu sitio. Amenazas corregidas correctamente. Por favor, confirma que quieres corregir todas las %s amenazas activas. La exploración ha encontrado %1$s amenazas potenciales con %2$s. Por favor, revísalas a continuación y lleva a cabo alguna acción o toca el botón de corregir todo. Estamos %3$s si nos necesitas. Trabajamos duro para corregir estas amenazas en segundo plano. Mientras tanto puedes seguir usando tu sitio como siempre, puedes volver a comprobar el progreso en cualquier momento. Editar el punto focal - Toque doble para abrir la hoja del fondo para editar, reemplazar o vaciar la imagen - Toque doble para abrir la hoja de acción para editar, reemplazar o vaciar la imagen example.com Teclea un nombre para tu sitio + Toque doble para abrir la hoja del fondo para editar, reemplazar o vaciar la imagen + Toque doble para abrir la hoja de acción para editar, reemplazar o vaciar la imagen <b>Se han completado todas las tareas</b><br/>Has llegado a más gente. ¡Buen trabajo! <b>Se han completado todas las tareas</b><br/>Has personalizado tu sitio. ¡Bien hecho! Ocultar por ahora @@ -812,18 +890,18 @@ Language: es Enlace de invitación Se ha encontrado una amenaza Se han encontrado amenazas + <b>Exploración finalizada</b><br>No se han encontrado amenazas potenciales <b>Exploración finalizada</b><br>%s amenazas potenciales encontradas <b>Exploración finalizada</b><br>Una amenaza potencial encontrada - <b>Exploración finalizada</b><br>No se han encontrado amenazas potenciales - Corrigiendo la amenaza Desactivar - Desanclar + Corrigiendo la amenaza Revisa tus páginas y haz cambios, o añade o elimina páginas. - Ve tu sitio + Desanclar Descubre y sigue sitios que te inspiren. - Compartir socialmente Comparte automáticamente las nuevas entradas en tus medios sociales. Dale un nombre a tu sitio que refleje su personalidad y temática. + Compartir socialmente + Ve tu sitio Revisa las estadísticas de tu sitio Trataremos de crear un archivo de copia de seguridad descargable. No hemos podido encontrar el estado para decir cuánto tardará tu copia de seguridad descargable. @@ -836,33 +914,33 @@ Language: es Vaya, no hemos podido encontrar el estado de tu restauración No hemos podido restaura tu sitio Confirmar - ¿Estás seguro de querer revertir tu sitio al %1$s a las %2$s?\n Todo lo que hayas cambiado desde entonces se perderá. No hemos podido crear tu copia de seguridad (SQL) (excluye temas, plugins y subidas) + ¿Estás seguro de querer revertir tu sitio al %1$s a las %2$s?\n Todo lo que hayas cambiado desde entonces se perderá. Directorio wp-content Raíz de WordPress - Elementos incluidos en esta descarga Subiendo… + Elementos incluidos en esta descarga Reemplazar archivo Reemplazar audio Problema al abrir el audio - ABRIR Ninguna aplicación puede gestionar esta solicitud. Icono de candado Fallo al insertar el archivo de audio. Por favor, toca para ver las opciones. Toca dos veces para seleccionar un archivo de audio + ABRIR Toca dos veces para escuchar el archivo de audio Elegir audio Reproductor de audio - archivo de audio Leyenda del audio. %s Leyenda del audio. Vacía AÑADIR AUDIO Accede o regístrate con WordPress.com - Usasr este audio Elige un audio del dispositivo Opcional: Introduce un mensaje personalizado que enviar con tu invitación. + Usasr este audio + archivo de audio Aprende más sobre los perfiles Corregido Encontrado @@ -873,19 +951,19 @@ Language: es Bienvenido a la herramienta de exploración de Jetpack, estamos echándole un primer vistazo a tu web en estos momentos, te mostraremos los resultados enseguida. Trabajamos duro para corregir estas amenazas en segundo plano. Mientras tanto puedes seguir usando tu sitio como siempre, puedes volver a comprobar el progreso en cualquier momento. Te enviaremos un aviso si se encuentra una amenaza. Mientras tanto, no dudes en seguir usando tu sitio con normalidad, puedes comprobar el progreso en cualquier momento. - Corrigiendo amenazas Jetpack Scan no ha podido realizar un análisis de tu sitio. Comprueba si tu sitio está caído. Si no, vuelve a intentarlo. Si tu sitio está caído o si Jetpack Scan sigue teniendo problemas, ponte en contacto con nuestro equipo de soporte. + Corrigiendo amenazas Algo ha salido mal Haciendo copia de seguridad del sitio - Haciendo copia de seguridad del sitio desde %1$s %2$s Creando una copia de seguridad descargable + Haciendo copia de seguridad del sitio desde %1$s %2$s La copia de seguridad de tu sitio se ha realizado correctamente + Elegir audio La copia de seguridad de tu sitio se ha realizado correctamente\nHecha copia de seguridad desde %1$s %2$s La copia de seguridad de tu sitio se está realizando\nHaciendo copia de seguridad desde %1$s %2$s - Elegir audio - Hay otra restauración en curso. Icono de error Botón Listo + Hay otra restauración en curso. No se ha podido restaurar Botón Visitar sitio Botón Listo @@ -914,11 +992,11 @@ Language: es Tableta Dispositivos móviles No mostrar de nuevo - Anclar Selecciona %1$s Páginas %2$s para ver tu lista de páginas. Cambia, añade o elimina páginas en tu sitio. Revisar las páginas del sitio Selecciona %1$s Página de inicio %2$s para editar tu página de inicio. + Anclar Marcar como no leída Marcar como leída No se han podido subir los elementos multimedia.\n%1$s @@ -927,8 +1005,8 @@ Language: es Marcar entrada como no leída Marcar entrada como leída Se ha producido un error al comprobar el estado de la reparación. Ponte en contacto con el servicio de soporte. - La amenaza se ha corregido correctamente. Se ha producido un error al corregir las amenazas. Ponte en contacto con el servicio de soporte. + La amenaza se ha corregido correctamente. Por favor, confirma que quieres corregir una amenaza activa. Corregir todas las amenazas No se ha podido ignorar la amenaza. Ponte en contacto con el servicio de soporte. @@ -936,7 +1014,6 @@ Language: es No deberías ignorar un problema de seguridad a menos que estés absolutamente seguro de que no es dañino. Si eliges ignorar esta amenaza, seguirá en tu sitio <b>%s</b>. No se ha podido corregir la amenaza. Ponte en contacto con el servicio de soporte. Amenaza ignorada - Amenaza corregida el %s Corrigiendo la amenaza Se ha ignorado No se encontró ningún elemento @@ -949,25 +1026,26 @@ Language: es Prueba a ajustar el rango de fechas No se han encontrado copias de seguridad coincidentes Tu primera copia de seguridad estará disponible aquí en 24 horas y recibirás una notificación una vez que se haya completado + Amenaza corregida el %s Tu primera copia de seguridad estará lista pronto Ha ocurrido un problema al gestionar la petición. Por favor, inténtalo de nuevo más tarde. Mover al final Cambiar la posición del bloque Subir Icono - También hemos enviado un enlace a tu archivo. Botón de compartir enlace - Botón de descarga - Icono de copia de seguridad descargable lista + También hemos enviado un enlace a tu archivo. Compartir enlace Descargar + Botón de descarga + Icono de copia de seguridad descargable lista Hemos creado una copia de seguridad de tu sitio desde %1$s %2$s. Tu copia de seguridad ya está disponible para descargarla Tu copia de seguridad - No hace falta que esperes. Te avisaremos cuando la copia de seguridad esté lista Icono de copia de seguridad descargable en curso Estamos creando una copia de seguridad descargable de tu sitio desde %1$s %2$s. Se está creando una copia de seguridad descargable de tu sitio + No hace falta que esperes. Te avisaremos cuando la copia de seguridad esté lista Descargar copia de seguridad Hay otra descarga en curso. Ha ocurrido un problema al gestionar la petición. Por favor, inténtalo de nuevo más tarde. @@ -978,66 +1056,66 @@ Language: es %1$s · entrada cruzada usuario - No coincide con %s. - Ha ocurrido un problema al cargar las sugerencias. - No hay sugerencias %s disponibles. - Escribe algo para filtrar la lista de sugerencias. - Consigue un presupuesto gratuito Ignorar amenaza Corregir amenaza Jetpack Scan solucionará la amenaza. Jetpack Scan editará el archivo o el directorio afectados. Jetpack Scan se actualizará a una versión más reciente (%s). Jetpack Scan borrará el archivo o el directorio afectados. + Consigue un presupuesto gratuito + No coincide con %s. + Ha ocurrido un problema al cargar las sugerencias. + No hay sugerencias %s disponibles. + Escribe algo para filtrar la lista de sugerencias. Jetpack Scan reemplazará el archivo o el directorio afectados. Jetpack Scan no puede solucionar automáticamente esta amenaza.\n Te sugerimos que soluciones esta amenaza manualmente: asegúrate de que WordPress, tu tema y todos los plugins están actualizados y elimina el código, tema o plugin que esté causando problemas en tu sitio web. \n \n\n Si necesitas más ayuda para resolver esta amenaza, te recomendamos <b>Codeable</b>, una plataforma de profesionales de confianza, altamente cualificados, expertos en WordPress.\n Han hecho una selección de expertos en seguridad para ayudarnos con estos proyectos. Los precios oscilan entre 70–120 USD/hora y puedes obtener un presupuesto gratuito sin compromiso. Solucionando la amenaza + ¿Cuál fue el problema? + Información técnica ¿Cómo lo solucionó Jetpack? ¿Cómo vamos a repararlo? Amenaza detectada en el archivo: - Información técnica - ¿Cuál fue el problema? Detalles de la amenaza + Amenaza encontrada %s Se ha encontrado una vulnerabilidad en un tema Se ha encontrado una vulnerabilidad en un plugin - Amenaza encontrada %s Se ha encontrado una vulnerabilidad en WordPress Varias vulnerabilidades - Tema vulnerable: %1$s (versión %2$s) - Plugin vulnerable: %1$s (versión %2$s) %s: patrón de código malicioso Amenazas de base de datos %s + Tema vulnerable: %1$s (versión %2$s) + Plugin vulnerable: %1$s (versión %2$s) %s: archivo principal infectado Se ha encontrado una amenaza + este sitio Corregir todo en unos segundos hace %s minuto(s) hace %s hora(s) - este sitio La última exploración de Jetpack se ejecutó %1$s y no encontró ningún riesgo. %2$s + Copias de seguridad Puede que tu sitio web esté desprotegido No te preocupes Analizar de nuevo Analizar ahora Icono de estado del análisis - Copias de seguridad Filtro de tipo de actividad (%s tipos seleccionados) - %1$s (mostrando %2$s elementos) Filtro de tipo de actividad No se han registrado actividades en el rango de fechas seleccionado. No hay actividades disponibles Revisa tu conexión a Internet e inténtalo de nuevo. + %1$s (mostrando %2$s elementos) Sin conexión Tipo de actividad (%s) Filtro de rango de fechas Intenta ajustar los filtros de rango de fecha o de tipo de actividad No se han encontrado eventos coincidentes - Base de datos del sitio - (incluye wp-config.php y cualquier archivo que no sea de WordPress) Subidas de medios Plugins de WordPress Temas de WordPress Crea un icono de copia de seguridad descargable + (incluye wp-config.php y cualquier archivo que no sea de WordPress) + Base de datos del sitio Crear un archivo descargable Crear una copia de seguridad descargable Descargar copia de seguridad @@ -1056,31 +1134,31 @@ Language: es Duplicar La historia está siendo guardada, por favor, espera… Nombre del archivo - Ajustes del archivo del bloque - Fallo al subir los archivos.\nPor favor, toca para ver las opciones. - Fallo al guardar los medios.\nPor favor, toca para ver las opciones. - Editar el archivo - Copiar la URL del archivo ELEGIR UN ARCHIVO + Copiar la URL del archivo + Editar el archivo + Fallo al guardar los medios.\nPor favor, toca para ver las opciones. + Fallo al subir los archivos.\nPor favor, toca para ver las opciones. + Ajustes del archivo del bloque Elige un dominio - Ajustes de Jetpack Jetpack + Error al recuperar el estado de suscripción para la entrada + No se ha podido crear la suscripción a los comentarios de esta entrada + No se ha podido anular la suscripción a los comentarios de esta entrada Siguiendo la conversación por correo electrónico Seguir la conversación por correo electrónico - No se ha podido anular la suscripción a los comentarios de esta entrada - No se ha podido crear la suscripción a los comentarios de esta entrada - Error al recuperar el estado de suscripción para la entrada + Ajustes de Jetpack Respuesta recibida no válida No se ha recibido ninguna respuesta Vaciar Aplicar Una o más diapositivas no se han añadido a tu historia porque en este momento las historias no son compatibles con archivos GIF. Por favor, elige una imagen estática o un vídeo de fondo en su lugar. - Los archivos GIF no son compatibles - No hemos podido encontrar en el sitio los medios para esta historia. No se puede editar la historia No ha sido posible subir medios a esta historia. Comprueba tu conexión a Internet e inténtalo de nuevo dentro de un momento. No se puede editar la historia Esta historia se ha editado en un dispositivo diferente y la posibilidad de editar ciertos objetos puede estar limitada. + No hemos podido encontrar en el sitio los medios para esta historia. + Los archivos GIF no son compatibles Edición limitada de la historia Los medios han sido eliminados. Intenta volver a crear tu historia. Fondo @@ -1091,10 +1169,10 @@ Language: es Hecho Siguiente Borrar - Se ha producido un error al elegir el tema. Por favor, revisa tu conexión a Internet e inténtalo de nuevo. Toca en reintentar cuando vuelvas a estar conectado. Los diseños no están disponibles sin conexión + Se ha producido un error al elegir el tema. Continuar con las credenciales de la tienda Encuentra tu correo electrónico conectado Seguir temas @@ -1102,9 +1180,9 @@ Language: es No hay entradas recientes ¡Bienvenido! Explorar - <b>Juan Gómez</b> ha respondido en tu entrada - Hoy has recibido <b>50 me gusta</b> en tu sitio A <b>Madison Ruíz</b> le ha gustado tu entrada + Hoy has recibido <b>50 me gusta</b> en tu sitio + <b>Juan Gómez</b> ha respondido en tu entrada Se ha abierto el menú de bloques desplazable. Selecciona un bloque. Se ha cerrado el menú de bloques desplazable. Elegir @@ -1131,47 +1209,46 @@ Language: es Pamela Nguyen Estoy muy inspirado por el trabajo del fotógrafo Cameron Karsten. Probaré estas técnicas en mi próximo Inspírate - Sigue tus sitios favoritos y descubre nuevos blogs. Observa cómo crece tu audiencia con analíticas avanzadas. - Mira los comentarios y avisos en tiempo real. Con el potente editor puedes publicar sobre la marcha. Bienvenido al maquetador web más popular del mundo. + Mira los comentarios y avisos en tiempo real. + Sigue tus sitios favoritos y descubre nuevos blogs. La carga del medio ha fallado Sitios a seguir Estamos trabajando duro para añadir más bloques con cada versión. «%s» no es totalmente compatible - Botón de ayuda - Editar usando el editor web - Elegir las imágenes - Crear una entrada de historia Son publicados como una nueva entrada de blog en tu sitio para que tu audiencia nunca se pierda nada. - Las entradas de historias no desaparecen + Crear una entrada de historia + Elegir las imágenes + Editar usando el editor web + Botón de ayuda Combina fotos, vídeos y texto para crear entradas de historias atractivas y accesibles que les encantarán a tus visitantes. - Ahora las historias son para todos - Título de la entrada de historia - Cómo crear una entrada de historias - Presentación de las entradas de historias - Página en blanco creada + Las entradas de historias no desaparecen Página creada - %1$s ha denegado el acceso a tus fotos. Para corregirlo, edita tus permisos y activa %2$s y %3$s. - Inserción del medio fallida. - Ha fallado la inserción del medio: %s + Página en blanco creada + Presentación de las entradas de historias + Cómo crear una entrada de historias + Título de la entrada de historia + Ahora las historias son para todos Elige desde la biblioteca de medios de WordPress + Ha fallado la inserción del medio: %s + Inserción del medio fallida. Volver + por Primeros pasos Sigue temáticas para descubrir nuevos blogs - por - Este referido no puede ser marcado como spam - Desmarcar como spam - Marcar como spam - Abrir la web - Subiendo medios GIF - Subiendo medios de inventarios Subiendo medios - Busca o escribe la URL - Añadir este enlace de teléfono - Añadir este enlace + Subiendo medios de inventarios + Subiendo medios GIF + Abrir la web + Marcar como spam + Desmarcar como spam + Este referido no puede ser marcado como spam + Busca o escribe la URL + Añadir este enlace de teléfono Añadir este enlace de correo electrónico + Añadir este enlace No hay conexión a Internet.\nNo están disponibles las sugerencias. Negrita Moderno @@ -1188,126 +1265,125 @@ Language: es No se puede mostrar este comentario Navegar por elementos Informar de esta entrada - Bienvenido al Lector. Descubre millones de blogs a tu alcance. - Ha ocurrido un error interno del servidor - Tu acción no está permitida %1$s elementos más + Tu acción no está permitida + Ha ocurrido un error interno del servidor + Bienvenido al Lector. Descubre millones de blogs a tu alcance. Seleccionar un diseño Nota: el diseño de la columna puede variar entre temas y tamaños de pantalla - Crear una entrada o historia - Crear una página Crear una entrada - Puede que te guste + Crear una página + Crear una entrada o historia Ocultar - Leyenda del vídeo. Vacía - Actualiza el título. - Pegar el bloque después - Título de la página. %s - Título de la página. Vacío - Ha ocurrido un error al reproducir tu vídeo - Este dispositivo no es compatible con la API de Camera2 - No se ha podido guardar el vídeo - Error al guardar la imagen + Puede que te guste + Ver el almacenamiento Operación en progreso, inténtalo de nuevo + Error al guardar la imagen + No se ha podido guardar el vídeo + Este dispositivo no es compatible con la API de Camera2 + Ha ocurrido un error al reproducir tu vídeo + Título de la página. Vacío + Título de la página. %s + Pegar el bloque después + Actualiza el título. + Leyenda del vídeo. Vacía No se ha podido encontrar la diapositiva de la historia - Ver el almacenamiento - Tenemos que guardar la historia en tu dispositivo antes de que pueda ser publicada. Revisa tus ajustes de almacenamiento y elimina archivos para ganar espacio. - Insuficiente almacenamiento en el dispositivo - Intenta volver a guardar o borrar las diapositivas y, después, intenta volver a publicar tu historia. - No se han podido guardar %1$d diapositivas - No se ha podido guardar 1 diapositiva - Gestionar - %1$d diapositivas necesitan una acción 1 diapositiva necesita una acción - No se ha podido subir «%1$s» - No se ha podido subir «%1$s» - Publicado «%1$s» + %1$d diapositivas necesitan una acción + Gestionar + No se ha podido guardar 1 diapositiva + No se han podido guardar %1$d diapositivas + Intenta volver a guardar o borrar las diapositivas y, después, intenta volver a publicar tu historia. + Insuficiente almacenamiento en el dispositivo + Tenemos que guardar la historia en tu dispositivo antes de que pueda ser publicada. Revisa tus ajustes de almacenamiento y elimina archivos para ganar espacio. Subiendo «%1$s»… - Quedan %1$d diapositivas - Queda 1 diapositiva - varias historias + Publicado «%1$s» + No se ha podido subir «%1$s» + No se ha podido subir «%1$s» Guardando «%1$s»… - Sin título - Descartar - La entrada de tu historia no se guardará como borrador. - ¿Descartar la entrada de la historia? - Borrar - Esta diapositiva aún no ha sido guardada. Si borras esta diapositiva, perderás cualquier edición que hayas hecho. - Esta diapositiva será eliminada de tu historia. - ¿Borrar la diapositiva de la historia? - Cambiar el color del texto - Cambiar la alineación del texto - con error - seleccionado + varias historias + Queda 1 diapositiva + Quedan %1$d diapositivas sin seleccionar - Diapositiva - Reintentar - Guardado - Cerrar - Compartir con - COMPARTIR - Guardado en fotos - Reintentar - Guardado - Guardando - Flash - Girar - Sonido - Texto - Pegatinas + seleccionado + con error + Cambiar la alineación del texto + Cambiar el color del texto + ¿Borrar la diapositiva de la historia? + Esta diapositiva será eliminada de tu historia. + Esta diapositiva aún no ha sido guardada. Si borras esta diapositiva, perderás cualquier edición que hayas hecho. + Borrar + ¿Descartar la entrada de la historia? + La entrada de tu historia no se guardará como borrador. + Descartar + Sin título + Toca crear %1$s. %2$s Después selecciona <b>Entrada del blog</b> + Pon un título a tu historia + Empieza eligiendo entre una amplia variedad de diseños de página prefabricados. O simplemente empieza con una página en blanco. + Crear una página en blanco + Crear una página + Vista previa + Capturar Flash + Pegatinas + Texto + Sonido Girar la cámara - Capturar - Vista previa - Crear una página - Crear una página en blanco - Empieza eligiendo entre una amplia variedad de diseños de página prefabricados. O simplemente empieza con una página en blanco. - Elegir un diseño - Pon un título a tu historia - Crear una entrada o historia + Girar + Flash + Guardando + Guardado + Reintentar + Guardado en fotos + COMPARTIR + Compartir con + Cerrar + Guardado + Reintentar + Diapositiva Crear una entrada, página o historia - Toca crear %1$s. %2$s Después selecciona <b>Entrada del blog</b> - Elegir el dispositivo + Crear una entrada o historia + Elegir un diseño + Cuota de almacenamiento superada + No se puede subir el archivo.\nSe ha superado la cuota de almacenamiento. + No se ha podido encontrar el salto de página enlazado Entrada de la historia + Elegir el dispositivo Para la edición de los iconos del sitio en sitios WordPress autoalojados se necesita el plugin Jetpack. - No se ha podido encontrar el salto de página enlazado - No se puede subir el archivo.\nSe ha superado la cuota de almacenamiento. - Cuota de almacenamiento superada Añadir un archivo - Reemplazar el vídeo Reemplazar la imagen o vídeo - Convertir en enlace - Elegir un vídeo - Elegir una imagen o vídeo - Elegir una imagen + Reemplazar el vídeo + Si continúas con Google y aún no tienes una cuenta de WordPress.com, crearás una cuenta y aceptas nuestros %1$stérminos del servicio%2$s. + Confirmación del registro Bloque eliminado + Elegir una imagen + Elegir una imagen o vídeo + Elegir un vídeo Introduce la dirección de tu sitio existente - Confirmación del registro - Si continúas con Google y aún no tienes una cuenta de WordPress.com, crearás una cuenta y aceptas nuestros %1$stérminos del servicio%2$s. + Convertir en enlace Si continúas, aceptas nuestros %1$stérminos del servicio%2$s. Usaremos esta dirección de correo electrónico para crear tu nueva cuenta de WordPress.com. Te hemos enviado por correo electrónico un enlace de registro para crear tu nueva cuenta de WordPress.com. Comprueba tu correo electrónico en este dispositivo y toca el enlace en el correo electrónico que has recibido de WordPress.com. Introduce la información de tu cuenta para %1$s. - o - Continuar con Google - Encuentra la dirección de tu sitio - Hecho - ¿No ves el correo electrónico? Comprueba tu carpeta de spam o correo no deseado. Comprueba tu correo electrónico en este dispositivo y toca el enlace en el correo electrónico que has recibido de WordPress.com. + ¿No ves el correo electrónico? Comprueba tu carpeta de spam o correo no deseado. + Hecho + Encuentra la dirección de tu sitio + Continuar con Google + o Te enviaremos por correo electrónico un enlace que te hará acceder automáticamente, sin necesidad de contraseña. - Comprobar el correo electrónico - Primeros pasos - Introduce tu dirección de correo electrónico para acceder o crear una cuenta de WordPress.com. - O escribe tu contraseña - Crear una cuenta - Enviar el enlace por correo electrónico Restablecer tu contraseña + Enviar el enlace por correo electrónico + Crear una cuenta + O escribe tu contraseña + Introduce tu dirección de correo electrónico para acceder o crear una cuenta de WordPress.com. + Primeros pasos + Comprobar el correo electrónico Ha habido un problema al gestionar la solicitud. Por favor, inténtalo de nuevo más tarde. - Comprueba el título de tu sitio Toca <b>%1$s</b> para configurar un nuevo título + Comprueba el título de tu sitio Al enviar esta entrada a la papelera también se descartarán los cambios locales, ¿estás seguro de que quieres continuar? Opciones del bloque %s - Eliminar el bloque Duplicar bloque Copiar bloque Bloque copiado @@ -1316,10 +1392,11 @@ Language: es Bloque cortado Bloque copiado El título del sitio solo puede ser cambiado por un usuario con el perfil de administrador. + Eliminar el bloque El título del sitio se muestra en la barra de título de un navegador web y en la cabecera de la mayoría de los temas. - Tema No se ha podido actualizar el título del sitio. Comprueba tu conexión de red e inténtalo nuevamente. Cambios sin guardar + Tema Abrir el enlace en un navegador Navega a la hoja de contenido anterior Navega para personalizar el degradado @@ -1336,7 +1413,6 @@ Language: es Descartar No establecido Las etiquetas ayudan a los lectores diciéndoles de qué se trata la entrada. - Fecha de publicación Añadir etiquetas Volver Guardar ahora @@ -1348,9 +1424,8 @@ Language: es Cancelar Mover a borrador Las entradas en la papelera no se pueden editar. ¿Deseas cambiar el estado de esta entrada a «borrador» para poder trabajar en ella? + Fecha de publicación ¿Mover entrada a borradores? - Elige tus temáticas - Elige tus temáticas Hecho Selecciona algunos para continuar Publicado @@ -1359,51 +1434,53 @@ Language: es Fecha de publicación Lee el aviso de privacidad de CCPA La Ley de Privacidad del Consumidor de California («CCPA») nos obliga a que proporcionemos información adicional a los residentes de California sobre las categorías de información personal que recopilamos y compartimos, dónde obtenemos esa información personal y cómo y por qué la usamos. + Elige tus temáticas + Elige tus temáticas Aviso de privacidad para usuarios de California Estado y visibilidad Actualizar ahora Abrir el menú de acciones de bloques Mover arriba - Insertar una mención - Toca dos veces para abrir la hoja inferior con las opciones disponibles Toca dos veces pata abrir la hoja de acción con las opciones disponibles - No podemos abrir las páginas en este momento. Por favor, inténtalo de nuevo más tarde - Establecer como página de entradas + Toca dos veces para abrir la hoja inferior con las opciones disponibles + Insertar una mención + La página de inicio seleccionada y la página de entradas no pueden ser la misma. + Blog clásico + Página de inicio estática + Página de entradas + Seleccionar la página Establecer como página de inicio + Establecer como página de entradas + No podemos abrir las páginas en este momento. Por favor, inténtalo de nuevo más tarde %1$s no es una %2$s válida - Seleccionar la página - Página de entradas - Página de inicio estática - Blog clásico - La página de inicio seleccionada y la página de entradas no pueden ser la misma. - Ha fallado la actualización de la página de inicio, comprueba tu conexión a internet - No se pueden guardar los ajustes de la página de inicio antes de que las páginas estén cargadas - No se pueden guardar los ajustes de la página de inicio - Aceptar - Ha fallado la carga de las páginas - Elige entre una página de inicio que muestre tus últimas publicaciones (blog clásico) o una página fija/estática. Ajustes de la página de inicio + Elige entre una página de inicio que muestre tus últimas publicaciones (blog clásico) o una página fija/estática. + Ha fallado la carga de las páginas + Aceptar + No se pueden guardar los ajustes de la página de inicio + No se pueden guardar los ajustes de la página de inicio antes de que las páginas estén cargadas + Ha fallado la actualización de la página de inicio, comprueba tu conexión a internet + Para establecer la página de inicio, activa «Página de inicio estática» en los ajustes del sitio + Para establecer la página de entradas, activa «Página de inicio estática» en los ajustes del sitio + Página de inicio actualizada correctamente + Ha fallado la actualización de la página de inicio + Página de entradas actualizada correctamente Página de inicio Ha fallado la actualización de la página de entradas - Página de entradas actualizada correctamente - Ha fallado la actualización de la página de inicio - Página de inicio actualizada correctamente - Para establecer la página de entradas, activa «Página de inicio estática» en los ajustes del sitio - Para establecer la página de inicio, activa «Página de inicio estática» en los ajustes del sitio Seleccionar un color - Toca dos veces para ir a los ajustes del color Cuando sigas sitios, verás aquí su contenido + Toca dos veces para ir a los ajustes del color Saber más - Qué hay de nuevo en %s Insertar %d recortar Fallo al cargar en el archivo, por favor, inténtalo de nuevo. Vista previa de la miniatura de la imagen - Usar este medio Usar este vídeo Elegir el medio Elegir el vídeo + Usar este medio No se ha podido seleccionar el sitio. Por favor, inténtalo de nuevo. + Qué hay de nuevo en %s Continuar Ha fallado reblog Gestionar los sitios @@ -1424,13 +1501,13 @@ Language: es Mover bloque a la izquierda Toca dos veces para mover el bloque hacia la derecha Toca dos veces para mover el bloque hacia la izquierda - Ajustes del bloque Creando el escritorio Configurar el tema Añadiendo las características del sitio Obteniendo la URL del sitio Tu sitio estará listo en breve ¡Hurra!\nCasi está hecho + Ajustes del bloque Cancelar la subida Ha habido un problema al gestionar la petición Funciona con Tenor @@ -1445,12 +1522,12 @@ Language: es Ha fallado el acceso al contenido de un sitio privado. Algunos medios pueden no estar disponibles Accediendo al contenido de un sitio privado Fallo al recortar y guardar la imagen, por favor, inténtalo de nuevo. - Fallo al cargar la imagen.\nPor favor, toca para volver a intentarlo. Previsualizar la imagen Formato de página desconocido No hemos podido completar esta acción y no se ha enviado esta página a revisión. No hemos podido completar esta acción y no se ha programado esta página. No hemos podido completar esta acción y no se ha publicado esta página privada. + Fallo al cargar la imagen.\nPor favor, toca para volver a intentarlo. No hemos podido completar esta acción y no se ha publicado esta página. No hemos podido enviar esta página a revisión, pero lo intentaremos de nuevo más tarde. No hemos podido programar esta página, pero lo intentaremos de nuevo más tarde. @@ -1465,20 +1542,20 @@ Language: es Programaremos tu página cuando tu dispositivo vuelva a estar online. Enviaremos tu página para revisión cuando tu dispositivo vuelva a estar online. Publicaremos la página cuando tu dispositivo vuelva a estar online. + Has hecho cambios no guardados en esta página Página en espera Subiendo la página El dispositivo está desconectado. La página se ha guardado localmente. - Has hecho cambios no guardados en esta página Tu página se está subiendo - La página ha fallado al subir los medios y ha sido guardada localmente Página guardada en el dispositivo La página se ha guardado online - Selecciona un blog para el atajo a QuickPress - Establecido por el ahorrador de batería Oscuro Claro Apariencia Recientemente has hecho cambios en esta página, pero no los has guardado. Elige una versión para cargar:\n\n + Establecido por el ahorrador de batería + La página ha fallado al subir los medios y ha sido guardada localmente + Selecciona un blog para el atajo a QuickPress Mostrar el contenido de la entrada Mostrar solo el extracto Enlazar a @@ -1493,8 +1570,8 @@ Language: es En la papelera Programada Publicada - La conexión con Facebook no puede encontrar ninguna página. Jetpack Social no puede conectar con perfiles de Facebook, solo con páginas publicadas. No conectado + La conexión con Facebook no puede encontrar ninguna página. Jetpack Social no puede conectar con perfiles de Facebook, solo con páginas publicadas. Me gusta Seguimientos Comentarios @@ -1504,25 +1581,25 @@ Language: es Actividad Entradas y páginas General - Añadir una nueva tarjeta Añadir una nueva tarjeta de estadísticas + Añadir una nueva tarjeta Usa el botón de filtro para encontrar entradas sobre temas específicos Selecciona una etiqueta o sitio, ventana emergente Selecciona un sitio o etiqueta para filtrar entradas Quitar el filtro actual - Gestionar temas y sitios Acceder a WordPress.com - Accede a WordPress.com para ver las últimas entradas de los temas que sigues + Gestionar temas y sitios Accede a WordPress.com para ver las últimas entradas de los sitios que sigues + Accede a WordPress.com para ver las últimas entradas de los temas que sigues Reemplazar el bloque actual Añadir al final Añadir al principio Añadir el bloque antes Añadir el bloque después - Añadir un tema Seguir un sitio - Puedes seguir entradas sobre un tema específico añadiendo un tema Mira las entradas más recientes de los sitios que sigues + Añadir un tema + Puedes seguir entradas sobre un tema específico añadiendo un tema Siguiendo Filtrar Leyenda del vídeo. %s @@ -1534,10 +1611,10 @@ Language: es Has escuchado todas las estadísticas de este período.\n Si vuelves a tocar, se reiniciará desde el principio. No hay estadísticas en este período. Actividad de publicación para %1$s - Los días con visitas %1$s para %2$s son: %2$s %3$s. Toca para más. explora todas las estadísticas para este período muy altas altas + Los días con visitas %1$s para %2$s son: %2$s %3$s. Toca para más. medias bajas   y %1$d %2$s @@ -1566,22 +1643,22 @@ Language: es No hemos podido acceder a tu sitio porque necesita <b>identificación HTTP</b>. Tendrás que contactar con tu alojamiento para solucionarlo. No hemos podido acceder en tu sitio al <b>archivo XMLRCP</b>. Tendrás que contactar con tu alojamiento para solucionarlo. ¡Ya casi estamos! Solo necesitamos verificar tu dirección de correo electrónico conectada a Jetpack <b>%1$s</b> - Accede con las credenciales de tu sitio %1$s - Página del sitio Siguiendo - Me gusta - Descubrir - Guardado - Temas - Sitios - %sE - %sP + Página del sitio + Accede con las credenciales de tu sitio %1$s + No podemos abrir las entradas en este momento. Por favor, inténtalo de nuevo más tarde + %sM %sT + Sitios + Guardado + Descubrir + Me gusta + No podemos cargar los datos para tu sitio en este momento. Por favor, inténtalo de nuevo más tarde %sG - %sM + %sP + %sE + Temas %sK - No podemos abrir las entradas en este momento. Por favor, inténtalo de nuevo más tarde - No podemos cargar los datos para tu sitio en este momento. Por favor, inténtalo de nuevo más tarde Biblioteca de medios de WordPress No compatible Desagrupar @@ -1607,12 +1684,12 @@ Language: es Mover el bloque arriba Mover el bloque hacia abajo, de la fila %1$s a la fila %2$s Mover el bloque abajo - Texto del enlace Enlace insertado Leyenda de la imagen. %s Ocultar el teclado Icono de ayuda Toca dos veces para deshacer el último cambio + Texto del enlace Toca dos veces para alternar los ajustes Toca dos veces para seleccionar una imagen Toca dos veces para seleccionar un vídeo @@ -1629,10 +1706,11 @@ Language: es Texto alternativo AÑADIR UN VÍDEO Añadir la URL - Añadir el texto alternativo AÑADIR UNA IMAGEN O UN VÍDEO AÑADIR UNA IMAGEN AÑADIR EL BLOQUE AQUÍ + Añadir el texto alternativo + Añadir descripción Toca el botón «Añadir a las entradas guardadas» para guardar una entrada en tu lista. La lista se ha cargado con %1$d elementos. Avisos @@ -1670,11 +1748,11 @@ Language: es Has hecho cambios no guardados en esta entrada La versión desde esta aplicación La versión desde otro dispositivo - Desde esta aplicación\nGuardado en %1$s\n\nDesde otro dispositivo\nGuardado en %2$s\n Recientemente has hecho cambios en esta entrada, pero no los has guardado. Elige una versión para cargar:\n\n ¿Qué versión te gustaría editar? Borrar permanentemente No guardaremos los últimos cambios en tu borrador. + Desde esta aplicación\nGuardado en %1$s\n\nDesde otro dispositivo\nGuardado en %2$s\n No programaremos estos cambios. No enviaremos estos cambios para revisión. No publicaremos estos cambios. @@ -1710,8 +1788,8 @@ Language: es Archivo Descargas de archivos Las estadísticas de descarga de archivos no se registraron antes del 28 de Junio de 2019. - Zona horaria del sitio (UTC -%s) Zona horaria del sitio (UTC +%s) + Zona horaria del sitio (UTC -%s) Zona horaria del sitio (UTC) Escritorio Por defecto @@ -1720,9 +1798,9 @@ Language: es Compartir Volver Avanzar - «%1$s» programado para publicar el «%2$s» en tu aplicación de %3$s\n%4$s Entrada programada de WordPress: «%s» «%s» se publicará en 10 minutos + «%1$s» programado para publicar el «%2$s» en tu aplicación de %3$s\n%4$s «%s» se publicará en 1 hora «%s» ha sido publicado Entrada programada: recordatorio de 10 minutos @@ -1732,9 +1810,9 @@ Language: es Cuando se publique 10 minutos antes 1 hora antes - Desactivado Añadir al calendario Aviso + Desactivado Fecha y hora Por favor, introduce una dirección completa de una web, como example.com. Accede con WordPress.com para conectar con %1$s @@ -1744,15 +1822,15 @@ Language: es Elemento contraído Elemento expandido Contraer - Ampliar Gráfico actualizado. %1$s %2$s del período: %3$s, cambio desde el período anterior - %4$s Cargando los datos de la tarjeta seleccionada Editor - Ampliar + Ampliar Cerrar Verifica tu dirección de correo electrónico - las instrucciones se enviaron a tu correo electrónico Verifica tu dirección de correo electrónico - las instrucciones se enviaron a %s + Ampliar Cancelar Aceptar http(s):// @@ -1785,22 +1863,21 @@ Language: es Histórico Visitas esta semana Añadir widget - Está tardando más tiempo del normal recargar los detalles del plugin. Por favor, compruébalo de nuevo más tarde. Si acabas de registrar un nombre de dominio, por favor, espera hasta que terminemos de configurarlo e inténtalo de nuevo.\n\nEn caso contrario, parece que algo fue mal y la característica del plugin podría no estar disponible para este sitio. + Está tardando más tiempo del normal recargar los detalles del plugin. Por favor, compruébalo de nuevo más tarde. Estado (no disponible) Al registrar este dominio aceptas nuestros %1$stérminos y condiciones%2$s Comprueba tu conexión a la red e inténtalo de nuevo. No se ha podido cargar esta página en este momento. - No se pudieron recuperar los ajustes. Algunas APIs no están disponibles para la cuenta e ID de esta aplicación OAuth. Al configurar Jetpack aceptas nuestros %1$stérminos y condiciones%2$s + No se pudieron recuperar los ajustes. Algunas APIs no están disponibles para la cuenta e ID de esta aplicación OAuth. No hay ninguna conexión. La edición está desactivada. - Para volver a conectar la aplicación con tu sitio alojado, introduce aquí la nueva contraseña del sitio. Contraseña actualizada Actualizar contraseña + Para volver a conectar la aplicación con tu sitio alojado, introduce aquí la nueva contraseña del sitio. Registrando el nombre de dominio… Selecciona la provincia Selecciona el país - Registrar un dominio Código postal Provincia Ciudad @@ -1809,6 +1886,7 @@ Language: es País Código del país Teléfono + Registrar un dominio Organización (opcional) Para tu comodidad, hemos precompletado tu información de contacto\n de WordPress.com. Por favor, comprueba que es la información correcta que quieres usar para este dominio. Información de contacto del dominio @@ -1827,35 +1905,34 @@ Language: es Fallo al insertar los medios.\nPor favor, toca para volver a intentarlo. Tu borrador se está subiendo Subiendo borrador - Borradores Ocurrió un error mientras se restauraba la entrada - Retroceder a: %s - Solo ves las estadísticas más relevantes. Añade y organiza tus detalles abajo. + Borradores Social Estadísticas anuales del sitio - Seguidores totales + Registrar dominio + Solo ves las estadísticas más relevantes. Añade y organiza tus detalles abajo. + Retroceder a: %s No se pudieron cargar las sugerencias de dominios Teclea una palabra clave para más ideas No se han encontrado sugerencias - Registrar dominio - Ahora que está instalado Jetpack, solo necesitamos que lo configures. Esto solo te llevará un minuto. - Quitar de los detalles + Seguidores totales Mover abajo Mover arriba - Ajustes de los parámetros de las estadísticas - La entrada se está moviendo a borradores - La entrada se está restaurando - Entrada restaurada + Cambios locales La entrada se está enviando a la papelera + Entrada restaurada + La entrada se está restaurando + La entrada se está moviendo a borradores + Quitar de los detalles + Ajustes de los parámetros de las estadísticas Al enviar esta entrada a la papelera también se descartarán los cambios sin guardar, ¿estás seguro de querer continuar? - Cambios locales - Mover a borradores - Cambiar a la vista de lista - Cambiar a la vista de tarjetas - No tienes ninguna entrada en la papelera - No tienes ninguna entrada en borrador - No tienes ninguna entrada programada Aún no has publicado ninguna entrada + No tienes ninguna entrada programada + No tienes ninguna entrada en borrador + No tienes ninguna entrada en la papelera + Cambiar a la vista de tarjetas + Cambiar a la vista de lista + Mover a borradores Por favor, accede con tu nombre de usuario y contraseña. Por favor, accede usando tu nombre de usuario de WordPress.com en vez de tu dirección de correo electrónico. Promedio de palabras/entrada @@ -1866,17 +1943,15 @@ Language: es Total de comentarios Entradas Año - Este año El sitio de esta dirección no es un sitio WordPress. Para que nos podamos conectar el sitio debe usar WordPress. + Este año Fallo al comprobar los créditos de dominio disponibles Comprobando créditos de dominio Registrar dominio Para instalar plugins necesitas tener un dominio personalizado asociado a tu sitio. Instalar plugin - Podrás personalizar la apariencia de tu sitio más tarde - Publicar el: %s - Programada para el: %s Publicado el: %s + Publicar el: %s Programado para el: %s Últimas semanas Vistas medias por dia @@ -1884,18 +1959,20 @@ Language: es Período Meses y años Cargar más + Podrás personalizar la apariencia de tu sitio más tarde + Programada para el: %s Hoy Mejor hora Mejor día Estadísticas del: No, gracias - Más tarde Valorar ahora + Más tarde ¡Qué gusto verte de nuevo! Si estás trabajando con la aplicación nos encantaría que nos puntuases en la Google Play Store. - Entrada devuelta a borrador - Actividad de publicación El sitio no se ha cargado todavía + Entrada devuelta a borrador Más entradas + Actividad de publicación Menos entradas Puedes perder lo que llevas hecho. ¿Estás seguro de que quieres salir? Ver los planes @@ -1904,33 +1981,33 @@ Language: es No podemos cargar los planes en este momento. Por favor, inténtalo de nuevo. No se pueden cargar los planes No hay conexión - Cambiar al editor de bloques Hubo un problema al cargar tus datos, recarga la página e inténtalo de nuevo. Datos no cargados Edita las nuevas entradas y páginas con el editor de bloques Usar editor de bloques + Cambiar al editor de bloques salir + Siguientes pasos + Tus visitantes verán tu icono en su navegador. Añade un icono personalizado para conseguir un aspecto profesional y refinado. Haz crecer tu audiencia Personaliza tu sitio - Siguientes pasos Elige un icono del sitio único - Tus visitantes verán tu icono en su navegador. Añade un icono personalizado para conseguir un aspecto profesional y refinado. - Selecciona las %1$s estadísticas %2$s para ver cómo está rindiendo tu sitio. Toca en %1$s Icono de tu sitio %2$s para subir uno nuevo Guarda en borrador y publica una entrada. + Selecciona las %1$s estadísticas %2$s para ver cómo está rindiendo tu sitio. Activar compartir entradas Comparte automáticamente las nuevas entradas en tus cuentas de medios sociales. Revisa las estadísticas de tu sitio - Mantente al día sobre el rendimiento de tu sitio. - Eliminar los siguientes pasos ocultará todas las visitas guiadas de este sitio. Esta acción es irreversible. Quitar los siguientes pasos Quitar esto + Eliminar los siguientes pasos ocultará todas las visitas guiadas de este sitio. Esta acción es irreversible. + Mantente al día sobre el rendimiento de tu sitio. Saltar tarea Recordatorio Elige el siguiente periodo Elige el periodo anterior - %1$s de visitas Hora más popular + %1$s de visitas %1$s (%2$s) +%1$s (%2$s) Mostrando la vista previa @@ -1939,19 +2016,19 @@ Language: es Cancelar el asistente de creación de sitios Estamos creando tu nuevo sitio Hubo un problema - Crear sitio Crear sitio Aquí es donde la gente te encontrará en Internet. + Crear sitio No hay direcciones disponibles que coincidan con tu búsqueda Error durante la comunicación con el servidor. Inténtalo de nuevo Hubo un problema Hubo un problema + Conflicto de versiones ¡Se ha creado tu sitio! %1$d de %2$d Crear sitio Sugerencias actualizadas No se ha podido seleccionar el sitio auto-hospedado que acabas de añadir - Conflicto de versiones Activa los informes de errores automáticos para ayudarnos a mejorar el rendimiento de la app. Informes de errores Deshacer @@ -1959,24 +2036,27 @@ Language: es Versión local descartada Actualizando contenido Descartar web + Resolver conflicto de sincronización + Este contenido tiene dos versiones en conflicto. Selecciona qué versión quieres descartar.\n Descartar local Local\nGuardado el %1$s\n\nWeb\nGuardado el %2$s\n - Este contenido tiene dos versiones en conflicto. Selecciona qué versión quieres descartar.\n - Resolver conflicto de sincronización No hay datos en este periodo Eliminar la ubicación de los medios No podemos abrir las estadísticas en este momento. Por favor, inténtalo de nuevo más tarde - Ningún medio coincide con tu búsqueda - ¡Busca para encontrar GIF para añadir a tu biblioteca de medios! - Vistas Autor Autores + LinkedIn + Google+ + Tumblr + Twitter + Facebook + Título + Ruta + Vistas Vistas Buscar término Buscar términos Vistas - Título - Vídeos Vistas País Paises @@ -1987,45 +2067,42 @@ Language: es Referente Referentes Entradas y páginas - Ruta - LinkedIn - Google+ - Tumblr - Twitter - Facebook Ver más Compartir entrada Crear entrada Han pasado %1$s desde que se publicó %2$s. Así es como ha funcionado la entrada hasta ahora: - Han pasado %1$s desde que se publicó %2$s. Pon la bola a rodar y aumenta las vistas de las entradas compartiendo tu entrada: + Ningún medio coincide con tu búsqueda + ¡Busca para encontrar GIF para añadir a tu biblioteca de medios! + Vídeos Etiquetas y categorías + Han pasado %1$s desde que se publicó %2$s. Pon la bola a rodar y aumenta las vistas de las entradas compartiendo tu entrada: Histórico %1$s - %2$s + %1$s | %2$s + Autor + Autores + Correo electrónico + WordPress.com + Título + Título Seguidores + Seguidor + Total %1$s seguidores: %2$s Servicio - %1$s | %2$s Vistas Título Vistas - Título Comentarios - Título - Autor Entradas y páginas - Autores Desde - Seguidor - Total %1$s seguidores: %2$s - Correo electrónico - WordPress.com Gestionar datos Aún no se han añadido impresiones Aún no hay datos Menú de depuración Cambiando contraseña… - Tu contraseña debe tener al menos seis caracteres de longitud. Para hacerla más fuerte, usa letras mayúsculas y minúsculas, números y símbolos como ! \" ? $ % ^ & ). Contraseña cambiada con éxito Cambiar contraseña + Tu contraseña debe tener al menos seis caracteres de longitud. Para hacerla más fuerte, usa letras mayúsculas y minúsculas, números y símbolos como ! \" ? $ % ^ & ). Nombre (sin título) Vista previa HTML @@ -2034,7 +2111,6 @@ Language: es Anterior Siguiente %1$s utilizado - Por favor, introduce un sitio WordPress WordPress.com o autoalojado conectado a Jetpack Cargando revisión Revisión cargada Cargar @@ -2043,23 +2119,24 @@ Language: es Aún no hay histórico Cuando haces cambios a tu entrada podrás ver aquí el histórico Cuando haces cambios a tu página podrás ver aquí el histórico + Por favor, introduce un sitio WordPress WordPress.com o autoalojado conectado a Jetpack Avatar del usuario Tamaño completo Grande Mediano Miniatura Historia - La página seleccionada no está disponible Pendiente de revisión + La página seleccionada no está disponible No tienes ninguna página en la papelera No tienes ninguna página programada - No tienes ninguna página en borrador Todavía no has publicado ninguna página Buscar páginas Ninguna página coincide con tu búsqueda Borrar permanentemente Mover a la papelera Mover a borradores + No tienes ninguna página en borrador Hacer superior Ver En la papelera @@ -2087,11 +2164,11 @@ Language: es Pon tu sitio en marcha. ¿A que se siente uno bien terminando una lista? Ver tu sitio - Previsualiza tu sitio para ver lo que verán tus visitantes. Comparte tu sitio + Conecta a tus cuentas de medios sociales - tu sitio compartirá automáticamente las nuevas entradas. Toca en %1$s Compartir %2$s para continuar Toca en %1$s Conexiones %2$s para añadir tus cuentas de medios sociales - Conecta a tus cuentas de medios sociales - tu sitio compartirá automáticamente las nuevas entradas. + Previsualiza tu sitio para ver lo que verán tus visitantes. Publica una entrada Toca en %1$s Crear entrada %2$s para crear una nueva entrada No, gracias @@ -2099,35 +2176,26 @@ Language: es Ir Cancelar Ahora no - Más No tienes sitios + Más Temas no seguidos Añade aquí temas para descubrir entradas sobre tus temáticas favoritas - Accede a la cuenta de WordPress.com que usaste para conectar Jetpack. - Reintentar - Configurar - Jetpack no se pudo instalar en este momento. - Hubo un problema - Jetpack instalado - Instalando Jetpack en tu sitio. Esto puede llevar unos minutos completarse. - Instalando Jetpack - Las credenciales de tu web no se almacenarán, y solo se utilizan para instalar Jetpack. - Instala Jetpack Jetpack FAQ de Jetpack + Accede a la cuenta de WordPress.com que usaste para conectar Jetpack. Para usar las estadísticas en tu sitio WordPress necesitas instalar el plugin Jetpack. No hay temas que coincidan con tu búsqueda ¿Que te gustaría encontrar? No hay etiquetas que coincidan con tu búsqueda No tienes ninguna etiqueta - Añade aquí las etiquetas que uses frecuentemente para poder seleccionarlas rápidamente al etiquetar tus entradas Crea una etiqueta No hay medios que coincidan con tu búsqueda ¿Salir de WordPress? + Añade aquí las etiquetas que uses frecuentemente para poder seleccionarlas rápidamente al etiquetar tus entradas Tienes cambios en entradas que no se han subido a tu sitio. Salir ahora borrará esos cambios de tu dispositivo. ¿Quieres salir de todos modos? - No hay lectores aún - No hay seguidores por correo electrónico aún No hay seguidores aún + No hay seguidores por correo electrónico aún + No hay lectores aún No hay usuarios aún Las entradas que te gusten aparecerán aquí Nada que te gustó aún @@ -2149,198 +2217,198 @@ Language: es imagen destacada Descartar foto de perfil - Dato transitorio + WordPress + Correo electrónico de contacto Correo electrónico Por favor, introduce tu dirección de correo electrónico + No establecido Para continuar, por favor, introduce tu dirección de correo electrónico y nombre + Dato transitorio Nuevo mensaje de «Ayuda y soporte» - WordPress - No establecido - Correo electrónico de contacto Restauración en progreso Restaurando a %1$s %2$s Actualmente restaurando tu sitio + Botón de acción del registro de actividad Tu sitio ha sido restaurado satisfactoriamente - Tu sitio ha sido restaurado correctamente\nRestaurado a %1$s %2$s Tu sitio está siendo restaurado\nRestaurando a %1$s %2$s - Botón de acción del registro de actividad + Tu sitio ha sido restaurado correctamente\nRestaurado a %1$s %2$s Gestionado automáticamente Guarda esta entrada y vuelve cuando quieras para leerla. Solo estará disponible en este dispositivo — las entradas guardadas no se sincronizan con otros dispositivos. - Guardar entradas para más tarde - No ha sido posible realizar la búsqueda No se han encontrado resultados - Lee la entrada de origen + No ha sido posible realizar la búsqueda + Guardar entradas para más tarde Sitios + Lee la entrada de origen Enlace mágico enviado Verificación del código Credenciales de acceso Enlace mágico enviado Acceso por enlace mágico - Acceso mediante la dirección del sitio Acceso mediante dirección de correo electrónico + Acceso mediante la dirección del sitio Toca %s para guardar una entrada en tu lista. - ¡Aún no hay entradas guardadas! Entrada guardada Ver todas Borrada de las entradas guardadas - Añadir a las entradas guardadas Entradas guardadas Borrado - Cambiar icono del sitio + ¡Aún no hay entradas guardadas! + Añadir a las entradas guardadas Cancelar - Eliminar Cambiar - No tienes permiso para editar el icono del sitio. - No tienes permiso para añadir un icono al sitio. + Cambiar icono del sitio + Activar ¿Cómo te gustaría editar el icono? - ¿Te gustaría añadir un icono de sitio? + Eliminar Icono del sitio este sitio - Activar - ¿Activar avisos para %1$s%2$s%3$s? - Activar los avisos del sitio - Desactivar los avisos del sitio - Icono de Jetpack - Evento + No tienes permiso para añadir un icono al sitio. + No tienes permiso para editar el icono del sitio. + ¿Te gustaría añadir un icono de sitio? Icono de actividad Registro de actividad + Recopilar información + Política de cookies + ¿Activar avisos para %1$s%2$s%3$s? + Evento + Icono de Jetpack + Política de privacidad + Ajustes de privacidad Lee la política de privacidad - Usamos otras herramientas de seguimiento, incluidas algunas de terceros. Lee acerca de estas y cómo controlarlas. Política de terceros - Esta información nos ayuda a mejorar nuestros productos, hacer que el marketing sea más relevante, personalizar tu experiencia en WordPress.com y más, tal como se detalla en nuestra política de privacidad. - Política de privacidad + Desactivar los avisos del sitio + Activar los avisos del sitio + Usamos otras herramientas de seguimiento, incluidas algunas de terceros. Lee acerca de estas y cómo controlarlas. Comparte información con nuestra herramienta de análisis acerca del uso que haces de los servicios mientras estás conectado a tu cuenta de WordPress.com. - Política de cookies - Ajustes de privacidad - Recopilar información + Esta información nos ayuda a mejorar nuestros productos, hacer que el marketing sea más relevante, personalizar tu experiencia en WordPress.com y más, tal como se detalla en nuestra política de privacidad. Entrada enviada Una característica del plugin requiere que el sitio esté en buen estado. Una característica del plugin necesita que la suscripción del dominio principal esté asociada con este usuario. - Una característica del plugin necesita privilegios de administrador. El plugin no puede instalarse en sitios VIP. El plugin no se puede instalar debido a las limitaciones de espacio del disco. - Una característica del plugin requiere una dirección de correo electrónico verificada. - Una característica del plugin requiere que el sitio sea público. Una característica del plugin requiere un plan business. Una característica del plugin requiere un dominio personalizado. - Estamos haciendo la configuración final, está casi listo… + Una característica del plugin necesita privilegios de administrador. + Una característica del plugin requiere una dirección de correo electrónico verificada. + Una característica del plugin requiere que el sitio sea público. + Estamos haciendo la configuración final, está casi listo… Instalando plugin… Instalar Instalar el primer plugin en tu sitio puede llevar hasta 1 minuto. Durante este tiempo no podrás realizar cambios en tu sitio. Instalar plugin - Avisos Enviarme nuevos comentarios por correo electrónico - Semanalmente Instantáneamente - Diariamente Entradas nuevas - Recibe avisos de las nuevas entradas de este sitio Enviarme nuevas entradas por correo electrónico + Semanalmente + Diariamente + Avisos + Recibe avisos de las nuevas entradas de este sitio Todos mis sitios seguidos Sitios seguidos - Dispositivo de lectura personal con avisos Gente mirando gráficos y tablas - %1$s en %2$s ¿Seguro que quieres eliminar definitivamente esta publicación? + Dispositivo de lectura personal con avisos + %1$s en %2$s Importante General Utilizar esta foto %1$d de %2$d Fotografías facilitadas por %s Busca para encontrar fotografías gratuitas para añadir a tu biblioteca de medios - Búsqueda en la biblioteca de fotos gratuitas Selecciona de la biblioteca gratuita de fotos No se puede guardar un borrador vacío - %1$s de ilimitado Vista previa %d Añadir %d + %1$s de ilimitado + Búsqueda en la biblioteca de fotos gratuitas Crear etiqueta + Imagen de perfil de %s + audio navegar hacia arriba - Avisos - Abrir enlace externo - mostrar más + abrir cámara foto + elige desde el dispositivo + elige desde medios de WordPress + reproduce + papelera + mostrar más borrar - Reproducir vídeo - reproducir vídeo destacado logo del plugin banner del plugin - elige desde medios de WordPress - abrir cámara - elige desde el dispositivo información del perfil - reproduce previsualizar imagen - vista previa - audio - reproducir vídeo - papelera reintentar vista previa de medios, nombre de archivo %s eliminar %s - Imagen de perfil de %s - marca de verificación Registrarse con Google… + vista previa + marca de verificación + Abrir enlace externo + Avisos + Reproducir vídeo + reproducir vídeo destacado + reproducir vídeo Se ha producido un error en la conexión a Jetpack: %s Ya estás conectado a Jetpack + %s TB Modo visual Modo HTML Vista previa Guardar como borrador - %s TB + %1$s de %2$s + %s B %s GB - %s MB %s kB - %s B - %1$s de %2$s - Si necesitas más espacio, considera actualizar tu plan de WordPress. - Espacio utilizado - Medios + %s MB + Comentario aprobado + Comentario borrado Comentario marcado como no spam Comentario marcado como spam - Comentario borrado Comentario restaurado Comentario enviado a la papelera - El comentario no ha gustado - El comentario ha gustado Comentario sin aprobar - Comentario aprobado + Cuenta nueva Detalle de notificación %s - Editar foto Elige el sitio - Cuenta nueva - Conectado como + Espacio utilizado + Si necesitas más espacio, considera actualizar tu plan de WordPress. + Medios + El comentario no ha gustado + El comentario ha gustado + Editar foto + Ajustes de avisos Detalle de la persona + Conectado como Detalles del archivo Botones de compartir - Avisos Lector Yo + Avisos Mi sitio - Ajustes de avisos Tu avatar se ha subido y estará disponible en breve. - Parece que has desactivado los permisos necesarios para esta característica.<br/><br/>Para cambiarlo, edita tus permisos y asegúrate de que <strong>%s</strong> está activado. Permisos Destacados + Versión %s + Parece que has desactivado los permisos necesarios para esta característica.<br/><br/>Para cambiarlo, edita tus permisos y asegúrate de que <strong>%s</strong> está activado. No puedes acceder a tus ajustes para compartir porque tu módulo de compartir de Jetpack está desactivado. Módulo para compartir desactivado. - Versión %s El sonido escogido tiene una ruta incorrecta. Por favor, elige uno distinto. QP %s quedan %1$d páginas / entradas Queda 1 página quedan %1$d páginas quedan %1$d entradas - %1$d páginas / entradas y 1 archivo restantes %1$d entradas y 1 archivo restantes + %1$d páginas / entradas y 1 archivo restantes %1$d páginas y 1 archivo restantes 1 entrada y 1 archivo restantes 1 página y 1 archivo restantes - %1$d páginas / entradas y %2$d de %3$d archivos restantes %1$d entradas y %2$d de %3$d archivos restantes + %1$d páginas / entradas y %2$d de %3$d archivos restantes quedan %1$d páginas y %2$d de %3$d archivos - queda 1 entrada y %1$d de %2$d archivos queda 1 página y %1$d de %2$d archivos + queda 1 entrada y %1$d de %2$d archivos %1$d entradas / páginas sin subir %1$d páginas sin subir 1 página sin subir @@ -2365,26 +2433,26 @@ Language: es Cambiar nombre de usuario Teclea para obtener más sugerencias Tu nombre de usuario actual es %1$s%2$s%3$s. Con pocas excepciones, otros solo verán tu nombre a mostrar, %4$s%5$s%6$s. - No se ha sugerido ningún nombre de usuario desde %1$s%2$s%3$s. Por favor, introduce más letras o números para obtener sugerencias. - Ocurrió un error al recuperar sugerencias de nombres de usuario. - ¿Descartas cambiar de nombre de usuario? Descartar Guardar Añadir avatar + No se ha sugerido ningún nombre de usuario desde %1$s%2$s%3$s. Por favor, introduce más letras o números para obtener sugerencias. + Ocurrió un error al recuperar sugerencias de nombres de usuario. + ¿Descartas cambiar de nombre de usuario? El correo electrónico ya existe en WordPress.com.\nAcceder. Actualizando cuenta… Enviando correo - Reintentar - Cerrar - Hubo algún problema al enviar el correo. Puedes reintentarlo ahora o cerrar e intentarlo más tarde. Nombre de usuario - Siempre puedes acceder con un enlace como el que acabas de usar, pero también puedes configurar una contraseña si lo prefieres. - Contraseña (opcional) Nombre a mostrar Reintentar Revertir + Reintentar + Cerrar + Contraseña (opcional) Hubo algún problema al actualizar tu cuenta. Puedes reintentarlo o revertir tus cambios para continuar. Hubo algún problema al subir tu avatar. + Hubo algún problema al enviar el correo. Puedes reintentarlo ahora o cerrar e intentarlo más tarde. + Siempre puedes acceder con un enlace como el que acabas de usar, pero también puedes configurar una contraseña si lo prefieres. Necesita actualizarse Buscar plugins Nuevo @@ -2427,11 +2495,11 @@ Language: es por %s Cambiar foto No es posible cargar plugins - Páginas Gestiona las etiquetas de tu sitio Guardando Borrando ¿Borrar permanentemente la etiqueta \'%s\'? + Páginas Ya existe una etiqueta con este nombre Añadir nueva etiqueta Descripción @@ -2518,8 +2586,8 @@ Language: es «Desconectar de WordPress.com» Puedes marcar una dirección IP (o series de direcciones) como «Siempre permitida», evitando que sea bloqueada por Jetpack. Se aceptan IPv4 y IPv6. Para especificar un rango, introduce un valor inferior y un valor superior separados por un guión. Ejemplo: 12.12.12.1–12.12.12.100 Requiere la identificación en dos pasos - Relacionar cuentas usando el correo electrónico Permitir el acceso con WordPress.com + Relacionar cuentas usando el correo electrónico Acceso con WordPress.com Bloquea intentos de acceso maliciosos Protección contra ataques de fuerza bruta @@ -2537,21 +2605,21 @@ Language: es Eliminando ¿Borrar este vídeo? ¿Eliminar esta imagen? - Detalles del documento Detalles del audio + Detalles del documento Detalles del vídeo - Detalles de la imagen Vista previa - Fecha de actualización + Detalles de la imagen Duración + Fecha de actualización Dimensiones de vídeo Sin imagen - Tipo de archivo Nombre del archivo + Tipo de archivo URL Texto alternativo - Conectar un sitio Luz parpadeante + Conectar un sitio Vibración del dispositivo Elige sonido Vistas y sonidos @@ -2566,8 +2634,8 @@ Language: es Activar los avisos Desactivar los avisos Desactivado - Activado Tamaño máximo de vídeo + Activado Tamaño máximo de imagen Hubo un error al subir los medios a esta entrada: %s. Hubo un error al subir esta entrada: %s. @@ -2590,8 +2658,8 @@ Language: es Entrada programada Reintentar Entrada a la espera - Subiendo «%s» Se ha perdido la conexión al servidor + Subiendo «%s» Mis sitios Mi sitio No se pudo detectar una aplicación cliente de correo electrónico @@ -2607,22 +2675,22 @@ Language: es Introduce la dirección del sitio WordPress con el que te gustaría conectar. Ya estás conectado a WordPress.com Seguir - Conectar otro sitio Introduce tu contraseña de WordPress.com + Conectar otro sitio Solicitando el correo electrónico de acceso Parece que esta contraseña es incorrecta. Por favor, vuelve a comprobar tu información e inténtalo de nuevo. Solicitando un código de verificación por SMS. Envíame un mensaje con un código en su lugar ¡Casi lo tenemos! Por favor, introduce un código de verificación desde tu aplicación Authenticator. - Abrir correo electrónico Siguiente Accede a WordPress.com usando una dirección de correo electrónico para gestionar todos tus sitios WordPress. + Abrir correo electrónico La optimización de imagen reduce las imágenes para que suban más rápido.\n\nPuedes cambiar esto en cualquier momento en los ajustes del sitio. ¿Activar optimización de la imagen? - Detener - Activar Foto de perfil Respuesta inesperada del servidor + Detener + Activar No se puede detener la subida porque ya ha finalizado Título Rehacer @@ -2634,24 +2702,24 @@ Language: es Ocurrió un error al arrastrar texto No está permitido arrastrar imágenes en el modo HTML Comparte tu historia aquí… - Privada Borrador Pendiente de revisión + Privada Publicar Ahora Solo los que tengan esta contraseña pueden ver esta entrada Los extractos son resúmenes opcionales del contenido hechos a mano. El slug es la versión amigable de la URL del título de la entrada. - Formato de entrada Etiquetas Slug Extracto - No definido + Formato de entrada Más opciones Categorías y etiquetas + No definido Todos - Nivel superior Categoría superior (opcional): + Nivel superior No tienes ningún audio No tienes ningún documento No tienes ningún vídeo @@ -2666,7 +2734,7 @@ Language: es Documentos Imágenes Todos - %1$s denegó el acceso a tus fotos. Para solucionar esto modifica tus permisos y activa %2$s. + %1$s denegó el acceso a tus archivos de medios. Para solucionar esto modifica tus permisos y activa %2$s. Ver comentarios Calidad de los vídeos. Valores más altos implican vídeos de mejor calidad. Redimensiona los vídeos en las entradas a este tamaño @@ -2764,11 +2832,11 @@ Language: es Media Baja Subido - Fallo al subir Se ha eliminado Eliminando Subiendo En cola + Fallo al subir Calidad de la imagen Todos los archivos de medios se han cancelado debido a un error desconocido. Por favor, vuelve a intentar cargarlos Formato de entrada desconocido @@ -2784,8 +2852,8 @@ Language: es Optimiza las imágenes Error de los archivos multimedia Hubo un error al cargar el archivo multimedia - Reintentar Confirmar + Reintentar No se pudo conectar. Se ha recibido un error 403 cuando se intenta acceder a \n tu endpoint del XMLRPC de sitio. La aplicación lo necesita para comunicarse con tu sitio. Ponte en contacto con tu proveedor para solucionar este problema. No se pudo conectar. Tu alojamiento está bloqueando las peticiones POST, y la aplicación las necesita \n para comunicarse con tu sitio. Ponte en contacto con tu alojamiento para solucionar este problema. Buscar en los sitios seguidos @@ -2815,8 +2883,8 @@ Language: es Procesando… ¡Acción realizada! Comentario marcado como me gusta - Cerrar sesión Iniciar sesión en WordPress.com + Cerrar sesión Más en WordPress.com Más de %s Abrir ajustes del dispositivo @@ -2828,47 +2896,47 @@ Language: es ¡Comentario aprobado! Me gusta ahora - Espectador Seguidor + Espectador Sin conexión, no se pudo guardar tu perfil - Derecha - Izquierda Ninguna + Izquierda + Derecha Seleccionado %1$d No se pudieron recuperar los usuarios del sitio - Correo electrónico del seguidor Seguidor + Correo electrónico del seguidor Recuperando usuarios… - Espectadores Suscriptores por correo electrónico + Espectadores Seguidores Equipo Invita como máximo a 10 personas con sus correos electrónicos o nombre de usuarios de WordPress.com. A aquellos que necesiten un nombre de usuario se le enviará instrucciones sobre cómo hacerlo. Si eliminas a este espectador, no podrá visitar tu sitio\n\n¿Todavía quieres eliminar a este espectador? Si lo eliminas, este seguidor dejará de recibir informaciones de tu sitio, a no ser que vuelta a seguirte. \n\n¿Todavía quieres eliminar a este seguidor? Desde %1$s - No se pudo quitar el espectador No se pudo quitar al seguidor + No se pudo quitar el espectador No se pudieron recuperar los correos electrónicos de los seguidores del sitio No se pudieron recuperar los seguidores del sitio Algunas subidas de medios han fallado. Puedes cambiar al modo HTML \ncuando esto pasa. ¿Borramos todas las subidas fallidas y seguimos? - Miniatura de la imagen Editor visual + Miniatura de la imagen + Cambios guardados Ancho - Enlazado a - Texto alternativo Leyenda - Cambios guardados + Texto alternativo + Enlazado a ¿Descartar cambios sin guardar? ¿Parar la subida? Hubo un error al insertar el archivo multimedia Ahora mismo estás subiendo medios. Por favor, espera hasta que termine. No se pueden insertar medios directamente en el modo HTML. Por favor, cambia al modo visual. Subiendo galería… - ¡Toca para probar de nuevo! Invitación enviada con éxito - %1$s: %2$s ¡Se han enviado las invitaciones pero ha habido errores! + ¡Toca para probar de nuevo! + %1$s: %2$s ¡Hubo un error al tratar de enviar la invitación! No se pudo enviar: Hay nombres de usuario o correos electrónicos no válidos. No se pudo enviar: Un nombre de usuario o correo electrónico no es válido @@ -2876,8 +2944,8 @@ Language: es Mensaje personalizado Invitar Nombres de usuarios o correos electrónicos - Invitar a gente Externo + Invitar a gente Borrar historial de búsqueda ¿Borrar historial de búsqueda? No se han encontrado resultados con %s para tu idioma @@ -2885,33 +2953,33 @@ Language: es Entrada relacionada Los enlaces están desactivados en la pantalla de vista previa Enviar - %1$s eliminado correctamente Si eliminas %1$s, ese usuario ya no será capaz de acceder a este sitio, pero cualquier contenido que fuera creado por %1$s permanecerá en el sitio.\n\n¿Aún te gustaría eliminar este usuario? + %1$s eliminado correctamente Eliminar %1$s - Perfil - Gente Los sitios de esta lista no han publicado nada últimamente. + Gente + Perfil No se ha podido eliminar el usuario No se ha podido actualizar el rol del usuario No se pudieron recuperar los espectadores del sitio Error al subir tu Gravatar - Error al recargar tu Gravatar Error al localizar la imagen recortada + Error al recargar tu Gravatar Error al recortar la imagen Comprobando correo electrónico Actualmente no disponible. Por favor, introduce tu contraseña Accediendo Cuando comentes se hará público Captura o elige imagen - Planes Plan + Planes Tus entradas, páginas y ajustes te serán enviadas por correo electrónico a %s. Exportar tu contenido - ¡Correo electrónico de exportación enviado! Exportando contenido… - Comprobando compras + ¡Correo electrónico de exportación enviado! Mostrar compras Tienes mejoras premium en tu sitio. Por favor, cancela tus mejoras antes de eliminar tu sitio. + Comprobando compras Mejoras premium Algo fue mal. No se pudo realizar la compra. Borrando sitio… @@ -2927,8 +2995,8 @@ Language: es Si quieres un sitio, pero no quieres ninguna de las entradas y las páginas que tiene ahora, nuestro equipo de soporte puede borrar sus mensajes, páginas, archivos multimedia y tus comentarios.\n\nEsto mantendrá su sitio y la URL activos, pero tendrás un nuevo comienzo en la creación de contenidos. Sólo tienes que contactar con nosotros para limpiar tu contenido actual.. Déjanos ayudarte Comienza tu sitio encima - Comenzar de nuevo Ajustes de la app + Comenzar de nuevo Eliminar subidas fallidas Avanzado No hay comentarios en la papelera @@ -2936,34 +3004,34 @@ Language: es No hay comentarios aprobados Saltar No se ha podido conectar. Los métodos XML-RPC necesarios faltan en el servidor. + Estado Centrado Vídeo - Estado - Estándar - Cita - Enlace - Imagen - Galería Chat + Galería + Imagen + Enlace + Cita + Estándar + Información sobre cursos y eventos de WordPress.com (online y presenciales). Audio Minientrada - Información sobre cursos y eventos de WordPress.com (online y presenciales). Oportunidades para participar en investigaciones y encuestas en WordPress. Consejos para sacar el máximo partido a WordPress.com Comunidad - Investigación - Sugerencias Respuestas a mis comentarios - Menciones del nombre de usuario + Sugerencias + Investigación Logros del sitio + Menciones del nombre de usuario Seguidores del sitio «Me gusta» en mis entradas «Me gusta» en mis comentarios Comentarios en mi sitio %d elementos 1 elemento - Todos los usuarios Comentarios de usuarios conocidos + Todos los usuarios Sin comentarios %d comentarios por página 1 comentario por página @@ -2973,11 +3041,11 @@ Language: es Aprobar automáticamente los comentarios de todo el mundo. Aprobar automáticamente si el usuario tiene un comentario previamente aprobado Se requiere aprobación manual de los comentarios de todos. - %d días 1 día - Dirección web + %d días Sitio principal Haz click en el enlace de verificación del correo electrónica enviado a %1$s para confirmar tu nueva dirección + Dirección web Actualmente estás subiendo archivos multimedia. Por favor, espera hasta que se complete. No se pudieron actualizar los comentarios ahora mismo - se muestran comentarios antiguos Establece la imagen destacada @@ -2986,13 +3054,13 @@ Language: es ¿Eliminar de forma permanente estos comentarios? ¿Eliminar de forma permanente este comentario? Eliminar - Restaurar Comentario eliminado + Restaurar No hay comentarios spam - Todos No se pudo cargar la página - Off + Todos Idioma del interface + Off Sobre la app No se pudieron guardar los ajustes de la cuenta No se pudieron recuperar los ajustes de tu cuenta @@ -3000,9 +3068,9 @@ Language: es No se pudo reconocer el código del idioma Permite los comentarios anidados. Anidar hasta + Eliminar Desactivado Buscar - Eliminar Tamaño original Tu sitio es visible únicamente por ti y por lo usuarios que apruebes Tu sitio es visible para todos pero pide a los motores de búsqueda no ser indexado @@ -3011,9 +3079,9 @@ Language: es Sobre mí El nombre público mostrará por defecto el nombre de usuario si no está establecido Nombre que se mostrará públicamente - Apellidos - Nombre Mi perfil + Nombre + Apellidos Imagen de vista previa de entradas relacionadas No pudo guardar la información del sitio No pudo recuperar la información del sitio @@ -3084,22 +3152,22 @@ Language: es Formato por defecto Categoría por defecto Dirección - Descripción corta Título del sitio + Descripción corta Por defecto para nuevas entradas - Escribiendo Cuenta + Escribiendo General Nuevos primero - Antiguos primero - Cerrar después de Comentarios - Entradas relacionadas Privacidad + Antiguos primero Discusión + Entradas relacionadas + Cerrar después de No tienes permisos para subir multimedia al sitio - Desconocido Nunca + Desconocido Esta entrada ya no existe No estás autorizado a ver esta entrada No se pudo recuperar esta entrada @@ -3111,22 +3179,22 @@ Language: es Se ha producido un error. No se ha podido activar el tema de %1$s Gracias por elegir %1$s - GESTIONAR SITIO - HECHO - Ayuda - Detalles - Vista Probar y personalizar + Vista + Detalles + Ayuda + HECHO + GESTIONAR SITIO Activar - Activo - Ayuda - Detalles - Personalizar Tema actual - Página actualizada - Entrada actualizada - Página publicada + Personalizar + Detalles + Ayuda + Activo Entrada publicada + Página publicada + Entrada actualizada + Página actualizada Lo sentimos, no se han encontrados temas. Cargar más entradas Ningún sitio coincide con «%s» @@ -3158,280 +3226,280 @@ Language: es Tipos de avisos No se han podido cargar los ajustes de avisos Me gusta al comentario - Avisos de la aplicación Correo electrónico + Avisos de la aplicación Pestaña de avisos Siempre mandamos correos electrónicos importantes relativos a tu cuenta, pero también obtendrás extras útiles. Sumario de la última entrada Sin conexión Entrada enviada a la papelera - Papelera Estadísticas + Papelera Vista previa Ver - Publicar Editar + Publicar No tienes autorización para acceder a este sitio No se pudo encontrar este sitio Deshacer La solicitud ha expirado. Accede a WordPress.com para volver a intentarlo. - Ignorar El mejor día + Ignorar Estadísticas de hoy Entradas, vistas y visitantes de todos los tiempos Detalles Salir de WordPress.com - Acceder a WordPress.com Iniciar/Cerrar sesión - Ajustes de la cuenta + Acceder a WordPress.com «%s» no se ha ocultado porque es el sitio actual + Ajustes de la cuenta Crear sitio en WordPress.com Añadir sitio autoalojado - Añadir sitio nuevo Mostrar/Ocultar sitios - Elegir sitio - Ver sitio - Ver Administrador + Añadir sitio nuevo Cambiar sitio - Ajustes del sitio - Entradas + Ver Administrador + Ver sitio + Elegir sitio Publicar Aspecto + Ajustes del sitio + Entradas Configuración Toca para mostrarlos - Anular todas las selecciones - Seleccionar todo - Ocultar Mostrar - Accede de nuevo para continuar. + Ocultar + Seleccionar todo + Anular todas las selecciones + Idioma Código de verificación no válido Código de verificación - Idioma - No se pudieron recuperar las entradas + Accede de nuevo para continuar. No se pudo abrir la notificación Términos de búsqueda desconocidos - Términos de búsqueda Autores + Términos de búsqueda Recuperando páginas… Recuperando entradas… Recuperando medios… - Los registros de la aplicación han sido copiados al portapapeles - Este sitio está vacío + No se pudieron recuperar las entradas Nuevas entradas - Ha ocurrido un error al copiar el texto en el portapapeles Subiendo entrada - %1$d años - Un año + Este sitio está vacío + Ha ocurrido un error al copiar el texto en el portapapeles + Los registros de la aplicación han sido copiados al portapapeles + Obteniendo temas… %1$d meses + Un año + %1$d años Un mes - %1$d días - Un día - %1$d horas - hace una hora %1$d minutos + hace una hora + %1$d horas + Un día + %1$d días hace un minuto hace unos segundos - Seguidores - Vídeos Entradas y páginas + Vídeos + Seguidores Países Me gusta - Visitantes - Vistas Años - Obteniendo temas… + Vistas + Visitantes Detalles %d seleccionados - Explora nuestras Preguntas frecuentes. Aún no hay comentarios - No hay entradas con esta temática - Me gusta Ver artículo original + Me gusta Los comentarios están cerrados %1$d de %2$d No se puede publicar una entrada vacía. No tienes permiso para ver o editar entradas. No tienes permiso para ver o editar páginas. - Más Hace más de 1 mes - Hace más de 1 semana + Más Hace más de 2 días - Ayuda y soporte + Hace más de 1 semana Me gustó Comentario El comentario se ha enviado a la papelera. Responder a %s + Explora nuestras Preguntas frecuentes. Aún no se han publicado entradas. ¿Por qué no crear una? Cerrando sesión… + No hay entradas con esta temática + Ayuda y soporte No es posible realizar esta acción No es posible bloquear este sitio Las entradas de este sitio no volverán a mostrarse Bloquear este sitio - Programar Actualizar - Sin sitios recomendados - No se puede dejar de seguir este sitio - No se puede seguir este sitio + Programar + Sitios que sigues Ya estás siguiendo este sitio - No se puede mostrar este sitio Sitio seguido + No se puede mostrar este sitio + No se puede seguir este sitio + No se puede dejar de seguir este sitio + Sin sitios recomendados + Sitio del lector Introduce una URL o tema para seguir - Sitios que sigues Temas seguidos - Sitio del lector - SI normalmente se conecta sin problemas a este sitio sin problemas, este error puede significar que alguien están intentando suplantar el sitio, por lo que no deberías continuar. ¿Quieres, de todas formas, confiar en el certificado? - Certificado SSL no válido Ayuda - El nombre de usuario o contraseña que has introducido son incorrectos - Introduce una dirección de correo electrónico válida - Tu dirección de correo electrónico no es válida - Error al descargar la imagen - No se pudo cargar el comentario + Certificado SSL no válido + SI normalmente se conecta sin problemas a este sitio sin problemas, este error puede significar que alguien están intentando suplantar el sitio, por lo que no deberías continuar. ¿Quieres, de todas formas, confiar en el certificado? + Ocurrió un error al obtener los temas. + Las entradas no pueden ser actualizadas en este momento + Las páginas no pueden ser actualizadas en este momento. + No es spam + No se pudo añadir la categoría + Ocurrió un error Ocurrió un error al editar el comentario Ocurrió un error al moderar el comentario - Ocurrió un error + Tu dirección de correo electrónico no es válida + El campo nombre de categoría es necesario + No se pudo cargar el comentario + Error al descargar la imagen + Se necesita una tarjeta SD montada para subir medios + El elemento multimedia no ha podido ser recuperado No se pudieron actualizar los comentarios - Las páginas no pueden ser actualizadas en este momento. - Las entradas no pueden ser actualizadas en este momento Ocurrió un error al eliminar la entrada - Sin avisos - Se necesita una tarjeta SD montada para subir medios - El campo nombre de categoría es necesario Categoría añadida correctamente - No se pudo añadir la categoría - No es spam - Ocurrió un error al obtener los temas. - Ha ocurrido un error al acceder a este blog - El elemento multimedia no ha podido ser recuperado + Introduce una dirección de correo electrónico válida + El nombre de usuario o contraseña que has introducido son incorrectos + Sin avisos No hay ninguna red disponible - No se ha podido eliminar este tema - No se ha podido añadir este tema - Registro de la aplicación - Ha ocurrido un error al crear la base de datos de la aplicación. Por favor, intenta reinstalar la aplicación. - Este blog está oculto y no se puede cargar. Actívalo de nuevo en ajustes y prueba de nuevo. - No se pueden actualizar los medios en este momento + Ha ocurrido un error al acceder a este blog + Error de conexión + Cancelar edición + Aprobado + Pendiente + Spam + En la papelera + Editar comentario + Aprobar + Rechazar + Spam + Enviar a la papelera + ¿Enviar a la papelera? + Papelera + Guardando cambios + Nueva entrada + Añadir nueva categoría + Nombre de la categoría + Cambios locales Blog de WordPress + Ajustes de página Ajustes de imagen - Cambios locales - Nuevo elemento multimedia - Nueva entrada - No hay avisos… aún. - Se necesita autorización - Comprueba que la URL del sitio introducida es válida + Ajustes de entrada + Seleccionar categorías + Texto del enlace (opcional) + Crear un enlace No se pudo crear un archivo temporal para subir el archivo multimedia. Asegúrate que haya suficiente espacio libre en tu dispositivo. - Nombre de la categoría - Añadir nueva categoría + Ocurrió un error al cargar la entrada. Actualiza tus entradas e intenta nuevamente. + Borrador local Ver en el navegador - Eliminar sitio - El comentario no ha cambiado - Comentario obligatorio + Registro de la aplicación + Se necesita autorización + Aprender más ¿Cancelar la edición de este comentario? - Guardando cambios - Papelera - ¿Enviar a la papelera? - Enviar a la papelera - Spam - Rechazar - Aprobar - Editar comentario - En la papelera - Spam - Pendiente - Aprobado - ¿Borrar página? - ¿Borrar entrada? - Ajustes de entrada - No ha sido posible encontrar el archivo a cargar. ¿Se ha borrado o cambiado de ubicación? + Comentario obligatorio + El comentario no ha cambiado Alineación horizontal - Borrador local - Ajustes de página - Crear un enlace - Texto del enlace (opcional) - Algunos medios no se han podido borrar en este momento. Inténtalo de nuevo más tarde. No tienes permiso para ver la librería multimedia - Cuadrícula de miniaturas - Aprender más - Ocurrió un error al cargar la entrada. Actualiza tus entradas e intenta nuevamente. + Este blog está oculto y no se puede cargar. Actívalo de nuevo en ajustes y prueba de nuevo. + Nuevo elemento multimedia + Eliminar sitio + Comprueba que la URL del sitio introducida es válida + No se pueden actualizar los medios en este momento + No ha sido posible encontrar el archivo a cargar. ¿Se ha borrado o cambiado de ubicación? Ocurrió un error al acceder a este plugin - Cancelar edición - Error de conexión - Seleccionar categorías + ¿Borrar entrada? + ¿Borrar página? + No hay avisos… aún. + Algunos medios no se han podido borrar en este momento. Inténtalo de nuevo más tarde. + Ha ocurrido un error al crear la base de datos de la aplicación. Por favor, intenta reinstalar la aplicación. + Cuadrícula de miniaturas + No se ha podido añadir este tema + No se ha podido eliminar este tema Compartir enlace Recuperando entradas… + Comentario no aprobado A ti, y a %,d personas más les gusta esto A %,d personas les gusta esto - No se puede compartir en WordPress si no tienes un blog visible Comentado marcado como spam - Comentario no aprobado + No se puede compartir en WordPress si no tienes un blog visible + Elige una foto + Elige un vídeo No fue posible recuperar esta entrada A ti y a otra persona os gusta esto - Elige un vídeo - Elige una foto - Registro - Imposible abrir %s - Imposible ver la imágen + Contestar al comentario + Añadido %s Imposible compartir - Este no es un tema válido - Ya estás siguiendo este tema No se pudo publicar tu comentario - Te gusta esto - A una persona le gusta esto + Imposible ver la imágen + Imposible abrir %s Eliminado %s - Añadido %s - Contestar al comentario - Siguiendo - Seguir - Compartir - Reblog + Esta lista esta vacía Sin título + Compartir + Seguir + A una persona le gusta esto + Te gusta esto + Siguiendo No hay comentarios aún - Esta lista esta vacía - Meses - Semanas - Días - Ayer - Hoy - Referentes + Reblog + Registro + Este no es un tema válido + Ya estás siguiendo este tema + Temas Etiquetas y categorías - Clics - Estadísticas - Compartir + Hoy + Ayer + Días + Semanas + Meses Activar - No se pudo actualizar - Descripción - Leyenda Título + Descripción + Compartir + Clics + Referentes Pase de diapositivas - Círculos Mosaico + Círculos Cuadrados - Temas - Descartar + Leyenda + No se pudo actualizar + Estadísticas Gestionar - y %d más. - %d nuevos avisos - Seguimientos + Descartar Respuesta publicada + y %d más. Acceder + Seguimientos + %d nuevos avisos Cargando… - Contraseña HTTP Usuario HTTP + Contraseña HTTP Se ha producido un error al cargar los archivos Nombre de usuario o contraseña incorrectos. - Acceder Nombre de usuario Contraseña + Acceder Lector - Incluír imagen en el contenido del mensaje Usar como imagen destacada + Entradas Ancho - Leyenda (opcional) Páginas - Entradas Anónimo + Incluír imagen en el contenido del mensaje No hay red disponible - hecho + Leyenda (opcional) OK + hecho URL Subiendo… Alineación @@ -3444,27 +3512,27 @@ Language: es El nombre del atajo no puede estar vacío. Privado Título - Separa las etiquetas con comas Categorías + Separa las etiquetas con comas Se necesita una tarjeta SD Multimedia Categoría actualizada correctamente. Aprobar Eliminar - Actualizando la categoría que falló Ninguno - Publicar ahora - Responder - en - Vista previa - Error de actualización de categorías + Actualizando la categoría que falló Error - No + Cancelar + Guardar + Añadir + Error de actualización de categorías + Vista previa + Responder + No + en Ajustes de avisos - Añadir - Guardar - Cancelar + Publicar ahora Una vez Dos veces diff --git a/WordPress/src/main/res/values-eu/strings.xml b/WordPress/src/main/res/values-eu/strings.xml index 722963633cba..417138e80827 100644 --- a/WordPress/src/main/res/values-eu/strings.xml +++ b/WordPress/src/main/res/values-eu/strings.xml @@ -126,11 +126,6 @@ Language: eu_ES Argitaratuta Zirriborroak Orrialdea argitaratua izan da - Saiatu berriro - Arazo bat egon da - Jetpack instalatuta - Jetpack instalatzen - Instalatu Jetpack Irudi galeria Gordetako bidalketak Gaitu diff --git a/WordPress/src/main/res/values-fr-rCA/strings.xml b/WordPress/src/main/res/values-fr-rCA/strings.xml index 7d9ad9e7e76d..7b0e5379694e 100644 --- a/WordPress/src/main/res/values-fr-rCA/strings.xml +++ b/WordPress/src/main/res/values-fr-rCA/strings.xml @@ -1,11 +1,93 @@ + Nous vous recommandons de supprimer l\'application WordPress pour éviter les conflits de données.<b></b> + Il semblerait que l\'application WordPress soit toujours installée. + Vous n\'avez plus besoin de l\'application WordPress + Nous vous recommandons de supprimer l\'application WordPress pour éviter les conflits de données.<b></b> + Retirer les blocs + Confidentialité et évaluation + Réglages de la lecture + Couleur de la barre de lecture + Manuel + Dynamique + Décrivez le rôle de cette image. Laissez le champ vide si elle est purement décorative. + Commencez par des mises en page sur mesure et adaptées aux appareils mobiles + Créer une autre page + Ajouter des pages à votre site + Masquer ceci + Affirmez votre présence sur Internet avec une adresse Web facile à trouver, à partager et à suivre. + Maîtrisez votre identité en ligne grâce à un domaine personnalisé + Pour utiliser les rappels de blog, vous devez activer les notifications push. + Activer les notifications push + Continuer avec le sous-domaine + Acheter un domaine + Photos et vidéos et Musique et son + Musique et son + Photos et vidéos + %s a besoin d’une autorisation pour accéder à vos contenus audio + %s a besoin d’une autorisation pour accéder à vos vidéos + %s a besoin d’une autorisation pour accéder à vos photos + %s a besoin d’une autorisation pour accéder à vos photos et vidéos + %s a besoin d’une autorisation pour accéder à vos contenus musicaux et audio, ainsi qu’à vos photos et vidéos + Activer les notifications + Accédez au menu Réglages &rarr; Notifications &rarr; Réglages de l’application, puis activez l’option %1$s pour recevoir une notification immédiate. + Vous devrez ouvrir l’application pour voir les notifications. + Les notifications push sont désactivées + Les notifications push sont désactivées. + Ignorez l’avertissement relatif à l’autorisation des notifications. + Réparer + <b>%1$s</b> utilise %2$s extensions Jetpack individuelles + <b>%1$s</b> utilise l’extension <b>%2$s</b> + Les sites avec des extensions Jetpack individuelles ne sont pas pris en charge pas l’app WordPress. + <b>%1$s</b>utilise des extensions Jetpack individuelles, qui ne sont pas prises en charge par l’app WordPress. + <b>%1$s</b> utilise l’extension <b>%2$s</b>, qui n’est pas prise en charge par l’app WordPress. + Impossible d’accéder à certains de vos sites + Impossible d’accéder à l’un de vos sites + Veuillez utiliser l’app Jetpack, dans laquelle nous vous guiderons pour connecter l’extension Jetpack afin d’utiliser ce site avec l’app. + Basculer vers l’app Jetpack + %1$s utilise une %2$s, qui ne prend pas encore en charge toutes les fonctionnalités de l’application.\n\nInstallez l’%3$s pour utiliser l’application avec ce site. + Ce site + %1$s utilise des %2$s, qui ne prennent pas encore en charge toutes les fonctionnalités de l’application. Installez l’%3$s. + %1$s utilise une %2$s, qui ne prend pas encore en charge toutes les fonctionnalités de l’application. Installez l’%3$s. + Le passage à l’application Jetpack est prévu dans quelques jours. + Passer d’une application à l’autre est gratuit et ne prend qu’une minute. + Les statistiques, le lecteur, les notifications et d’autres fonctionnalités reposant sur Jetpack ont été supprimées de l’application WordPress et sont désormais uniquement disponibles dans l’application Jetpack. + Lire la suite sur Jetpack.com + Passer à l’application Jetpack + Les %s sont désormais disponibles dans l’application Jetpack. + Le %s est désormais disponible dans l’application Jetpack. + Administrateur WP + Gérer + Trafic + Contenu + Configurer + Terminé + Maintenant que Jetpack est installé, il ne nous reste plus qu’à procéder à votre configuration. Ce processus ne prendra qu’une minute. + Donner de la visibilité à un article avec Blaze maintenant + Donner de la visibilité à cette page avec Blaze + Donner de la visibilité à cet article avec Blaze + Suivez vos performances, et activez ou désactivez Blaze à tout moment. + Votre contenu s’affichera sur des millions de sites WordPress et Tumblr. + Promouvez n’importe quel article ou n’importe quelle page en quelques minutes et pour quelques dollars par jour seulement. + Améliorer le trafic sur votre site avec Blaze + Ce nom de domaine est déjà enregistré. + Promo + Recommandé + Meilleure alternative + Aide + Notre FAQ comporte les réponses aux questions fréquentes que vous vous posez peut-être. + Merci d’être passé à l’application Jetpack ! + Journaux + Gratuit + Aide + Blaze + Billets Menu Blocs Masquer ceci Diffusez votre contenu sur des millions de sites. @@ -18,25 +100,24 @@ Language: fr extension Jetpack complète extensions Jetpack individuelles l’extension %1$s - %1$s utilise des %2$s, qui ne prennent pas encore en charge toutes les fonctionnalités de l’application.\n\nInstallez l’%3$s pour utiliser l’application avec ce site. + %1$s utilise des %2$s, qui ne prennent pas encore en charge toutes les fonctionnalités de l’application.\n\nInstallez l’%3$s pour utiliser l’application avec ce site. Installez l’extension Jetpack complète Un seul site est disponible. Vous ne pouvez donc pas changer de site principal. - Ce site utilise une extension individuelle, qui ne prend pas encore en charge toutes les fonctionnalités de l’application. Installez l’extension Jetpack complète. - Contacter l’assistance - Réessayer - Impossible d’installer Jetpack pour le moment. - Un problème est survenu - Icône d\'erreur - Terminé - Vous pouvez désormais utiliser ce site avec l’application. - Jetpack installé - Installation de Jetpack sur votre site… Ce processus peut prendre quelques minutes. - Installation de Jetpack - Continuer - Icône Jetpack + Contacter l’assistance + Réessayer + Impossible d’installer Jetpack pour le moment. + Un problème est survenu + Icône d’erreur + Vous pouvez désormais utiliser ce site avec l’application. + Jetpack installé + Installation de Jetpack sur votre site… Ce processus peut prendre quelques minutes. + Installation de Jetpack + Continuer + Icône Jetpack Promouvoir avec Blaze - Les identifiants de connexion de votre site Web servent uniquement à l’installation de Jetpack et ne seront pas enregistrés. - Libérez tout le potentiel de votre site. Profitez des statistiques, des notifications et de bien plus encore avec Jetpack. + Les identifiants de connexion de votre site Web servent uniquement à l’installation de Jetpack et ne seront pas enregistrés. + Installer Jetpack + Libérez tout le potentiel de votre site. Profitez de statistiques, de notifications et de bien d’autres choses encore avec Jetpack. Votre site comporte l’extension Jetpack L’application mobile Jetpack est conçue pour compléter l’extension Jetpack. Effectuez le changement dès maintenant pour accéder aux statistiques, aux notifications, au lecteur et bien plus encore. Recevez des notifications pour tout nouveau commentaire, mention J\'aime, vue, et plus encore. @@ -54,18 +135,28 @@ Language: fr Forums communautaires Rappels de blog Afficher les invites + Bloguer Please install Google Play Store to get the Jetpack app Plus tard Passer à Jetpack Les statistiques, le lecteur, les notifications et d’autres fonctionnalités reposant sur Jetpack ont été supprimées de l’application WordPress. Les fonctionnalités Jetpack ont été déplacées. %1$s is moving in %2$s + %1$s vont être déplacés dans %2$s + %1$s vont bientôt être déplacées + %1$s va bientôt être déplacée Obtenir l’application Jetpack Consulter les réponses du formulaire inférieur de %1$s par rapport à la semaine précédente 7 jours précédents 7 derniers jours 1 semaine + %d semaines + %1$s en hausse par rapport aux 7 jours précédents + Au cours des 7 derniers jours, vous avez enregistré %1$s de visiteurs en moins par rapport aux 7 jours précédents. + Au cours des 7 derniers jours, vous avez enregistré %1$s de visiteurs en plus par rapport aux 7 jours précédents. + Au cours des 7 derniers jours, vous avez enregistré %1$s de vues en moins par rapport aux 7 jours précédents. + Au cours des 7 derniers jours, vous avez enregistré %1$s de vues en plus par rapport aux 7 jours précédents. Depuis le <b>jour 1</b> Masquer ceci Me rappeler ultérieurement @@ -121,9 +212,6 @@ Language: fr Ouvrir les liens dans Jetpack Besoin d’aide ? J’ai compris - Veuillez <b>supprimer l’application WordPress</b> pour éviter les conflits de données. - Il semblerait que l\'application WordPress soit toujours installée. Nous vous recommandons de supprimer l\'application WordPress pour éviter les conflits de données. - Vous n\'avez plus besoin de l\'application WordPress Nous ne pouvons pas transférer vos données et réglages sans connexion réseau. Assurez-vous que votre connexion réseau fonctionne, puis réessayez. Impossible de se connecter à Internet. @@ -133,13 +221,11 @@ Language: fr Réessayer Terminer Supprimer l’icône de l’application WordPress - Veuillez <b>supprimer l’application WordPress</b> pour éviter les conflits de données. Nous avons transféré tous vos réglages et données. Tout se trouve au même endroit qu’avant. Merci d’être passé à Jetpack ! Nous désactiverons les notifications de l’application WordPress. Vous recevrez les mêmes notifications, mais elles proviendront maintenant de l’application Jetpack. Les notifications proviennent désormais de Jetpack - Supprimer l\'application WordPress Centre d’aide WordPress Assistance Autorisez l’application à désactiver les notifications de WordPress. @@ -719,6 +805,7 @@ Language: fr GIF Un Aucun aperçu disponible + Ajouter un titre Couleur du texte Padding À la Une @@ -726,6 +813,7 @@ Language: fr Embarquer un contenu URL personnalisée Colonne %d + Plus Décrivez brièvement le lien pour aider l’utilisateur du lecteur d’écran Ajouter des blocs Aucun site Jetpack trouvé @@ -1142,7 +1230,6 @@ Language: fr Présentation des publications de story Page vide créée Page créée - %1$s s’est vu refuser l’accès à vos photos. Pour corriger cela, modifiez vos autorisations et activez %2$s et %3$s. L’insertion du média a échoué. L’insertion du média a échoué : %s Choisir dans la bibliothèque des médias WordPress @@ -1622,6 +1709,7 @@ Language: fr AJOUTER UNE IMAGE AJOUTER UN BLOC ICI Ajouter du texte alternatif + Ajouter une description Touchez le bouton Ajouter pour enregistrer les articles pour enregistrer un article dans votre liste. « La liste a chargé %1$d éléments. » Notifications @@ -1824,7 +1912,6 @@ Language: fr Statistiques annuelles du site Impossible de charger les suggestions de domaine Enregistrer un domaine - Maintenant que Jetpack est installé, vous devez procéder à quelques réglages. Cela ne prendra qu\'une minute. Saisissez un mot-clé pour obtenir davantage d\'idées Aucune suggestion n’a été trouvée Totaux des abonnés @@ -2093,17 +2180,8 @@ Language: fr Aucun sujet suivi Ajoutez des sujets ici pour rechercher des articles sur vos rubriques préférées Connectez-vous au compte WordPress.com que vous utilisez pour connecter Jetpack. - Réessayer - Un problème est survenu - Jetpack installé - Installation de Jetpack - Installer Jetpack Jetpack FAQ Jetpack - Configurer - Impossible d\'installer Jetpack pour le moment. - Installation de Jetpack en cours sur votre site. Cette opération peut prendre quelques minutes. - Vos informations d\'identification sur le site Web ne seront pas conservées. Elles sont utilisées uniquement pour installer Jetpack. Pour utiliser les statistiques sur votre site WordPress, vous devez installer l\'extension Jetpack. Aucun thème ne correspond à votre recherche. Que recherchez-vous ? @@ -2655,7 +2733,7 @@ Language: fr Documents Images Tous - %1$s s\'est vu refuser l\'accès à vos photos. Pour résoudre ce problème, modifiez vos autorisations et activez %2$s. + %1$s s’est vu refuser l’accès à vos fichiers multimédia. Pour corriger cela, modifiez vos autorisations et activez %2$s. Afficher les commentaires Qualité des vidéos. Des valeurs élevées signifient une meilleure qualité des vidéos. Redimensionne les vidéos des articles à cette taille @@ -3188,10 +3266,10 @@ Language: fr Articles Configuration Touchez pour les afficher - Tout déselectionner Montrer Tout selectionner Masquer + Tout désélectionner Langue Code de vérification Code de vérification invalide diff --git a/WordPress/src/main/res/values-fr/strings.xml b/WordPress/src/main/res/values-fr/strings.xml index 7d9ad9e7e76d..7b0e5379694e 100644 --- a/WordPress/src/main/res/values-fr/strings.xml +++ b/WordPress/src/main/res/values-fr/strings.xml @@ -1,11 +1,93 @@ + Nous vous recommandons de supprimer l\'application WordPress pour éviter les conflits de données.<b></b> + Il semblerait que l\'application WordPress soit toujours installée. + Vous n\'avez plus besoin de l\'application WordPress + Nous vous recommandons de supprimer l\'application WordPress pour éviter les conflits de données.<b></b> + Retirer les blocs + Confidentialité et évaluation + Réglages de la lecture + Couleur de la barre de lecture + Manuel + Dynamique + Décrivez le rôle de cette image. Laissez le champ vide si elle est purement décorative. + Commencez par des mises en page sur mesure et adaptées aux appareils mobiles + Créer une autre page + Ajouter des pages à votre site + Masquer ceci + Affirmez votre présence sur Internet avec une adresse Web facile à trouver, à partager et à suivre. + Maîtrisez votre identité en ligne grâce à un domaine personnalisé + Pour utiliser les rappels de blog, vous devez activer les notifications push. + Activer les notifications push + Continuer avec le sous-domaine + Acheter un domaine + Photos et vidéos et Musique et son + Musique et son + Photos et vidéos + %s a besoin d’une autorisation pour accéder à vos contenus audio + %s a besoin d’une autorisation pour accéder à vos vidéos + %s a besoin d’une autorisation pour accéder à vos photos + %s a besoin d’une autorisation pour accéder à vos photos et vidéos + %s a besoin d’une autorisation pour accéder à vos contenus musicaux et audio, ainsi qu’à vos photos et vidéos + Activer les notifications + Accédez au menu Réglages &rarr; Notifications &rarr; Réglages de l’application, puis activez l’option %1$s pour recevoir une notification immédiate. + Vous devrez ouvrir l’application pour voir les notifications. + Les notifications push sont désactivées + Les notifications push sont désactivées. + Ignorez l’avertissement relatif à l’autorisation des notifications. + Réparer + <b>%1$s</b> utilise %2$s extensions Jetpack individuelles + <b>%1$s</b> utilise l’extension <b>%2$s</b> + Les sites avec des extensions Jetpack individuelles ne sont pas pris en charge pas l’app WordPress. + <b>%1$s</b>utilise des extensions Jetpack individuelles, qui ne sont pas prises en charge par l’app WordPress. + <b>%1$s</b> utilise l’extension <b>%2$s</b>, qui n’est pas prise en charge par l’app WordPress. + Impossible d’accéder à certains de vos sites + Impossible d’accéder à l’un de vos sites + Veuillez utiliser l’app Jetpack, dans laquelle nous vous guiderons pour connecter l’extension Jetpack afin d’utiliser ce site avec l’app. + Basculer vers l’app Jetpack + %1$s utilise une %2$s, qui ne prend pas encore en charge toutes les fonctionnalités de l’application.\n\nInstallez l’%3$s pour utiliser l’application avec ce site. + Ce site + %1$s utilise des %2$s, qui ne prennent pas encore en charge toutes les fonctionnalités de l’application. Installez l’%3$s. + %1$s utilise une %2$s, qui ne prend pas encore en charge toutes les fonctionnalités de l’application. Installez l’%3$s. + Le passage à l’application Jetpack est prévu dans quelques jours. + Passer d’une application à l’autre est gratuit et ne prend qu’une minute. + Les statistiques, le lecteur, les notifications et d’autres fonctionnalités reposant sur Jetpack ont été supprimées de l’application WordPress et sont désormais uniquement disponibles dans l’application Jetpack. + Lire la suite sur Jetpack.com + Passer à l’application Jetpack + Les %s sont désormais disponibles dans l’application Jetpack. + Le %s est désormais disponible dans l’application Jetpack. + Administrateur WP + Gérer + Trafic + Contenu + Configurer + Terminé + Maintenant que Jetpack est installé, il ne nous reste plus qu’à procéder à votre configuration. Ce processus ne prendra qu’une minute. + Donner de la visibilité à un article avec Blaze maintenant + Donner de la visibilité à cette page avec Blaze + Donner de la visibilité à cet article avec Blaze + Suivez vos performances, et activez ou désactivez Blaze à tout moment. + Votre contenu s’affichera sur des millions de sites WordPress et Tumblr. + Promouvez n’importe quel article ou n’importe quelle page en quelques minutes et pour quelques dollars par jour seulement. + Améliorer le trafic sur votre site avec Blaze + Ce nom de domaine est déjà enregistré. + Promo + Recommandé + Meilleure alternative + Aide + Notre FAQ comporte les réponses aux questions fréquentes que vous vous posez peut-être. + Merci d’être passé à l’application Jetpack ! + Journaux + Gratuit + Aide + Blaze + Billets Menu Blocs Masquer ceci Diffusez votre contenu sur des millions de sites. @@ -18,25 +100,24 @@ Language: fr extension Jetpack complète extensions Jetpack individuelles l’extension %1$s - %1$s utilise des %2$s, qui ne prennent pas encore en charge toutes les fonctionnalités de l’application.\n\nInstallez l’%3$s pour utiliser l’application avec ce site. + %1$s utilise des %2$s, qui ne prennent pas encore en charge toutes les fonctionnalités de l’application.\n\nInstallez l’%3$s pour utiliser l’application avec ce site. Installez l’extension Jetpack complète Un seul site est disponible. Vous ne pouvez donc pas changer de site principal. - Ce site utilise une extension individuelle, qui ne prend pas encore en charge toutes les fonctionnalités de l’application. Installez l’extension Jetpack complète. - Contacter l’assistance - Réessayer - Impossible d’installer Jetpack pour le moment. - Un problème est survenu - Icône d\'erreur - Terminé - Vous pouvez désormais utiliser ce site avec l’application. - Jetpack installé - Installation de Jetpack sur votre site… Ce processus peut prendre quelques minutes. - Installation de Jetpack - Continuer - Icône Jetpack + Contacter l’assistance + Réessayer + Impossible d’installer Jetpack pour le moment. + Un problème est survenu + Icône d’erreur + Vous pouvez désormais utiliser ce site avec l’application. + Jetpack installé + Installation de Jetpack sur votre site… Ce processus peut prendre quelques minutes. + Installation de Jetpack + Continuer + Icône Jetpack Promouvoir avec Blaze - Les identifiants de connexion de votre site Web servent uniquement à l’installation de Jetpack et ne seront pas enregistrés. - Libérez tout le potentiel de votre site. Profitez des statistiques, des notifications et de bien plus encore avec Jetpack. + Les identifiants de connexion de votre site Web servent uniquement à l’installation de Jetpack et ne seront pas enregistrés. + Installer Jetpack + Libérez tout le potentiel de votre site. Profitez de statistiques, de notifications et de bien d’autres choses encore avec Jetpack. Votre site comporte l’extension Jetpack L’application mobile Jetpack est conçue pour compléter l’extension Jetpack. Effectuez le changement dès maintenant pour accéder aux statistiques, aux notifications, au lecteur et bien plus encore. Recevez des notifications pour tout nouveau commentaire, mention J\'aime, vue, et plus encore. @@ -54,18 +135,28 @@ Language: fr Forums communautaires Rappels de blog Afficher les invites + Bloguer Please install Google Play Store to get the Jetpack app Plus tard Passer à Jetpack Les statistiques, le lecteur, les notifications et d’autres fonctionnalités reposant sur Jetpack ont été supprimées de l’application WordPress. Les fonctionnalités Jetpack ont été déplacées. %1$s is moving in %2$s + %1$s vont être déplacés dans %2$s + %1$s vont bientôt être déplacées + %1$s va bientôt être déplacée Obtenir l’application Jetpack Consulter les réponses du formulaire inférieur de %1$s par rapport à la semaine précédente 7 jours précédents 7 derniers jours 1 semaine + %d semaines + %1$s en hausse par rapport aux 7 jours précédents + Au cours des 7 derniers jours, vous avez enregistré %1$s de visiteurs en moins par rapport aux 7 jours précédents. + Au cours des 7 derniers jours, vous avez enregistré %1$s de visiteurs en plus par rapport aux 7 jours précédents. + Au cours des 7 derniers jours, vous avez enregistré %1$s de vues en moins par rapport aux 7 jours précédents. + Au cours des 7 derniers jours, vous avez enregistré %1$s de vues en plus par rapport aux 7 jours précédents. Depuis le <b>jour 1</b> Masquer ceci Me rappeler ultérieurement @@ -121,9 +212,6 @@ Language: fr Ouvrir les liens dans Jetpack Besoin d’aide ? J’ai compris - Veuillez <b>supprimer l’application WordPress</b> pour éviter les conflits de données. - Il semblerait que l\'application WordPress soit toujours installée. Nous vous recommandons de supprimer l\'application WordPress pour éviter les conflits de données. - Vous n\'avez plus besoin de l\'application WordPress Nous ne pouvons pas transférer vos données et réglages sans connexion réseau. Assurez-vous que votre connexion réseau fonctionne, puis réessayez. Impossible de se connecter à Internet. @@ -133,13 +221,11 @@ Language: fr Réessayer Terminer Supprimer l’icône de l’application WordPress - Veuillez <b>supprimer l’application WordPress</b> pour éviter les conflits de données. Nous avons transféré tous vos réglages et données. Tout se trouve au même endroit qu’avant. Merci d’être passé à Jetpack ! Nous désactiverons les notifications de l’application WordPress. Vous recevrez les mêmes notifications, mais elles proviendront maintenant de l’application Jetpack. Les notifications proviennent désormais de Jetpack - Supprimer l\'application WordPress Centre d’aide WordPress Assistance Autorisez l’application à désactiver les notifications de WordPress. @@ -719,6 +805,7 @@ Language: fr GIF Un Aucun aperçu disponible + Ajouter un titre Couleur du texte Padding À la Une @@ -726,6 +813,7 @@ Language: fr Embarquer un contenu URL personnalisée Colonne %d + Plus Décrivez brièvement le lien pour aider l’utilisateur du lecteur d’écran Ajouter des blocs Aucun site Jetpack trouvé @@ -1142,7 +1230,6 @@ Language: fr Présentation des publications de story Page vide créée Page créée - %1$s s’est vu refuser l’accès à vos photos. Pour corriger cela, modifiez vos autorisations et activez %2$s et %3$s. L’insertion du média a échoué. L’insertion du média a échoué : %s Choisir dans la bibliothèque des médias WordPress @@ -1622,6 +1709,7 @@ Language: fr AJOUTER UNE IMAGE AJOUTER UN BLOC ICI Ajouter du texte alternatif + Ajouter une description Touchez le bouton Ajouter pour enregistrer les articles pour enregistrer un article dans votre liste. « La liste a chargé %1$d éléments. » Notifications @@ -1824,7 +1912,6 @@ Language: fr Statistiques annuelles du site Impossible de charger les suggestions de domaine Enregistrer un domaine - Maintenant que Jetpack est installé, vous devez procéder à quelques réglages. Cela ne prendra qu\'une minute. Saisissez un mot-clé pour obtenir davantage d\'idées Aucune suggestion n’a été trouvée Totaux des abonnés @@ -2093,17 +2180,8 @@ Language: fr Aucun sujet suivi Ajoutez des sujets ici pour rechercher des articles sur vos rubriques préférées Connectez-vous au compte WordPress.com que vous utilisez pour connecter Jetpack. - Réessayer - Un problème est survenu - Jetpack installé - Installation de Jetpack - Installer Jetpack Jetpack FAQ Jetpack - Configurer - Impossible d\'installer Jetpack pour le moment. - Installation de Jetpack en cours sur votre site. Cette opération peut prendre quelques minutes. - Vos informations d\'identification sur le site Web ne seront pas conservées. Elles sont utilisées uniquement pour installer Jetpack. Pour utiliser les statistiques sur votre site WordPress, vous devez installer l\'extension Jetpack. Aucun thème ne correspond à votre recherche. Que recherchez-vous ? @@ -2655,7 +2733,7 @@ Language: fr Documents Images Tous - %1$s s\'est vu refuser l\'accès à vos photos. Pour résoudre ce problème, modifiez vos autorisations et activez %2$s. + %1$s s’est vu refuser l’accès à vos fichiers multimédia. Pour corriger cela, modifiez vos autorisations et activez %2$s. Afficher les commentaires Qualité des vidéos. Des valeurs élevées signifient une meilleure qualité des vidéos. Redimensionne les vidéos des articles à cette taille @@ -3188,10 +3266,10 @@ Language: fr Articles Configuration Touchez pour les afficher - Tout déselectionner Montrer Tout selectionner Masquer + Tout désélectionner Langue Code de vérification Code de vérification invalide diff --git a/WordPress/src/main/res/values-gl/strings.xml b/WordPress/src/main/res/values-gl/strings.xml index 0d5de1191ddc..6b83080d32f1 100644 --- a/WordPress/src/main/res/values-gl/strings.xml +++ b/WordPress/src/main/res/values-gl/strings.xml @@ -1,29 +1,117 @@ + Eliminar bloques + Privacidade e valoracións + Axustes de reprodución + Cor da barra de reprodución + Manual + Dinámica + Describe o propósito da imaxe. Déixao baleiro se a imaxe é decorativa. + Comeza con deseños personalizados e preparados para dispositivos móbiles + Crear outra páxina + Engadir páxinas ao teu sitio + Ocultar isto + Faite co teu rincón en Internet cun enderezo web fácil de encontrar, de compartir e de seguir. + Sé o dono da túa identidade en liña cun dominio propio + Para usar recordatorios para publicar, tes que activar os avisos instantáneos. + Activar os avisos instantáneos + Continuar con subdominio + Comprar dominio + Fotos e vídeos, música e audio + Música e audio + Fotos e vídeos + %s necesita permisos para acceder aos teus audios + %s necesita permisos para acceder aos teus vídeos + %s necesita permisos para acceder ás túas fotos + %s necesita permisos para acceder ás túas fotos e vídeos + %s necesita permisos para acceder á túa música, audios, fotos e vídeos + Activar os avisos + Vai a Axustes &rarr; Notificacións &rarr; Axustes da app, e activa %1$s para recibir notificacións inmediatamente. + Terás que abrir a aplicación para ver as notificacións. + As notificacións push están desactivadas + As notificacións push están desactivadas. + Descarta o aviso do permiso de notificacións. + Corrección + <b>%1$s</b> está usando %2$s plugins individuais de Jetpack + <b>%1$s</b> está usando o plugin <b>%2$s</b> + A aplicación de WordPress non é compatible cos plugins individuais de Jetpack. + <b>%1$s</b> está usando plugins individuais de Jetpack que non son compatibles coa aplicación de WordPress. + <b>%1$s</b> está usando o plugin <b>%2$s</b>, que non é compatible coa aplicación de WordPress. + Non se puido acceder a algúns dos teus sitios + Non se puido acceder a un dos teus sitios + Por favor, pásate á aplicación Jetpack, onde te guiaremos para que conectes o plugin Jetpack para usar este sitio coa aplicación. + Cambia á aplicación de Jetpack + %1$s usa %2$s, que aínda non é compatible con todas as funcións da aplicación.\n\nInstala o %3$s para usar a aplicación con este sitio. + Este sitio + %1$s usa %2$s, que aínda non é compatible con todas as funcións da aplicación. Instala o %3$s. + %1$s usa %2$s, que aínda non é compatible con todas as funcións da aplicación. Instala o %3$s. + Pásate á aplicación de Jetpack en poucos días. + O cambio é gratuíto e só che levará un minuto. + Eliminamos algunhas funcións (como Estatísticas, Lector ou Notificacións, entre outras) da aplicación de WordPress e, agora, só están dispoñibles na de Jetpack. + Encontrarás máis información en Jetpack.com + Cambia á aplicación de Jetpack + %s trasladáronse á aplicación de Jetpack. + %s trasladouse á aplicación de Jetpack. + WP Admin + Xestionar + Tráfico + Contido + Configurar + Feito + Agora que Jetpack está instalado, só tenemos que configuralo. Só che levará un minuto. + Promover unha entrada con Blaze agora + Promover esta páxina con Blaze + Promover esta entrada con Blaze + Fai un seguimento do rendemento, inicia e para a actividade promocional de Blaze en calquera momento. + O teu contido aparecerá en millóns de sitios de WordPress e Tumblr. + Promove calquera entrada ou páxina en cuestión de minutos por só uns euros ao día. + Xera máis tráfico cara o teu sitio con Blaze + Blaze + Este dominio xa está rexistrado + Oferta + Recomendado + Mellor alternativa + Axuda + Consulta o noso FAQ para obter respostas a preguntas habituais que poderías ter. + Grazas por cambiar á aplicación de Jetpack! + Rexistros + Entradas + Gratis + Axuda Menú de bloques Ocultar isto + Amosa o teu traballo en millóns de sitios. + Promove o teu contido con Blaze Pechar Contactar con soporte + Instalar o plugin completo Termos e condicións - Contactar con soporte - Reintentar - Jetpack non se puido instalar neste momento. - Houbo un problema - Icona de erro - Feito - Jetpack instalado - Instalando Jetpack no teu sitio. Isto pode levar uns minutos completarse. - Instalando Jetpack - Continuar - As credenciais da túa web non se almacenarán, e só se utilizan para instalar Jetpack. - Instala Jetpack - Icona de Jetpack + Ao configurar Jetpack, aceptas os nosos + plugin completo de Jetpack + plugins individuais de Jetpack + o plugin %1$s + %1$s usa %2$s, que aínda non é compatible con todas as funcións da aplicación.\n\nInstala o %3$s para usar a aplicación con este sitio. + Por favor, instala o plugin completo de Jetpack + Só hai un sitio dispoñible, polo que non podes cambiar o teu sitio principal. + Contactar con soporte + Reintentar + Jetpack non se puido instalar agora. + Produciuse un problema + Icona de erro + Todo listo para usar este sitio coa aplicación. + Jetpack instalado + Instalando Jetpack no teu sitio. Isto pode levar uns minutos completarse. + Instalando Jetpack + Continuar + As credenciais da túa web non se almacenarán, e só se utilizan para instalar Jetpack. + Instalar Jetpack + Icona de Jetpack Promocionar con Blaze Libera todo o potencial do teu sitio. Obtén estatísticas, notificacións e máis con Jetpack. O teu sitio ten o plugin de Jetpack @@ -120,9 +208,6 @@ Language: gl_ES Abrir ligazóns en Jetpack Necesitas axuda? De acordo - Por favor, <b>borra a aplicación de WordPress</b> para evitar conflitos de datos. - Parece que tes instalada a aplicación de WordPress. Recomendámosche que elimines a aplicación de WordPress para evitar conflitos de datos. - Xa non necesitas a aplicación de WordPress Non podemos transferir os teus datos e axustes sen unha conexión de rede. Comproba a túa conexión de rede para asegurarte de que funcione e volve a intentalo. Non se puido conectar a Internet. @@ -132,13 +217,11 @@ Language: gl_ES Volver a intentalo Terminar Icona para quitar a aplicación de WordPress - Por favor, <b>borra a aplicación de WordPress</b> para evitar conflitos de datos. Transferimos todos os teus datos e axustes. Todo está tal e como o deixaches. Grazas por cambiar a Jetpack! Desactivaremos as notificacións da aplicación de WordPress. Recibirás as mesmas notificacións, pero a partir de agora desde a aplicación de Jetpack. Agora as notificacións chegan de Jetpack - Elimina a aplicación de WordPress Centro de axuda de WordPress Soporte Permite que a aplicación desactive as notificacións de WordPress. @@ -717,6 +800,7 @@ Language: gl_ES Reintentar GIF Un + Engade o título Vista previa non dispoñible Cor do texto Recheo @@ -725,6 +809,7 @@ Language: gl_ES URL personalizada Crear uhna incrustación Columna %d + Máis Describe brevemente o enlace para axudar aos usuarios de lectores de pantalla Engadir bloques Non se encontraron sitios de Jetpack @@ -1141,7 +1226,6 @@ Language: gl_ES Presentación das entradas de historias Páxina en branco creada Páxina creada - %1$s denegou o acceso ás túas fotos. Para correxilo, edita os teus permisos e activa %2$s e %3$s. Inserción do medio fallida. Fallou a inserción do medio: %s Elixe desde a biblioteca de medios de WordPress @@ -1621,6 +1705,7 @@ Language: gl_ES ENGADIR UNHA IMAXE OU UN VÍDEO ENGADIR UNHA IMAXE ENGADIR O BLOQUE AQUÍ + Engadir descrición Toca o botón «Engadir ás entradas gardadas» para gardar unha entrada na túa lista. A lista cargouse con %1$d elementos. Notificacións @@ -1826,7 +1911,6 @@ Language: gl_ES Teclea unha palabra clave para máis ideas Non se encontraron suxerencias Rexistrar dominio - Agora que está instalado Jetpack, só necesitamos que o configures. Isto só che levará un minuto. Quitar dos detalles Mover abaixo Mover arriba @@ -2092,15 +2176,6 @@ Language: gl_ES Temas non seguidos Engade aquí temas para descubrir entradas sobre as túas temáticas favoritas Accede á conta de WordPress.com que usaches para conectar con Jetpack. - Reintentar - Configurar - Jetpack non se puido instalar neste momento. - Houbo un problema - Jetpack instalado - Instalando Jetpack no teu sitio. Isto pode levar uns minutos completarse. - Instalando Jetpack - As credenciais da túa web non se almacenarán, e só se utilizan para instalar Jetpack. - Instala Jetpack Jetpack FAQ de Jetpack Para usar as estatísticas no teu sitio WordPress necesitas instalar o plugin Jetpack. @@ -2654,7 +2729,7 @@ Language: gl_ES Documentos Imaxes Todos - %1$s denegou o acceso ás túas fotos. Para solucionar isto modifica os teus permisos e activa %2$s. + %1$s denegou o acceso aos teus arquivos de medios. Para solucionar, isto modifica os teus permisos e activa %2$s. Ver comentarios Calidade dos vídeos. Valores máis altos implican vídeos de mellor calidade. Redimensiona os vídeos nas entradas a este tamaño diff --git a/WordPress/src/main/res/values-he/strings.xml b/WordPress/src/main/res/values-he/strings.xml index 55c021e0235a..eaa0564b2b31 100644 --- a/WordPress/src/main/res/values-he/strings.xml +++ b/WordPress/src/main/res/values-he/strings.xml @@ -1,11 +1,94 @@ + אנחנו ממליצים <b>להסיר את ההתקנה של האפליקציה של WordPress</b> כדי להימנע מהתנגשויות נתונים. + נראה שהאפליקציה של WordPress עדיין מותקנת אצלך. + כבר אין לך צורך באפליקציה של WordPress במכשיר לך + אנחנו ממליצים <b>להסיר את ההתקנה של האפליקציה של WordPress</b> כדי להימנע מהתנגשויות נתונים. + שמחים שהצטרפת לאפליקציה של Jetpack. אפשר להסיר את ההתקנה של האפליקציה של WordPress כעת. + להסיר בלוקים + פרטיות ודירוג + הגדרות ניגון + הצבע של סרגל הניגון + ידני + דינמי + לתאר את מטרת התמונה. אם התמונה נועדה לקישוט בלבד, יש להשאיר את השדה ריק. + להתחיל עם פריסות ידידותיות למכשירים ניידים בהתאמה אישית + ליצור עמוד נוסף + להוסיף עמודים לאתר שלך + להסתיר את המידע + לבנות את הפינה האישית שלך באינטרנט בעזרת כתובת אתר שיהיה קל למצוא, לשתף ולעקוב אחריה. + לשלוט בזהות המקוונת שלך באמצעות דומיין אישי + כדי להשתמש בתזכורות לכתיבת בלוג, יש להפעיל הודעות בדחיפה. + הפעלה של הודעות בדחיפה + להמשיך עם דומיין משנה + לרכוש דומיין + תמונות וסרטוני וידאו ומוזיקה ואודיו + מוזיקה ואודיו + תמונות וסרטוני וידאו + יש להגדיר הרשאות גישה עבור %s לקובצי האודיו שלך + יש להגדיר הרשאות גישה עבור %s לסרטוני הווידאו שלך + יש להגדיר הרשאות גישה עבור %s לתמונות שלך + יש להגדיר הרשאות גישה עבור %s לתמונות ולסרטוני הווידאו שלך + יש להגדיר הרשאות גישה עבור %s למוזיקה, לאודיו, לתמונות ולסרטוני הווידאו שלך + הפעלת הודעות + יש לעבור אל \'הגדרות\' ← \'הודעות\' ← \'הגדרות אפליקציה\' ולהפעיל את ⁦%1$s⁩ כדי להתחיל לקבל הודעות באופן מיידי. + עליך לפתוח את האפליקציה כדי להציג את ההודעות. + ההודעות בדחיפה מושבתות + ההודעות בדחיפה מושבתות. + להתעלם מהאזהרה לגבי ההרשאה להודעות. + תיקון + האתר <b>%1$s</b> משתמש ב-⁦%2$s⁩ תוספים יחידים + האתר <b>%1$s</b> משתמש בתוסף יחיד <b>%2$s</b> + אתרים עם תוספים יחידים של Jetpack לא נתמכים באפליקציה של WordPress. + האתר <b>%1$s</b> משתמש בתוספים יחידים של Jetpack, אשר אינם נתמכים באפליקציה של WordPress. + האתר <b>%1$s</b> משתמש בתוסף <b>%2$s</b>, אשר אינו נתמך באפליקציה של WordPress. + לא ניתן היה לגשת לכמה מהאתרים שלך + לא ניתן היה לגשת לאחד האתרים שלך + עליך לעבור לאפליקציה של Jetpack. אנחנו נדריך אותך איך להתחבר לתוסף המלא של Jetpack כדי להשתמש באתר הזה עם האפליקציה. + החלפה לאפליקציה של Jetpack + האתר ⁦%1$s⁩ משתמש ב-⁦%2$s⁩ שעדיין לא תומך בכל האפשרויות של האפליקציה.\n\nיש להתקין את ⁦%3$s⁩ כדי להשתמש באפליקציה עם האתר הזה. + האתר הזה + האתר ⁦%1$s⁩ משתמש ב-⁦%2$s⁩ שעדיין לא תומכים בכל האפשרויות של האפליקציה. יש להתקין את ⁦%3$s⁩. + האתר ⁦%1$s⁩ משתמש ב-⁦%2$s⁩ שעדיין לא תומך בכל האפשרויות של האפליקציה. יש להתקין את ⁦%3$s⁩. + האפשרות תעבור לאפליקציה של Jetpack בימים הקרובים. + ההחלפה היא חינמית ואורכת דקה בלבד. + נתונים סטטיסטיים, Reader, הודעות ואפשרויות אחרות שמופעלות על ידי Jetpack הוסרו מאפליקציית WordPress וכעת ניתן לגשת אליהן רק באפליקציה של Jetpack. + מידע נוסף באתר Jetpack.com + החלפה לאפליקציה של Jetpack + האפשרויות של %s הועברו לאפליקציה של Jetpack. + האפשרות של %s הועברה לאפליקציה של Jetpack. + ניהול WP + ניהול + תעבורה + תוכן + הגדרה + הושלם + כעת, לאחר ההתקנה של Jetpack, צריך להגדיר את החשבון. התהליך יארך כדקה. + לקדם פוסט עם Blaze עכשיו + להשתמש ב-Blaze בעמוד הזה + להשתמש ב-Blaze בפוסט הזה + לעקוב אחר ביצועים, להתחיל קמפיין ב-Blaze ולהפסיק אותו בכל עת. + התוכן שלך יופיע במיליוני אתרים של WordPress ו-Tumblr. + לקדם כל פוסט או עמוד תוך דקות ספורות בעלות של כמה דולרים ביום. + לקבל תעבודה גדולה יותר לאתר שלך בעזרת Blaze + Blaze + הדומיין הזה כבר רשום. + מבצע + מומלץ + החלופה הטובה ביותר + עזרה + כדאי לעיין בשאלות הנפוצות שלנו כדי למצוא מענה על כמה מהשאלות הרגילות שאולי יהיו לך. + תודה שהחלפת לאפליקציה של Jetpack! + קובצי יומן + כרטיסים + חינם + עזרה תפריט בלוקים להסתיר את המידע להציג את העבודה שלך במיליוני אתרים. @@ -18,26 +101,24 @@ Language: he_IL התוסף המלא של Jetpack תוספים יחידים של Jetpack התוסף ⁦%1$s⁩ - האתר ⁦%1$s⁩ משתמש ב-⁦%2$s⁩ שעדיין לא תומכים בכל האפשרויות של האפליקציה.\n\nיש להתקין את ⁦%3$s⁩ כדי להשתמש באפליקציה עם האתר הזה. + האתר ⁦%1$s⁩ משתמש ב-⁦%2$s⁩ שעדיין לא תומכים בכל האפשרויות של האפליקציה.\n\nיש להתקין את ⁦%3$s⁩ כדי להשתמש באפליקציה עם האתר הזה. יש להתקין את התוסף המלא של Jetpack יש רק אתר אחד זמין ולכן אין אפשרות לשנות את האתר הראשי שלך. - האתר משתמש בתוסף יחיד שעדיין לא תומך בכל האפשרויות של האפליקציה. יש להתקין את התוסף המלא של Jetpack. - לפנות לתמיכה - יש לנסות שנית - אין אפשרות להתקין את Jetpack כעת. - הייתה בעיה - סמל שגיאה - הושלם - הכול מוכן לשימוש באתר הזה עם האפליקציה. - Jetpack מותקן - אנחנו מתקינים את Jetpack באתר שלך. להשלמת השלב הזה יידרשו מספר דקות. - התקנת Jetpack - המשך - פרטי הכניסה שלך לאתר לא יאוחסנו. אנחנו משתמשים בהם כדי להתקין את Jetpack בלבד. - להתקין את Jetpack - סמל Jetpack + לפנות לתמיכה + יש לנסות שנית + אין אפשרות להתקין את Jetpack כעת. + הייתה בעיה + סמל שגיאה + הכול מוכן לשימוש באתר הזה עם האפליקציה. + Jetpack מותקן + אנחנו מתקינים את Jetpack באתר שלך. להשלמת השלב הזה יידרשו מספר דקות. + התקנת Jetpack + המשך + פרטי הכניסה שלך לאתר לא יאוחסנו. אנחנו משתמשים בהם כדי להתקין את Jetpack בלבד. + להתקין את Jetpack + סמל Jetpack לקדם עם Blaze - להתחיל לממש את מלוא הפוטנציאל של האתר. לראות נתונים סטטיסטיים, לקרוא הודעות ועוד בעזרת Jetpack. + להתחיל לממש את מלוא הפוטנציאל של האתר. לקבל נתונים סטטיסטיים, לקרוא הודעות ועוד בעזרת Jetpack. באתר שלך מותקן תוסף של Jetpack האפליקציה לנייד של Jetpack נועדה לעבוד בשילוב עם התוסף של Jetpack. כדאי להחליף עכשיו כדי לגשת לנתונים סטטיסטיים, להודעות, ל-Reader ועוד. לקבל הודעות על תגובות חדשות, לייקים, צפיות ועוד. @@ -80,6 +161,7 @@ Language: he_IL מתוך <b>DayOne</b> להסתיר את המידע הזכירו לי מאוחר יותר + האפשרויות של נתונים סטטיסטיים, Reader, הודעות ואפשרויות אחרות יועברו בקרוב אל האפליקציה של Jetpack לנייד. החלפה לאפליקציה של Jetpack מידע נוסף באתר Jetpack.com ההחלפה היא חינמית ואורכת דקה בלבד. @@ -131,9 +213,6 @@ Language: he_IL לפתוח את הקישורים ב-Jetpack נדרשת לך עזרה? הבנתי - יש <b>למחוק את האפליקציה של WordPress</b> כדי להימנע מהתנגשויות נתונים. - נראה שהאפליקציה של WordPress עדיין מותקנת אצלך. אנחנו ממליצים למחוק את האפליקציה של WordPress כדי להימנע מהתנגשויות נתונים. - כבר אין לך צורך באפליקציה של WordPress אין לנו אפשרות להעביר את הנתונים וההגדרות שלך ללא חיבור לרשת. יש לבדוק שהחיבור לרשת תקין ולנסות שוב. לא ניתן להתחבר לאינטרנט. @@ -143,13 +222,11 @@ Language: he_IL לנסות שוב סיום סמל של \'להסיר את האפליקציה של WordPress\' - יש <b>למחוק את האפליקציה של WordPress</b> כדי להימנע מהתנגשויות נתונים. העברנו את כל הנתונים וההגדרות שלך. השארנו הכול במקום. תודה שעברת אל Jetpack! אנחנו נשבית את קבלת ההודעות מהאפליקציה של WordPress. אותן הודעות ימשיכו להישלח אליך אבל כעת הן יגיעו מהאפליקציה של Jetpack. ההודעות מגיעות עכשיו מ-Jetpack - יש למחוק את האפליקציה של WordPress מרכז העזרה של WordPress תמיכה הפעולה מאפשרת לאפליקציה להשבית את ההודעות של WordPress. @@ -728,6 +805,7 @@ Language: he_IL לנסות שוב GIF אחד + להוסיף כותרת תצוגה מקדימה לא זמינה צבע טקסט מרווחים @@ -736,6 +814,7 @@ Language: he_IL כתובת URL מותאמת ליצור תוכן מוטמע טור %d + עוד יש לתאר בקצרה את הקישור כדי לסייע למשתמשים בקוראי מסכים להוסיף בלוקים לא נמצאו אתרים של Jetpack @@ -1152,7 +1231,6 @@ Language: he_IL אנו שמחים להציג את האפשרות \'פוסטים של סטורי\' נוצר עמוד ריק העמוד נוצר - הגישה של ⁦%1$s⁩ אל תמונות שלך נדחתה. כדי לתקן זאת, יש לערוך את ההרשאות ולהפעיל את ⁦%2$s⁩ ואת ⁦%3$s⁩. הכנסת המדיה נכשלה. הכנסת המדיה נכשלה: %s לבחור מתוך ספריית המדיה של WordPress @@ -1632,6 +1710,7 @@ Language: he_IL להוסיף תמונה או וידאו להוסיף תמונה להוסיף בלוק כאן + להוסיף תיאור יש להקיש על הכפתור \'הוספה\' ו-\'שמירת פוסט\' כדי לשמור את הפוסט ברשימה שלך. \"הרשימה נטענה עם %1$d פריטים.\" הודעות @@ -1837,7 +1916,6 @@ Language: he_IL יש להקליד מילת מפתח לרעיונות נוספים לא נמצאו הצעות רישום דומיין - כעת, לאחר ההתקנה של Jetpack, צריך להגדיר את החשבון. התהליך יארך כדקה. הסרה מהתובנות העברה למטה העברה למעלה @@ -2103,15 +2181,6 @@ Language: he_IL אין נושאים במעקב אפשר להוסיף כאן נושאים כדי למצוא פוסטים על הנושאים המועדפים עליך יש להיכנס לחשבון WordPress.com שדרכו חיברת את Jetpack. - יש לנסות שנית - הגדר - אין אפשרות להתקין את Jetpack כעת. - הייתה בעיה - Jetpack מותקן - אנחנו מתקינים את Jetpack באתר שלך. השלב הזה יארך מספר דקות לפני השלמתו. - מתקין את Jetpack - פרטי הכניסה שלך לאתר לא יאוחסנו. אנחנו משתמשים בהם כדי להתקין את Jetpack בלבד. - התקנת Jetpack Jetpack שאלות נפוצות בנושא Jetpack להשתמש בנתונים סטטיסטיים באתר שלך ב-WordPress, עליך להתקין את התוסף של Jetpack. @@ -2665,7 +2734,7 @@ Language: he_IL מסמכים תמונות הכל - ל-%1$s אין אפשרות גישה לתמונות שלך. כדי לתקן זאת, יש לערוך את ההרשאות ולהפעיל את %2$s. + הגישה של ⁦%1$s⁩ אל קובצי המדיה שלך נדחתה. כדי לתקן זאת, יש לערוך את ההרשאות ולהפעיל את ⁦%2$s⁩. הצגת התגובות איכות קובצי הווידאו. ערכים גבוהים יותר מאפשרים איכות טובה יותר של קובצי הווידאו. שינוי גודל של קובצי וידאו בפוסטים לממדים אלו diff --git a/WordPress/src/main/res/values-id/strings.xml b/WordPress/src/main/res/values-id/strings.xml index a76057be19fb..23ec4a785dbf 100644 --- a/WordPress/src/main/res/values-id/strings.xml +++ b/WordPress/src/main/res/values-id/strings.xml @@ -1,11 +1,89 @@ + Hapus blok + Privasi dan rating + Pengaturan Playback + Warna Bar Playback + Manual + Dinamis + Jelaskan maksud gambar tersebut. Biarkan kosong jika hanya berupa hiasan. + Mulai dengan bespoke, tampilan ramah perangkat seluler + Buat Halaman Lain + Tambahkan Halaman ke situs Anda + Tutup + Ajukan klaim domain Anda dengan alamat situs yang mudah ditemukan, dibagikan, dan diikuti. + Miliki identitas online Anda dengan domain khusus + Untuk menggunakan pengingat blogging, Anda harus mengaktfikan pemberitahuan push. + Aktifkan pemberitahuan push + Lanjutkan dengan subdomain + Beli domain + Foto dan video & Musik dan audio + Musik dan audio + Foto dan video + %s memerlukan izin untuk mengakses audio Anda + %s memerlukan izin untuk mengakses video Anda + %s memerlukan izin untuk mengakses foto Anda + %s memerlukan izin untuk mengakses foto dan video Anda + %s memerlukan izin untuk mengakses musik, audio, foto, dan video Anda + Aktifkan pemberitahuan + Buka Pengaturan &rarr; Pemberitahuan &rarr; Pengaturan Aplikasi, dan nyalakan %1$s agar segera mendapatkan pemberitahuan. + Anda harus membuka aplikasi untuk melihat pemberitahuan. + Pemberitahuan push dinonaktifkan + Pemberitahuan push dinonaktifkan. + Tutup peringatan izin pemberitahuan. + Perbaiki + <b>%1$s</b> menggunakan plugin individual Jetpack %2$s + <b>%1$s</b> menggunakan plugin <b>%2$s</b> + Situs dengan plugin individual Jetpack tidak didukung oleh aplikasi WordPress. + <b>%1$s</b> menggunakan plugin individual Jetpack, yang tidak didukung oleh aplikasi WordPress. + <b>%1$s</b> menggunakan plugin <b>%2$s</b>, yang tidak didukung oleh aplikasi WordPress. + Tidak dapat mengakses beberapa situs Anda + Tidak dapat mengakses salah satu situs Anda + Silakan beralih ke aplikasi Jetpack, di mana kami akan memandu Anda dengan menghubungkan plugin lengkap Jetpack untuk mengoperasikan situs ini dengan aplikasi. + Ganti ke aplikasi Jetpack + %1$s ini menggunakan %2$s yang belum mendukung semua fitur dalam aplikasi.\n\nSilakan instal %3$s untuk menggunakan aplikasi dengan situs ini. + Situs berikut + %1$s ini menggunakan %2$s yang belum mendukung semua fitur dalam aplikasi. Silakan instal %3$s. + %1$s ini menggunakan %2$s yang belum mendukung semua fitur dalam aplikasi. Silakan instal %3$s. + Berpindah ke aplikasi Jetpack dalam beberapa hari. + Beralihlah ke aplikasi—gratis dan prosesnya cepat! + Statistik, Pembaca, Pemberitahuan, dan beragam fitur lain berbasis Jetpack telah dihapus dari aplikasi WordPress dan hanya dapat ditemukan di aplikasi Jetpack. + Baca selengkapnya di Jetpack.com + Ganti ke aplikasi Jetpack + %s telah berpindah ke aplikasi Jetpack. + %s telah berpindah ke aplikasi Jetpack. + Admin WP + Kelola + Lalu Lintas + Konten + Siapkan + Selesai + Sekarang Jetpack telah terinstal, kami hanya perlu Anda menyiapkannya. Ini hanya memerlukan waktu satu menit. + Promosikan pos dengan Blaze sekarang + Promosikan halaman ini dengan Blaze + Promosikan pos ini dengan Blaze + Lacak performa, mulai, dan hentikan Blaze Anda kapan saja. + Konten Anda akan muncul di jutaan situs WordPress dan Tumblr. + Promosikan pos atau halaman apa saja dalam hitungan menit dengan biaya harian sangat terjangkau. + Tingkatkan jumlah pengunjung situs Anda dengan Blaze + Blaze + Domain ini sudah terdaftar + Obral + Disarankan + Alternatif Terbaik + Bantuan + Tanya Jawab Umum akan menjawab berbagai pertanyaan yang umum diajukan. + Terima kasih sudah beralih ke aplikasi Jetpack. + Log + Tiket + Gratis + Bantuan Menu blok Sembunyikan Tunjukkan karya Anda di jutaan situs. @@ -18,24 +96,22 @@ Language: id plugin Jetpack lengkap plugin Jetpack versi individual Plugin %1$s - %1$s ini menggunakan %2$s yang belum mendukung semua fitur aplikasi.\n\nSilakan instal %3$s untuk menggunakan aplikasi dengan situs ini. + %1$s ini menggunakan %2$s yang belum mendukung semua fitur dalam aplikasi.\n\nSilakan instal %3$s untuk menggunakan aplikasi dengan situs ini. Silakan pasang plugin Jetpack versi lengkap Hanya ada satu situs, sehingga Anda tidak bisa memilih situs yang utama. - Situs ini menggunakan plugin versi individual yang belum mendukung semua fitur aplikasi. Silakan pasang plugin Jetpack versi lengkap. - Hubungi Dukungan - Coba lagi - Jetpack tidak dapat diinstal untuk saat ini. - Terjadi kendala - Ikon eror - Selesai - Situs ini siap digunakan dengan aplikasi. - Jetpack sudah diinstal - Menginstal Jetpack di situs Anda. Mungkin perlu waktu beberapa menit untuk menyelesaikan proses ini. - Menginstal Jetpack - Lanjutkan - Kredensial situs web Anda tidak akan disimpan dan hanya akan digunakan untuk tujuan penginstalan Jetpack. - Instal Jetpack - Ikon Jetpack + Hubungi Dukungan + Coba lagi + Jetpack tidak dapat diinstal untuk saat ini. + Ada masalah: + Ikon Error + Situs ini siap digunakan dengan aplikasi. + Jetpack terpasang + Menginstal Jetpack di situs Anda. Mungkin perlu waktu beberapa menit untuk menyelesaikan proses ini. + Menginstal Jetpack + Lanjutkan + Kredensial situs web Anda tidak akan disimpan dan hanya akan digunakan untuk tujuan penginstalan Jetpack. + Pasang Jetpack + Ikon Jetpack Promosikan dengan Blaze Maksimalkan potensi situs Anda. Dapatkan statistik, pemberitahuan, dan lainnya dengan Jetpack. Situs Anda memiliki plugin Jetpack @@ -132,9 +208,6 @@ Language: id Buka tautan di Jetpack Butuh bantuan? Saya mengerti - Harap <b>hapus aplikasi WordPress</b> untuk mencegah terjadinya konflik data. - Aplikasi WordPress sepertinya masih terinstal. Kami sarankan untuk menghapus aplikasi WordPress guna mencegah terjadinya konflik data. - Anda tidak lagi membutuhkan aplikasi WordPress Tanpa koneksi internet, kami tidak dapat mentransfer data dan pengaturan. Pastikan koneksi internet berfungsi lalu coba kembali. Tidak dapat menyambung ke internet. @@ -144,13 +217,11 @@ Language: id Coba lagi Selesai Hapus ikon Aplikasi WordPress - Harap <b>hapus aplikasi WordPress</b> untuk mencegah terjadinya konflik data. Kami telah mentransfer semua data dan pengaturan Anda. Semua percis seperti terakhir Anda tinggalkan. Terima kasih telah beralih ke Jetpack! Kami akan menonaktifkan pemberitahuan dari aplikasi WordPress. Anda akan tetap menerima pemberitahuan tersebut, tetapi dari aplikasi Jetpack. Pemberitahuan kini dikirimkan lewat Jetpack - Harap hapus aplikasi WordPress Pusat bantuan WordPress Dukungan Mengizinkan aplikasi untuk menonaktifkan pemberitahuan WordPress. @@ -729,6 +800,7 @@ Language: id Coba lagi GIF Satu. + Tambahkan judul Pratinjau tidak tersedia Warna teks Padding @@ -737,6 +809,7 @@ Language: id URL Tersuai Buat sematan Kolom %d + Lainnya Menjelaskan secara singkat tautan untuk membantu pengguna pembaca layar Tambahkan blok Situs Jetpack tidak ditemukan @@ -1153,7 +1226,6 @@ Language: id Memperkenalkan Pos Cerita Halaman kosong dibuat Halaman dibuat - %1$s ditolak untuk mengakses foto Anda. Untuk memperbaiki ini, edit perizinan Anda dan aktifkan %2$s dan %3$s. Penyisipan media gagal. Penyisipan media gagal: %s Pilih dari Pustaka Media WordPress @@ -1633,6 +1705,7 @@ Language: id TAMBAHKAN GAMBAR ATAU VIDEO TAMBAHKAN GAMBAR TAMBAHKAN BLOK DI SINI + Tambahkan deskripsi Ketuk tombol Tambahkan untuk Simpan Pos untuk menyimpan pos ke daftar Anda. \"Daftar ini telah diisi dengan %1$d item.\" Pemberitahuan @@ -1838,7 +1911,6 @@ Language: id Ketikkan kata kunci untuk mendapatkan lebih banyak ide Saran tidak ditemukan Daftarkan Domain - Sekarang Jetpack telah terinstal, kami hanya perlu Anda menyiapkannya. Ini hanya memerlukan waktu satu menit. Hapus dari wawasan Geser bawah Geser atas @@ -2104,15 +2176,6 @@ Language: id Tidak ada topi yang diikuti Tambahkan topik di sini untuk menemukan pos tentang topik favorit Anda. Login ke akun WordPress.com yang Anda gunakan untuk menghubungkan Jetpack. - Coba lagi - Siapkan - Jetpack tidak dapat diinstal untuk saat ini. - Ada masalah - Jetpack sudah diinstal - Menginstal Jetpack di situs Anda. Mungkin perlu waktu beberapa menit untuk menyelesaikan proses ini. - Menginstal Jetpack - Kredensial situs web Anda tidak akan disimpan dan hanya akan digunakan untuk tujuan penginstalan Jetpack. - Instal Jetpack Jetpack FAQ Jetpack Untuk menggunakan Statistik di situs WordPress, Anda perlu menginstal plugin Jetpack. @@ -2666,7 +2729,7 @@ Language: id Dokumen Gambar Semua - Akses %1$s ke foto Anda ditolak. Untuk memperbaikinya, sunting izin Anda dan aktifkan %2$s. + %1$s ditolak untuk mengakses berkas media Anda. Untuk memperbaiki ini, edit perizinan Anda dan aktifkan %2$s. Lihat komentar Kualitas video. Semakin tinggi nilainya, semakin bagus kualitas videonya. Ubah ukuran video dalam pos menjadi ukuran ini diff --git a/WordPress/src/main/res/values-it/strings.xml b/WordPress/src/main/res/values-it/strings.xml index 8b38140a3fca..27d6e79e40e1 100644 --- a/WordPress/src/main/res/values-it/strings.xml +++ b/WordPress/src/main/res/values-it/strings.xml @@ -1,11 +1,89 @@ + Rimuovi i blocchi + Prezzo e valutazione + Impostazioni di riproduzione + Colore della barra di riproduzione + Manuale + Dinamica + Descrivi lo scopo dell\'immagine. Lascia vuoto se decorativa. + Inizia con layout su misura e ottimizzati per i dispositivi mobili + Crea un\'altra pagina + Aggiungi pagine al sito + Nascondi + Rivendica il tuo angolo di Web con un indirizzo del sito facile da trovare, condividere e seguire. + Possiedi la tua identità online con un dominio personalizzato + Per utilizzare i promemoria relativi al blog, devi attivare le notifiche push. + Disattiva notifiche push + Continua con il sottodominio + Acquista dominio + Foto e video & Musica e audio + Musica e audio + Foto e video + %s necessita dell\'autorizzazione per accedere ai tuoi audio + %s necessita dell\'autorizzazione per accedere ai tuoi video + %s necessita dell\'autorizzazione per accedere alle tue foto + %s necessita dell\'autorizzazione per accedere alle tue foto e ai tuoi video + %s necessita dell\'autorizzazione per accedere a musica, audio, foto e video + Attiva notifiche + Vai in Impostazioni &rarr; Notifiche &rarr; Impostazioni app e attiva %1$s per ricevere notifiche immediate. + Per visualizzare le notifiche, è necessario aprire l\'app. + Le notifiche push sono disattivate + Le notifiche push sono disattivate. + Ignora l\'avviso di autorizzazione alla notifica. + Correggi + <b>%1$s</b> sta usando %2$s plugin Jetpack individuali + <b>%1$s</b> sta usando il plugin <b>%2$s</b> + I siti con plugin Jetpack individuali non sono supportati dall\'app WordPress. + <b>%1$s</b> sta usando plugin Jetpack individuali non supportati dall\'app WordPress. + <b>%1$s</b> sta usando il plugin <b>%2$s</b> non supportato dall\'app WordPress. + Impossibile accedere ad alcuni dei tuoi siti. + Impossibile accedere a uno dei tuoi siti + Passa all\'app Jetpack dove ti guideremo attraverso il collegamento del plugin Jetpack completo per poter utilizzare questo sito con l\'app. + Passa all\'app Jetpack + %1$s utilizza %2$s, che non supportano ancora tutte le funzionalità dell\'app.\n\nInstalla %3$s per usare l\'app coon questo sito. + Questo sito + %1$s utilizza %2$s, che non supportano ancora tutte le funzionalità dell\'app. Please install the %3$s. + %1$s utilizza %2$s, che non supportano ancora tutte le funzionalità dell\'app. Please install the %3$s. + Moving to the Jetpack app in a few days. + Il passaggio è gratuito e richiede solo un minuto. + Statistiche, Reader, Notifiche e altre funzionalità basate su Jetpack sono state rimosse dall\'app WordPress. + Maggiori informazioni su jetpack.com + Passa all\'app Jetpack + %s have moved to the Jetpack app. + %s has moved to the Jetpack app. + WP Admin + Gestisci + Traffico + Contenuto + Configura + Fatto + Ora che Jetpack è installato, manca solo la configurazione. Ci vorrà soltanto un minuto. + Diffondi un articolo ora + Diffondi questa pagina + Diffondi questo articolo + Tieni traccia delle prestazioni, avvia e interrompi Blaze in qualsiasi momento. + I tuoi contenuti verranno mostrati su milioni di siti WordPress e Tumblr. + Promuovi qualsiasi articolo o pagina in pochi minuti per pochi dollari al giorno. + Attira più traffico sul tuo sito con Blaze + Diffondi + Questo dominio è già registrato + Offerta + Consigliato + Alternativa migliore + Aiuto + Consulta le domande frequenti per ricevere risposte ai dubbi più comuni che potresti avere. + Grazie per essere passato all\'app di Jetpack. + Log + Biglietti + Gratis + Aiuto Menu Blocchi Nascondi Mostra il tuo lavoro su milioni di siti. @@ -18,24 +96,22 @@ Language: it plugin Jetpack completo plugin Jetpack individuali il plugin %1$s - %1$s utilizza %2$s, che non supportano ancora tutte le funzionalità dell\'app.\n\nInstalla %3$s per usare l\'app coon questo sito. + %1$s utilizza %2$s, che non supportano ancora tutte le funzionalità dell\'app.\n\nInstalla %3$s per usare l\'app coon questo sito. Installa il plugin Jetpack completo È disponibile un solo sito, quindi non puoi modificare il sito principale. - Questo sito utilizza un plugin individuale, che non supportano ancora tutte le funzionalità dell\'app. Installa il plugin Jetpack completo. - Contatta il supporto - Riprova - Jetpack non può essere installato in questo momento. - Si è verificato un problema - Icona Errore - Fatto - Pronto per utilizzare questo sito con l\'app. - Jetpack installato - Installa Jetpack sul tuo sito. Il completamento può richiedere alcuni minuti. - Installazione di Jetpack - Continua - Le credenziali del tuo sito web non verranno archiviate e saranno utilizzate per installare Jetpack. - Installa Jetpack - Icona Jetpack + Contatta il supporto + Riprova + Jetpack non può essere installato in questo momento. + Si è verificato un problema + Icona Errore + Pronto per utilizzare questo sito con l\'app. + Jetpack installato + Installa Jetpack sul tuo sito. Il completamento può richiedere alcuni minuti. + Installazione di Jetpack + Continua + Le credenziali del tuo sito web non verranno archiviate e saranno utilizzate per installare Jetpack. + Installa Jetpack + Icona Jetpack Promuovi con la diffusione Sblocca tutto il potenziale del tuo sito web. Ottieni statistiche, notifiche e altro con Jetpack. Il tuo sito ha il plugin Jetpack @@ -80,6 +156,7 @@ Language: it Da <b>DayOne</b> Nascondi Ricordamelo più tardi + Statistiche, Reader, Notifiche e altre funzionalità passeranno presto all\'app mobile Jetpack. Passa all\'app Jetpack Maggiori informazioni su jetpack.com Il passaggio è gratuito e richiede solo un minuto. @@ -131,9 +208,6 @@ Language: it Apri i collegamenti in Jetpack Hai bisogno di aiuto? OK - Elimina <b>l\'app WordPress</b> per evitare conflitti di dati. - Sembra che tu abbia ancora l\'app WordPress installata. Ti consigliamo di eliminare l\'app WordPress per evitare conflitti di dati. - Non hai più bisogno dell\'app WordPress Non siamo in grado di trasferire i dati e le impostazioni senza una connessione di rete. Verifica che la tua connessione di rete funzioni e riprova. Impossibile connettersi a Internet. @@ -143,13 +217,11 @@ Language: it Riprova Finito Rimuovi l\'icona dell\'app WordPress - Elimina <b>l\'app WordPress</b> per evitare conflitti di dati. Abbiamo trasferito tutti i tuoi dati e le impostazioni. Tutto è dove lo hai lasciato. Grazie per aver scelto di passare a Jetpack! Disattiveremo le notifiche dall\'app WordPress. Riceverai tutte le stesse notifiche, ma ora arriveranno dall\'app Jetpack. Le notifiche ora arrivano da Jetpack - Elimina l\'app WordPress Centro d\'assistenza di WordPress Supporto Consenti all\'app di disabilitare le notifiche WordPress. @@ -728,6 +800,7 @@ Language: it Riprova GIF Uno + Aggiungi titolo Nessuna anteprima disponibile Colore del testo Padding @@ -1152,7 +1225,6 @@ Language: it Introduzione agli articoli della storia Pagina bianca creata Pagina creata - %1$s è stato negato l\'accesso alle tue foto. Per risolvere questo problema, modifica le tue autorizzazioni e attiva %2$s e %3$s. Inserimento degli elementi multimediali non riuscito. Inserimento degli elementi multimediali non riuscito: %s Scegli dalla Libreria multimediale di WordPress @@ -1632,6 +1704,7 @@ Language: it AGGIUNGI IMMAGINE O VIDEO AGGIUNGI IMMAGINE AGGIUNGI BLOCCO QUI + Aggiungi descrizione Tocca il pulsante Aggiungi ad articoli salvati per salvare un articolo nell\'elenco. \"L\'elenco è stato caricato con %1$d voci.\" Notifiche @@ -1837,7 +1910,6 @@ Language: it Digita una parola chiave per ulteriori idee Nessun suggerimento trovato Registra dominio - Ora che Jetpack è installato, manca solo la configurazione. Ci vorrà soltanto un minuto. Rimuovi da Panoramica Sposta in basso Sposta in alto @@ -2103,15 +2175,6 @@ Language: it Nessun argomento seguito Aggiungi qui gli argomenti per trovare gli articoli con i tuoi temi preferiti Accedi all\'account WordPress.com che hai utilizzato per connettere Jetpack. - Riprova - Continua - Jetpack non può essere installato in questo momento - Si è verificato un problema - Jetpack installato - Installa Jetpack sul tuo sito. Può richiedere alcuni minuti per completare l\'operazione. - Installazione di Jetpack - Le credenziali del tuo sito web non verranno archiviate e saranno utilizzate per installare Jetpack. - Installa Jetpack Jetpack FAQ Jetpack Per utilizzare le Statistiche sul tuo sito WordPress, sarà necessario installare il plugin di Jetpack. @@ -2665,7 +2728,7 @@ Language: it Documenti Immagini Tutto - A %1$s è stato negato l\'accesso alle tue foto. Per risolvere questo problema, modifica le tue autorizzazioni e attiva %2$s. + %1$s è stato negato l\'accesso ai tuoi file multimediali. Per risolvere questo problema, modifica le tue autorizzazioni e attiva %2$s. Visualizza commenti Qualità dei video. Valori più alti comportano una migliore qualità dei video. Ridimensiona i video negli articoli secondo queste dimensioni diff --git a/WordPress/src/main/res/values-ja/strings.xml b/WordPress/src/main/res/values-ja/strings.xml index 10cfba2a9b02..e63f3c0cf3cc 100644 --- a/WordPress/src/main/res/values-ja/strings.xml +++ b/WordPress/src/main/res/values-ja/strings.xml @@ -1,11 +1,89 @@ + ブロックを削除 + プライバシーと評価 + 再生設定 + 再生バーの色 + 手動 + ダイナミック + 画像の目的を説明します。 デザインである場合は、空欄のままにします。 + カスタムメイドのスマートフォンに対応したレイアウトから始める + 別のページを作成 + サイトにページを追加 + 非表示 + 見つけやすく、共有やフォローがしやすいサイトアドレスを使って自分のサイトであることを示しましょう。 + カスタムドメインで自分のデジタルアイデンティティを手に入れましょう + ブログ投稿のリマインダーを使用するには、プッシュ通知を有効にする必要があります + プッシュ通知を有効にする + サブドメインで続行 + ドメインを購入する + 写真と動画 & 音楽と音声ファイル + 音楽と音声ファイル + 写真と動画 + %s は音声ファイルにアクセスするための権限が必要です + %s は動画にアクセスするための権限が必要です + %s は写真にアクセスするための権限が必要です + %s は写真と動画にアクセスするための権限が必要です + %s は音楽、音声ファイル、写真、動画にアクセスするための権限が必要です + 通知を有効にする + 「設定」&rarr;「通知」&rarr;「アプリ設定」の順に移動し、すぐに通知を受け取れるよう%1$sをオンにします。 + 通知を表示するにはアプリを開く必要があります。 + プッシュ通知はオフになっています + プッシュ通知はオフになっています。 + 通知権限の警告を無視します。 + 修正 + <b>%1$s</b> は個々の%2$s個の Jetpack プラグインを使用しています + <b>%1$s</b> は <b>%2$s</b> プラグインを使用しています + 個々の Jetpack プラグインを使用するサイトは WordPress アプリでサポートされていません。 + <b>%1$s</b> は WordPress アプリでサポートされていない個々の Jetpack プラグインを使用しています。 + <b>%1$s</b> は WordPress アプリでサポートされていない <b>%2$s</b> プラグインを使用しています。 + 一部のサイトにアクセスできません + サイトの1つにアクセスできません + Jetpack アプリに切り替えてください。完全な Jetpack プラグインを接続して、このサイトをアプリで使用する方法についてご案内します。 + Jetpack アプリに切り替える + %1$s が使用している %2$s は、まだアプリのすべての機能をサポートしていません。\n\nアプリをこのサイトと一緒に使用するには、%3$s をインストールしてください。 + このサイト + %1$s が使用している %2$s は、まだアプリのすべての機能をサポートしていません。 %3$s をインストールしてください。 + %1$s が使用している %2$s は、まだアプリのすべての機能をサポートしていません。 %3$s をインストールしてください。 + 数日後に Jetpack アプリに移動予定です。 + 切り替えは無料で、数分で終わります。 + 統計、Reader、通知などの Jetpack 機能は WordPress アプリから削除され、Jetpack アプリでのみ使用できるようになりました。 + 詳細は Jetpack.com をご覧ください + Jetpack アプリに切り替える + %sは Jetpack アプリに移動しました。 + %sは Jetpack アプリに移動しました。 + WP 管理画面 + 管理 + トラフィック + コンテンツ + セットアップ + 完了 + Jetpack がインストールされたので、あとはセットアップするだけです。 ほんの1分ほどで済みます。 + 今すぐ投稿を目立たせる + このページを目立たせる + この投稿を目立たせる + パフォーマンスの追跡、Blaze の開始と停止をいつでも実行できます。 + コンテンツは数百万もの WordPress と Tumblr のサイトに表示されます。 + 1日わずか数百円、たった数分で投稿やページを宣伝できます。 + Blaze でサイトのトラフィックを増やす + このドメインはすでに登録されています + セール + おすすめ + こちらもおすすめ + ヘルプ + Jetpack アプリにお切り替えいただきありがとうございます ! + ログ + チケット + 無料 + ヘルプ + Blaze + FAQ でよくある質問に対する回答をご確認ください。 ブロックのメニュー 非表示 何百万ものサイトに作品を表示しましょう。 @@ -18,26 +96,24 @@ Language: ja_JP 完全な Jetpack プラグイン 個々の Jetpack プラグイン %1$s プラグイン - %1$s が使用している %2$s は、まだアプリのすべての機能をサポートしていません。\n\nアプリをこのサイトと一緒に使用するには、%3$s をインストールしてください。 + %1$s が使用している %2$s は、まだアプリのすべての機能をサポートしていません。\n\nアプリをこのサイトと一緒に使用するには、%3$s をインストールしてください。 完全な Jetpack プラグインをインストールしてください 1つのサイトのみを利用できるため、主要サイトを変更できません。 - このサイトが使用している個別のプラグインは、まだアプリのすべての機能をサポートしていません。 完全な Jetpack プラグインをインストールしてください。 - サポートに連絡 - 再試行 - Jetpack は現在インストールできません。 - 問題が発生しました - エラーアイコン - 完了 - Jetpack がインストールされています - サイトに Jetpack をインストールしています。 完了まで数分かかります。 - Jetpack をインストールしています - 続行 - サイトの証明書は保存されず、Jetpack インストールの目的のみに使用されます。 - Jetpack をインストール - Jetpack アイコン + サポートに連絡 + 再試行 + Jetpack は現在インストールできません。 + 問題が発生しました + エラーアイコン + Jetpack がインストールされています + サイトに Jetpack をインストールしています。 完了まで数分かかります。 + Jetpack をインストールしています + 続行 + サイトの証明書は保存されず、Jetpack インストールの目的のみに使用されます。 + Jetpack をインストール + Jetpack アイコン Blaze を使って宣伝 - アプリでこのサイトを使用する準備ができました。 - サイトの可能性を十分に引き出すことができます。 Jetpack で統計、通知などを利用できます。 + アプリでこのサイトを使用する準備ができました。 + サイトの可能性を十分に引き出すことができます。 Jetpack で統計や通知などを利用できます。 ご利用のサイトには Jetpack プラグインが設定されています Jetpack モバイルアプリは、Jetpack プラグインと連携させて利用できます。 今すぐ乗り換えて、統計、通知、Reader などをご利用ください。 新しいコメントや「いいね」、表示数などの通知を受信できます。 @@ -80,6 +156,7 @@ Language: ja_JP <b>DayOne</b> から 非表示 後で再通知 + 統計、Reader、通知などの機能は、まもなく Jetpack モバイルアプリに移動します。 Jetpack アプリに切り替える 詳細は jetpack.com をご覧ください 切り替えは無料で、数分で終わります。 @@ -131,9 +208,6 @@ Language: ja_JP Jetpack でリンクを開く ヘルプが必要ですか ? OK - データの競合を避けるため、<b>WordPress アプリを削除</b>してください。 - WordPress アプリがまだインストールされているようです。 データの競合を避けるため、WordPress アプリを削除することをお勧めします。 - WordPress アプリは不要になりました ネットワークに接続しないと、データと設定を転送できません。 ネットワーク接続が有効であることを確認し、もう一度お試しください。 インターネットに接続できません。 @@ -143,13 +217,11 @@ Language: ja_JP 再試行 完了 WordPress アプリのアイコンを削除 - データの競合を避けるため、<b>WordPress アプリを削除</b>してください。 すべてのデータと設定を転送しました。 すべて移行先にあります。 Jetpack にお切り替えいただきありがとうございます。 WordPress アプリからの通知をオフにします。 通知内容はすべて同じですが、Jetpack アプリから送信されるようになりました。 Jetpack から通知が届くようになりました - WordPress アプリを削除してください WordPress ヘルプセンター サポート アプリが WordPress の通知を無効にすることを許可します。 @@ -729,6 +801,7 @@ Language: ja_JP GIF 1 利用できるプレビューがありません + タイトルを追加 テキスト色 パディング おすすめ @@ -736,6 +809,7 @@ Language: ja_JP 埋め込みを作成 カスタム URL カラム %d + 詳細 スクリーンリーダーのユーザーを支援するため、リンクについて簡単に説明してください ブロックを追加 Jetpack サイトが見つかりませんでした @@ -1152,7 +1226,6 @@ Language: ja_JP ストーリー投稿の紹介 空白ページが作成されました ページが作成されました - %1$s は写真へのアクセスを拒否されました。 これを解決するには、権限を編集して%2$sと%3$sをオンにしてください。 メディアの挿入に失敗しました。 メディアの挿入に失敗しました : %s WordPress メディアライブラリから選択 @@ -1632,6 +1705,7 @@ Language: ja_JP ここにブロックを追加 不明なエラーが発生しました。もう一度お試しください。 代替テキストを追加 + 説明を追加 「Add to Save Posts」ボタンをタップし、投稿をリストに保存します。 「リストに %1$d 項目が読み込まれました」 通知 @@ -1837,7 +1911,6 @@ Language: ja_JP その他のアイデアについてはキーワードを入力します 提案が見つかりません ドメインを登録 - Jetpack がインストールされたので、あとはセットアップするだけです。ほんの1分ほどで済みます。 統計概要から削除 下に移動 上に移動 @@ -2103,17 +2176,8 @@ Language: ja_JP フォローしているトピックがありません ここでトピックを追加して、お気に入りのトピックに関する投稿を検索します Jetpack を接続するのに使用した WordPress.com アカウントにログインします。 - 再試行 - 問題が発生しました - Jetpack がインストールされています - Jetpack をインストール - Jetpack をインストール Jetpack Jetpack よくある質問 - 設定 - Jetpack は現在インストールできません。 - サイトに Jetpack をインストールしています。完了まで数分かかることがあります。 - サイトのログイン情報は保存されず、Jetpack インストールのためのみに使用されます。 WordPress サイトの統計情報を利用するには、Jetpack プラグインのインするが必要です。 検索と一致するテーマがありません 何をお探しですか ? @@ -2665,7 +2729,7 @@ Language: ja_JP 文書 画像 すべて - %1$sは写真へのアクセスを拒否されました。これを解決するには、権限を編集して、%2$sをオンにしてください。 + %1$s はメディアファイルへのアクセスを拒否されました。 これを解決するには、権限を編集して %2$s をオンにします。 コメントを表示 動画の画質。値が高いほど、画質が良いことを意味します。 サイズ変更と動画圧縮を可能にするには有効化してください diff --git a/WordPress/src/main/res/values-kmr/strings.xml b/WordPress/src/main/res/values-kmr/strings.xml index 26eea8efbb15..c10c1fe10824 100644 --- a/WordPress/src/main/res/values-kmr/strings.xml +++ b/WordPress/src/main/res/values-kmr/strings.xml @@ -143,7 +143,6 @@ Language: ku_TR Şandiya çîrokî çawa tê afirandin Rûpela vala hat afirandin Danasîna Şandiyên Çîrokî - Hewla %1$s\'ê ya ji bo bigihîje wêneyên te hat redkirin. Ji bo vêya çareser bikî, destûrên xwe sererast bike, %2$s û %3$s\'ê veke. Tevlîkirina medyayê bi ser neket. Tevlîkirina medyayê bi ser neket: %s Ji Medyageha WordPressê Hilbijêre @@ -795,7 +794,6 @@ Language: ku_TR Pêşniyarên navperê(domain) nehatin barkirin Ji bo fikrên zêdetir, peyvkilîdekê binivîse Domain\'ê Tomar Bike - Va ye Jetpack hate sazkirin, îja divê em sazkariyên te temam bikin. Ev ê tenê xulekekê bigire. Şandî hat vegerandin Şandî tê vegerandin Şandî tê jêbirin @@ -1045,18 +1043,9 @@ Language: ku_TR Ti malperên te tune ye Mijarên şopandî tune ye Ji bo ku tu şandiyên têkildarî mijarên xwe yên favorî bibinî, mijaran tevlî vir bike - Dîsa Biceribîne - Bidomîne - Pirsgirêkek hebû - Jetpack tê sazkirin Jetpack - Jetpack hat sazkirin - Jetpack\'ê Saz Bike Jetpack PPP Ji bo Jetpackê girêbidî têkeve hesabê xwe yê WordPress.com\'ê yê tu bi kar tînî. - Jetpack niha nayê sazkirin. - Jetpack li malpera te tê sazkirin. Qedandina vê kirariyê dê çend xulekan bigire. - Dê agahiyên nasnameya malpera te neyê veşartin ew ê tenê ji bo sazkirina Jetpackê werin bi kar anîn. Ji bo ku tu Amarên malpera xwe ya WordPressê bi kar bînî divê tu pêveka Jetpackê saz bikî. Tu dixwazî çi bibînî? Lêgerîna te û ti medya li hev nayên @@ -1593,7 +1582,6 @@ Language: ku_TR Wêne Hemû Dosye - Hewla %1$s\'ê ya ji bo bigihîje wêneyên te hat redkirin. Ji bo vê çareser bikî, destûrên xwe sererast bike û %2$s\'ê veke. Şîroveyan bibîne Kalîteya vîdeoyan. Nirxên bilind tê wateya vîdyoyên kalîteya çêtir. Mezinahiya vîdeoyên di şandiyê de dike bi qasî vê mezinahiyê diff --git a/WordPress/src/main/res/values-ko/strings.xml b/WordPress/src/main/res/values-ko/strings.xml index ff95173f4b22..63665df58bae 100644 --- a/WordPress/src/main/res/values-ko/strings.xml +++ b/WordPress/src/main/res/values-ko/strings.xml @@ -1,11 +1,89 @@ + 블록 제거 + 개인정보 보호 및 평점 + 재생 설정 + 재생 표시줄 색상 + 수동 + 동적 + 이미지의 용도를 설명합니다. 장식용이면 비워둡니다. + 모바일 친화적인 맞춤형 레이아웃으로 시작 + 다른 페이지 생성 + 사이트에 페이지 추가 + 숨기기 + 찾고 공유하고 팔로우하기 쉬운 사이트 주소로 웹 코너에 대한 권리를 주장하세요. + 사용자 정의 도메인으로 온라인 정체성 소유 + 블로깅 알림을 사용하려면 푸시 알림을 켜야 합니다. + 푸시 알림 켜기 + 하위 도메인으로 계속 + 도메인 구매 + 사진과 비디오 및 음악과 오디오 + 음악과 오디오 + 사진과 비디오 + 오디오에 접근하는 권한이 %s에 필요 + 비디오에 접근하는 권한이 %s에 필요 + 사진에 접근하는 권한이 %s에 필요 + 사진과 비디오에 접근하는 권한이 %s에 필요 + 음악, 오디오, 사진 및 비디오에 접근하는 권한이 %s에 필요 + 알림 켜기 + 설정 &rarr; 알림 &rarr; 앱 설정으로 이동하여 즉시 %1$s의 알림을 켜세요. + 앱을 열어야 알림이 표시됩니다. + 푸시 알림이 꺼져 있음 + 푸시 알림이 꺼져 있습니다. + 알림 권한 경고를 해제하세요. + 해결 + 개별 젯팩 플러그인 %2$s개를 <b>%1$s</b>에서 사용하고 있습니다. + <b>%2$s</b> 플러그인을 <b>%1$s</b>에서 사용하고 있습니다. + 개별 젯팩 플러그인을 사용하는 사이트는 워드프레스 앱에서 지원되지 않습니다. + 워드프레스 앱에서 지원되지 않는 개별 젯팩 플러그인을 <b>%1$s</b>에서 사용하고 있습니다. + 워드프레스 앱에서 지원되지 않는 <b>%2$s</b> 플러그인을 <b>%1$s</b>에서 사용하고 있습니다. + 사이트 중 일부에 접근할 수 없음 + 사이트 중 하나에 접근할 수 없음 + 전체 젯팩 플러그인 연결 방법을 안내해 드리는 젯팩 앱으로 전환하여 앱으로 이 사이트를 이용하세요. + 젯팩 앱으로 전환 + %1$s에서는 %2$s을(를) 사용하고 있으며 여기에서는 아직 앱의 모든 기능이 지원되지 않습니다.\n\n이 사이트에서 앱을 사용하려면 %3$s을(를) 설치하세요. + 이 사이트 + %1$s에서는 %2$s을(를) 사용하고 있으며 여기에서는 아직 앱의 모든 기능이 지원되지 않습니다. %3$s을(를) 설치하세요. + %1$s에서는 %2$s을(를) 사용하고 있으며 여기에서는 아직 앱의 모든 기능이 지원되지 않습니다. %3$s을(를) 설치하세요. + 며칠 후 젯팩 앱이 이동됩니다. + 전환은 무료이며 1분밖에 걸리지 않습니다. + 통계, 리더, 알림 및 기타 젯팩에서는 워드프레스 앱에서 제거된 기능이 제공되며, 젯팩 앱에서만 찾을 수 있습니다. + Jetpack.com에서 더 알아보기 + 젯팩 앱으로 전환 + %s이(가) 젯팩 앱으로 이동되었습니다. + %s이(가) 젯팩 앱으로 이동되었습니다. + WP 관리자 + 관리 + 트래픽 + 콘텐츠 + 설정 + 완료 + 젯팩 설치되었으므로 설정해야 합니다. 이 작업은 1분 정도 소요됩니다. + 지금 글 Blaze + 이 페이지 Blaze + 이 글 Blaze + 성과를 추적하며 언제든지 Blaze를 시작하고 중지하세요. + 콘텐츠가 수백만 개의 워드프레스 및 Tumblr 사이트에 표시됩니다. + 하루에 적은 금액으로 단 몇 분 만에 글 또는 페이지를 홍보하세요. + Blaze를 통해 사이트에 더 많은 트래픽 유도 + Blaze + 이 도메인은 이미 등록되었습니다. + 세일 + 권장 + 최선의 대안 + 도움말 + 궁금할 수도 있는 일반적인 질문에 대한 답변은 FAQ를 참조하세요. + 젯팩 앱으로 전환해 주셔서 감사합니다! + 로그 + 티켓 + 무료 + 도움말 블록 메뉴 숨기기 수백만 개 사이트에 작업을 표시하세요. @@ -18,26 +96,24 @@ Language: ko_KR 전체 젯팩 플러그인 개별 젯팩 플러그인 %1$s 플러그인 - %1$s에서는 %2$s 플러그인을 사용하고 있어서 아직 앱의 모든 기능이 지원되지 않습니다.\n\n이 사이트에서 %3$s 앱을 사용하려면 을(를) 설치하세요. + %1$s에서는 %2$s을(를) 사용하고 있으며 여기에서는 아직 앱의 모든 기능이 지원되지 않습니다.\n\n이 사이트에서 앱을 사용하려면 %3$s을(를) 설치하세요. 전체 젯팩 플러그인을 설치하세요. 하나의 사이트만 이용할 수 있어서 기본 사이트를 변경할 수 없습니다. - 이 사이트에서는 개별 플러그인을 사용하고 있어서 아직 앱의 모든 기능이 지원되지 않습니다. 전체 젯팩 플러그인을 설치하세요. - 지원 문의 - 다시 시도 - 지금은 젯팩을 설치할 수 없습니다 - 문제가 발생했습니다. - 오류 아이콘 - 완료 - 앱에서 이 사이트를 이용할 준비가 되었습니다. - 젯팩 설치됨 - 사이트에 젯팩을 설치하는 중입니다. 완료하는 데 몇 분 정도 걸릴 수 있습니다. - 젯팩 설치하기 - 계속 - 웹사이트 자격 증명은 저장되지 않으며 젯팩을 설치하는 용도로만 사용됩니다. - 젯팩 설치 - 젯팩 아이콘 + 지원 문의 + 다시 시도 + 지금은 젯팩을 설치할 수 없습니다 + 문제가 발생했습니다. + 오류 아이콘 + 앱에서 이 사이트를 이용할 준비가 되었습니다. + 젯팩 설치됨 + 사이트에 젯팩을 설치하는 중입니다. 완료하는 데 몇 분 정도 걸릴 수 있습니다. + 젯팩 설치하기 + 계속 + 웹사이트 자격 증명은 저장되지 않으며 젯팩을 설치하는 용도로만 사용됩니다. + 젯팩 설치 + 젯팩 아이콘 Blaze로 홍보 - 사이트의 전체 잠재력을 잠금 해제하세요. 젯팩으로 통계, 알림 등을 받으세요. + 사이트의 전체 잠재력을 잠금 해제하세요. 젯팩으로 통계, 알림 등을 이용하세요. 사이트에 젯팩 플러그인 있음 젯팩 모바일 앱은 젯팩 플러그인과 연동하도록 설계되었습니다. 지금 전환하여 통계, 알림, 리더 등에 대한 접근 권한을 받으세요. 새 댓글, 좋아요, 조회수 등에 대한 알림을 받으세요. @@ -47,10 +123,13 @@ Language: ko_KR 젯팩을 통해 워드프레스 사이트에서 더 많은 작업을 수행할 수 있습니다. 전환은 무료이며 1분밖에 걸리지 않습니다. 젯팩으로 워드프레스 성능 향상 언제든지 내 사이트 > 설정 > 블로깅에서 블로깅 프롬프트와 알림을 관리할 수 있습니다. + 영감을 주는 단어 또는 짧은 구문이 알림에 포함됩니다. <b>사이트 설정</b>으로 이동하여 다시 켜기 블로깅 프롬프트 숨김 프롬프트 끄기 자원봉사자 그룹의 도움말 받기 + 커뮤니티 포럼 + 블로깅 알림 프롬프트 표시 블로깅 젯팩 앱을 이용하려면 Google Play 스토어를 설치하세요. @@ -77,6 +156,7 @@ Language: ko_KR <b>DayOne</b>부터 숨기기 나중에 다시 알림 + 곧 통계, 리더, 알림 및 기타 기능이 젯팩 모바일 앱으로 이동됩니다. 젯팩 앱으로 전환 jetpack.com에서 더 알아보기 전환은 무료이며 1분밖에 걸리지 않습니다. @@ -128,9 +208,6 @@ Language: ko_KR 젯팩에서 링크 열기 도움말이 필요하신가요? 알겠습니다. - 데이터가 충돌하지 않도록 <b>워드프레스 앱</b>을 삭제하세요. - 아직 워드프레스 앱이 설치되어 있는 것 같습니다. 데이터가 충돌하지 않도록 워드프레스 앱을 삭제하는 것이 좋습니다. - 더는 워드프레스 앱 필요 없음 네트워크 연결 없이 데이터와 설정을 이전할 수 없습니다. 네트워크 연결이 작동하도록 점검하고 다시 시도하세요. 인터넷에 연결할 수 없습니다. @@ -140,13 +217,11 @@ Language: ko_KR 다시 시도 마침 워드프레스 앱 아이콘 제거 - 데이터가 충돌하지 않도록 <b>워드프레스 앱</b>을 삭제하세요. 모든 데이터와 설정을 이전했습니다. 모든 것이 그대로 다 남아 있습니다. 젯팩으로 전환해 주셔서 감사합니다! 워드프레스 앱의 알림을 해제하겠습니다. 동일한 알림을 모두 받겠지만, 이제는 젯팩 앱에서 알림을 보냅니다. 이제 젯팩에서 알림 전송 - 워드프레스 앱 삭제 요망 워드프레스 도움말 센터 지원 앱에 워드프레스 알림 비활성화를 허용하세요. @@ -161,8 +236,8 @@ Language: ko_KR 아이콘 상위 페이지 페이지 속성 - 뉴스 참여 + 뉴스 1개 답변 워드프레스 아이콘 어디에서나 쓰고, 편집하고 발생합니다. @@ -239,9 +314,9 @@ Language: ko_KR 합계 기타 검색 + 워드프레스 조회수 예약 - 워드프레스 글 예약 알림 설정 블로깅 알림 설정 @@ -258,8 +333,8 @@ Language: ko_KR 로그인 코드 검사 ⭐️ 최신 글 %1$s에 %2$s 좋아요가 있습니다. 활동이 충분하지 않습니다. 나중에 사이트의 방문자가 증가하면 다시 확인하세요! - %1$s(%2$s%%) %1$s, 총 팔로워 중 %2$s%% + %1$s(%2$s%%) 링크 복사 축하합니다! 척척박사<br/> 앱 알아보기 @@ -356,10 +431,10 @@ Language: ko_KR 참고: 영감을 발휘할 수 있도록 알림판에 매일 새로운 프롬프트가 표시됩니다! 더 좋은 작가가 되는 가장 좋은 방법은 글쓰기를 습관화하고 다른 사람과 공유하는 것입니다. 그래서 프롬프트가 필요합니다! - 알림 설정 - 정기적으로 글을 작성하여 신규 독자를 유치하세요. 글을 쓰고 싶을 때 알려주시면 알림을 보내드리겠습니다! 소개\n블로깅 프롬프트 + 알림 설정 블로깅 프롬프트 포함 + 정기적으로 글을 작성하여 신규 독자를 유치하세요. 글을 쓰고 싶을 때 알려주시면 알림을 보내드리겠습니다! 습관화하여 더 좋은 작가 되기 글쓰기 및 시 여행 @@ -394,8 +469,8 @@ Language: ko_KR 예: 패션, 시, 정치 사이트 주제 계속하려면 <b>%1$s</b>을(를) 누르세요. - 더 많은 프롬프트 보기 오늘 건너뛰기 + 더 많은 프롬프트 보기 %d개 답변 블로깅 프롬프트 공유 ✓ 답변됨 @@ -405,8 +480,8 @@ Language: ko_KR 이 색상 조합은 사람들이 읽기 힘들 수 있습니다. 더 밝은 배경 색상 및/또는 더 어두운 텍스트 색상을 사용해 보세요. 이 색상 조합은 사람들이 읽기 힘들 수 있습니다. 더 어두운 배경 색상 및/또는 더 밝은 텍스트 색상을 사용해 보세요. 미디어를 삽입하지 못했습니다.\n자세히 알아보려면 누르세요. - 웹사이트의 주제가 무엇인가요? 아래 목록에서 테마를 선택하거나 원하는 테마를 입력하세요. + 웹사이트의 주제가 무엇인가요? 주간 총정리 카테고리 추가하기 @@ -435,24 +510,24 @@ Language: ko_KR 뒤로 아이콘 Automattic 로고 워드프레스 - 젯팩 - 소스 코드 - 개인정보 취급방침 - 서비스 약관 - 어디에서나 근무 WooCommerce Tumblr Simplenote Pocket Casts + 젯팩 Day One + 소스 코드 + 개인정보 취급방침 + 서비스 약관 + 어디에서나 근무 워드프레스닷컴 사용 Automattic 회원 법적 고지 사항 및 기타 + Twitter 인스타그램 평가하기 친구와 공유 편집기의 웹 버전을 사용하여 이 블록을 편집하실 수 있습니다. - Twitter 젯팩 보안 설정 열기 참고: 모바일 편집기에서 이 블록을 편집하려면 워드프레스닷컴 로그인을 허용해야 합니다. 참고: 레이아웃은 테마와 화면 크기에 따라 달라질 수 있습니다. @@ -463,8 +538,8 @@ Language: ko_KR 알림판이 업데이트되지 않았습니다. 새로 고치려면 연결을 확인하고 끌어오세요. 알림판을 업데이트할 수 없습니다. 비디오가 업로드되지 않았습니다! 5분을 초과하는 비디오를 업로드하려면 유료 요금제가 필요합니다. - 캘리포니아주 개인정보 공지 승인 + 캘리포니아주 개인정보 공지 버전 %1$s 승인 법적 고지 사항 및 기타 @@ -483,8 +558,8 @@ Language: ko_KR 첫 번째 댓글 달기 모든 댓글 보기 글 데이터를 가져오는 중 오류가 발생했습니다. - 대화 팔로우 설정 댓글을 가져오는 중 오류가 발생했습니다. + 대화 팔로우 설정 클립보드에서 특성 이미지 클립보드에서 URL 복사, %s @@ -527,8 +602,8 @@ Language: ko_KR 이 대화 구독 해지됨 이 대화 팔로우 중\n앱 내 알림을 활성화하나요? 도메인 검색 - 이용 중인 요금제에 1년 무료 도메인 등록이 포함되어 있음 이 사이트에서 구매한 도메인에서는 방문자가 <b>%s</b>(으)로 리디렉팅됩니다. + 이용 중인 요금제에 1년 무료 도메인 등록이 포함되어 있음 무료 도메인 신청 도메인 관리 도메인 추가 @@ -538,8 +613,8 @@ Language: ko_KR 기본 사이트 주소 사이트 주소 변경 무료 워드프레스닷컴 주소: - %s<span style=\"color:#50575e;\">/년</span> 첫해 </span><span style=\"color:#50575e;\"><s>%2$s/년 <span style=\"color:#B26200;\">%1$s</s></span> + %s<span style=\"color:#50575e;\">/년</span> 취소하시겠어요? 저장되지 않은 변경 사항이 있음 댓글은 비워 둘 수 없음 @@ -563,14 +638,14 @@ Language: ko_KR <a href=\"\">회원님</a>이 좋아합니다. 줄 높이 도메인 가져오기 - 권장 앱 템플릿 가져오기 중 알 수 없는 오류 발생 %s + 권장 앱 템플릿 가져오기 중 알 수 없는 오류 발생 유효하지 않은 응답 수신됨 수신된 응답 없음 + Automattic 앱 - 어느 화면에서나 사용할 수 있는 앱 친구와 WordPress 공유 퀵 링크 도메인 - Automattic 앱 - 어느 화면에서나 사용할 수 있는 앱 주간 총정리: %s 알림 시간 블로깅에 대한 알림을 <b>매일</b> <b>%s</b>에 받게 됩니다. @@ -600,8 +675,8 @@ Language: ko_KR 블록은 코드를 작성하는 방법을 몰라도 삽입, 재정렬 및 스타일 지정이 가능한 콘텐츠 조각입니다. 블록은 아름다운 레이아웃을 생성할 수 있는 쉽고 현대적인 방법입니다. 블록을 사용하면 메시지를 전달하는 데 도움이 되도록 필요한 모든 형식 지정 도구가 준비되어 있으므로 콘텐츠 작성에 집중할 수 있습니다. 콘텐츠를 열로 정렬하고, 행동 유도 버튼을 추가하고, 이미지에 텍스트를 입히세요. - %1$s/%2$s개 완료 왼쪽 하단의 도구 모음에서 + 아이콘을 눌러 언제든지 새 블록을 추가하세요. + %1$s/%2$s개 완료 간단한 연습을 통해 기본 사항을 알아보세요. 하나 이상의 댓글 검토 실패 사이트 생성하기 @@ -640,9 +715,9 @@ Language: ko_KR 블로깅할 일자 선택 내 사이트 > 설정에 > 블로깅 알림에서 언제든지 업데이트할 수 있습니다. 설정된 알림이 없습니다. + 블로깅에 대한 알림을 일주일에 %1$s번 %2$s %3$s에 받게 됩니다. 알림이 제거되었습니다! 모두 설정되었습니다! - 블로깅에 대한 알림을 일주일에 %1$s번 %2$s %3$s에 받게 됩니다. 업데이트 설정 없음 일주일에 %s번 @@ -658,14 +733,14 @@ Language: ko_KR 완료 알림 받기 클릭 한 번으로 백업에서 사이트를 복원할 수 있도록 <a href=\"%1$s\">서버 자격 증명을 입력하세요</a>. - 카테고리 생성 - Android용 WordPress 지원 특성 이미지로 설정 특성 이미지 제거 + 카테고리 생성 + Android용 WordPress 지원 사이트의 카테고리 관리 카테고리 - 최신 글 페이지의 콘텐츠는 자동으로 생성되며 편집할 수 없습니다. 알림 + 최신 글 페이지의 콘텐츠는 자동으로 생성되며 편집할 수 없습니다. 경계선 설정 다시 보지 않음 저장 공간 보기 @@ -688,8 +763,8 @@ Language: ko_KR 두 번 눌러 이미지 또는 비디오를 추가할 작업 시트 열기 현재 단위는 %s입니다. 교차 글 - 열 설정 일반 블록으로 %s 변환됨 + 열 설정 %s에 링크 추가 링크 텍스트 추가 이미지 또는 비디오 추가 @@ -706,13 +781,13 @@ Language: ko_KR 이미 사이트가 있으면 무료 젯팩 플러그인을 설치하고 워드프레스닷컴 계정에 연결해야 합니다. 프로필 사진 %1$s에 이 앱을 사용하려면 설치되고 워드프레스닷컴 계정에 연결된 젯팩 플러그인이 있어야 합니다. - 링크 관계 - 이미지를 뒤로 이동 이미지를 앞으로 이동 + 이미지를 뒤로 이동 폭 설정 + 링크 관계 열 설정 - 사이트 (제목이 없습니다) + 사이트 사용자 프로필 하단 시트 정보 좋아요 목록 %s 2 @@ -720,19 +795,21 @@ Language: ko_KR %s 소셜 아이콘 멘션 신규 - 페이지 미리 보기 글 미리 보기 + 페이지 미리 보기 재시도 GIF 1 + 제목 추가 사용 가능한 미리보기 없음 텍스트 색상 안쪽 여백 - 추천 4 - 임베드 생성 + 추천 사용자 정의 URL + 임베드 생성 열 %d + 더 보기 화면 리더 사용자에게 도움이 되도록 간략하게 링크 설명 블록 추가 젯팩 사이트를 찾을 수 없음 @@ -741,10 +818,10 @@ Language: ko_KR 블록 변형… 미디어를 삽입하는 데 실패했습니다. 오디오 파일 삽입에 실패했습니다. + 이미지의 용도를 설명합니다. 이미지가 장식용이면 비워둡니다. %2$s(으)로 변형된 %1$s 좋아요 데이터 로드 중 오류가 발생했습니다. %s. %d 좋아요 - 이미지의 용도를 설명합니다. 이미지가 장식용이면 비워둡니다. 좋아요 1개 제안: 아이콘 버튼 사용 @@ -752,6 +829,7 @@ Language: ko_KR 검색 버튼입니다. 현재 버튼 텍스트는 블록 검색 블록 레이블을 검색합니다. 현재 텍스트는 + 외부 설정된 사용자 정의 플레이스홀더 없음 내부 검색 헤딩요소 숨기기 @@ -759,7 +837,6 @@ Language: ko_KR 두 번 눌러 레이블 텍스트 편집하기 두 번 눌러 버튼 텍스트 편집하기 두 번 눌러 단위 변경하기 - 외부 현재 플레이스홀더 텍스트는 검색 지우기 검색 취소 @@ -770,8 +847,8 @@ Language: ko_KR 사용 가능한 네트워크가 없습니다. 응답 없는 댓글 없음 응답 없음 - 설정 검색 링크 추가 + 설정 검색 항상 허용되는 IP 주소 허용되지 않는 댓글 추가 버튼 텍스트 @@ -780,8 +857,8 @@ Language: ko_KR 해제 다운로드하기 위협이 해결되었습니다. - 스캔에서 %2$s에 대한 잠재적 위협 %1$s개를 찾았습니다. 아래 내용을 검토하고 조치를 취하거나 모두 해결 버튼을 누르세요. 도움말이 필요하면 %3$s을(를) 클릭하세요. %s개의 활성 위협을 모두 해결할지 확인해 주세요. + 스캔에서 %2$s에 대한 잠재적 위협 %1$s개를 찾았습니다. 아래 내용을 검토하고 조치를 취하거나 모두 해결 버튼을 누르세요. 도움말이 필요하면 %3$s을(를) 클릭하세요. 이러한 위협을 해결하기 위해 백그라운드에서 노력하는 중입니다. 그동안 정상적으로 사이트를 계속 사용하면서 언제든지 진행률을 확인하실 수 있습니다. 초점 편집하기 이미지를 편집, 교체 또는 지울 하단 시트를 두 번 눌러 열기 @@ -813,13 +890,13 @@ Language: ko_KR <b>스캔 완료됨</b> <br> 찾은 위협 없음 위협 해결 중 비활성화 - 페이지를 점검하고 내용을 변경하거나 페이지를 추가 또는 제거하세요. 이 항목 고정 취소 + 페이지를 점검하고 내용을 변경하거나 페이지를 추가 또는 제거하세요. + 사이트 보기 회원님께 영감을 주는 사이트를 발견하고 팔로우하세요. + 소셜 공유 새 글을 소셜 미디어에 자동으로 공유합니다. 개성과 게시글을 반영하는 이름을 사이트에 지정하세요. - 소셜 공유 - 사이트 보기 사이트 통계 확인 그래도 다운로드 가능한 백업 파일 만들기는 시도됩니다. 다운로드 가능한 백업에 걸리는 시간을 알려주는 상태를 찾을 수 없습니다. @@ -869,8 +946,8 @@ Language: ko_KR Jetpack Scan에 오신 것을 환영합니다. 지금 회원님 사이트를 살펴보고 있으며 결과를 곧 알려드리겠습니다. 이 위협을 해결하기 위해 백그라운드에서 노력하는 중입니다. 그동안 정상적으로 사이트를 계속 사용하면서 언제든지 진행률을 확인하실 수 있습니다. 위협이 발견되면 알림을 보내드립니다. 그동안 정상적으로 사이트를 계속 사용하면서 언제든지 진행률을 확인하실 수 있습니다. - Jetpack Scan에서 사이트 스캔을 완료할 수 없습니다. 사이트가 다운되었는지 확인해 주세요. 다운되지 않았으면 다시 시도해 주세요. 다운되었거나 Jetpack Scan에서 계속 문제가 발생하는 경우 지원팀에 문의하세요. 위협 해결 중 + Jetpack Scan에서 사이트 스캔을 완료할 수 없습니다. 사이트가 다운되었는지 확인해 주세요. 다운되지 않았으면 다시 시도해 주세요. 다운되었거나 Jetpack Scan에서 계속 문제가 발생하는 경우 지원팀에 문의하세요. 문제가 발생했습니다 사이트 백업 중 %1$s %2$s에서 사이트 백업 중 @@ -879,12 +956,12 @@ Language: ko_KR 사이트가 백업되었습니다.\n%1$s %2$s에서 백업됨 사이트가 백업되는 중입니다.\n%1$s %2$s에서 백업 중 오디오 선택 - 완료 버튼 - 오류 아이콘 실행 중인 다른 복원이 있습니다. - 완료 버튼 + 오류 아이콘 + 완료 버튼 복원 실패 사이트 방문 버튼 + 완료 버튼 복원 아이콘 사이트 방문 이제 선택한 항목이 모두 %1$s %2$s(으)로 다시 복원되었습니다. @@ -897,8 +974,6 @@ Language: ko_KR 느낌표가 있는 빨간색 원 이미지 경고 사이트 복원 버튼 - 완료 - 완료 버튼 복원 아이콘 사이트 복원 %1$s %2$s이(가) 사이트를 복원을 위해 선택한 시점입니다. @@ -906,15 +981,17 @@ Language: ko_KR 복원할 항목 선택: 복원 X로 표시된 구름 아이콘 - 모바일 + 완료 버튼 + 완료 다운로드 실패 태블릿 + 모바일 다시 보지 않음 + 이 항목 고정 페이지 목록을 참조하려면 %1$s 페이지 %2$s을(를) 선택하세요. 사이트의 페이지를 변경, 추가 또는 제거합니다. 사이트 페이지 검토 홈페이지를 편집하려면 %1$s 홈페이지 %2$s을(를) 선택하세요. - 이 항목 고정 안 읽음으로 표시 읽음으로 표시 미디어 업로드에 실패했습니다.\n%1$s @@ -923,15 +1000,16 @@ Language: ko_KR 안 읽음으로 표시됨 읽음으로 표시됨 해결 상태를 가져오는 중 오류가 발생했습니다. 지원팀에 문의하세요. + 위협이 해결되었습니다. 위협 해결 중 오류가 발생했습니다. 지원팀에 문의하세요. 1개의 활성 위협을 해결할지 확인해 주세요. - 위협이 해결되었습니다. 위협 모두 해결 위협을 무시하는 중 오류가 발생했습니다. 지원팀에 문의하세요. 위협이 무시되었습니다. 안전한 것이 확실하지 않은 한 보안을 무시해서는 안 됩니다. 이 위협 무시를 선택하면 사이트 <b>%s</b>에서 위협이 유지됩니다. 위협 해결 중 오류가 발생했습니다. 지원팀에 문의하세요. 위협 무시됨 + %s에 위협 해결됨 위협 해결 중 무시됨 항목을 찾을 수 없음 @@ -944,509 +1022,507 @@ Language: ko_KR 날짜 범위 조정 시도 일치하는 백업을 찾을 수 없음 첫 번째 백업이 24시간 이내에 여기에 표시되고 백업이 완료되면 알림이 수신됩니다. - %s에 위협 해결됨 첫 번째 백업이 곧 준비됩니다 요청 처리 중 문제가 발생했습니다. 나중에 다시 시도하세요. 아래로 이동하기 블록 위치 변경 - 링크 버튼 공유하기 - 파일에 연결하는 링크도 이메일했습니다. - 아이콘 업로드하기 - 이제 백업을 다운로드할 수 있습니다 - %1$s %2$s에서 사이트의 백업을 성공적으로 만들었습니다. - 다운로드하기 - 링크 공유하기 - 다운로드할 수 있는 백업 준비 아이콘 + 아이콘 + 파일에 연결하는 링크도 이메일했습니다. + 링크 버튼 공유하기 다운로드하기 버튼 - 현재 다운로드할 수 있는 사이트의 백업을 만드는 중입니다 - %1$s %2$s에서 다운로드할 수 있는 사이트의 백업을 만들고 있습니다. - 다운로드할 수 있는 백업 만들기 아이콘 + 다운로드할 수 있는 백업 준비 아이콘 + 링크 공유하기 + 다운로드하기 + %1$s %2$s에서 사이트의 백업을 성공적으로 만들었습니다. + 이제 백업을 다운로드할 수 있습니다 백업 기다리시지 않아도 됩니다. 백업이 준비되면 알려드립니다. - %1$s %2$s은(는) 다운로드할 수 있는 백업을 만들기 위해 선택한 지점입니다. - 다운로드할 수 있는 백업 만들기 버튼 - 요청을 처리하는 중에 문제가 있었습니다. 나중에 다시 시도하기 바랍니다. - 다른 다운로드를 실행하고 있습니다. + 다운로드할 수 있는 백업 만들기 아이콘 + %1$s %2$s에서 다운로드할 수 있는 사이트의 백업을 만들고 있습니다. + 현재 다운로드할 수 있는 사이트의 백업을 만드는 중입니다 백업 다운로드하기 + 다른 다운로드를 실행하고 있습니다. + 요청을 처리하는 중에 문제가 있었습니다. 나중에 다시 시도하기 바랍니다. + 다운로드할 수 있는 백업 만들기 버튼 + %1$s %2$s은(는) 다운로드할 수 있는 백업을 만들기 위해 선택한 지점입니다. %1$s · %2$s · - %1$s · %1$s · %2$s - 젯팩 검사하기가 영향이 있는 파일이나 디렉터리를 지울 것입니다. - 젯팩 검사하기를 새 버전(%s)으로 업데이트할 것입니다. - 젯팩 검사하기가 영향이 있는 파일이나 디렉터리를 편집할 것입니다. - 젯팩 검사하기가 그 위협을 해결할 것입니다. - 위협 고치기 - 위협 무시하기 - 무료 견적 얻기 - 사용자 + %1$s · 교차글 - 제안 목록을 입력하여 필터링하세요. - 사용 가능한 %s 제안이 없습니다. - 제안 로드 중 문제가 발생했습니다. + 사용자 일치하는 %s이(가) 없습니다. + 제안 로드 중 문제가 발생했습니다. + 사용 가능한 %s 제안이 없습니다. + 제안 목록을 입력하여 필터링하세요. + 무료 견적 얻기 + 위협 무시하기 + 위협 고치기 + 젯팩 검사하기가 그 위협을 해결할 것입니다. + 젯팩 검사하기가 영향이 있는 파일이나 디렉터리를 편집할 것입니다. + 젯팩 검사하기를 새 버전(%s)으로 업데이트할 것입니다. + 젯팩 검사하기가 영향이 있는 파일이나 디렉터리를 지울 것입니다. 젯팩 검사하기는 영향이 있는 파일이나 디렉터리를 교체할 것입니다. 젯팩 검사하기가 이 위협을 자동으로 고칠 수 없습니다.\n 그 위협을 직접 해결하기를 권장합니다: 워드프레스, 테마, 그리고 모든 플러그인이 최신인지 확인하고, 사이트에서 문제일 수 있는 코드, 테마, 또는 플러그인을 제거하세요. \n \n\n 이 위협을 해결하는데 도움이 필요하면, 고도로 검증한 워드프레스 전문가의 신뢰할 수 있는 프리랜서 시장인<b>코더블</b>을 권장합니다.\n 이 프로젝트를 도울 수 있는 보안 전문가의 그룹을 선별했습니다. 가격대는 시간당 70–120 달러이고, 고요할 의무 없이 무료로 견적을 확인할 수 있습니다.\n - 문제가 무엇이었나요? - 기술 세부정보 - 파일에서 발견한 위협: - 어떻게 고칠까요? - 젯팩을 어떻게 고쳤을까요? 위협 해결하는 중 - 대이터배이스 %s 위협 - %s: 의심스러운 코드 유형 - 기타 취약점 + 젯팩을 어떻게 고쳤을까요? + 어떻게 고칠까요? + 파일에서 발견한 위협: + 기술 세부정보 + 문제가 무엇이었나요? + 위협 세부정보 + 테마에서 취약점을 찾았습니다 + 플러그인에서 취약점을 찾았습니다 위협을 찾았습니다 %s 워드프레스에서 취약점을 찾았습니다 - 플러그인에서 취약점을 찾았습니다 - 테마에서 취약점을 찾았습니다 - 위협 세부정보 - 취약한 플러그인: %1$s(버전 %2$s) + 기타 취약점 취약한 테마: %1$s(버전 %2$s) - 이 사이트 - %s시간 전 - %s분 전 - 위협을 찾았습니다 + 취약한 플러그인: %1$s(버전 %2$s) + %s: 의심스러운 코드 유형 + 대이터배이스 %s 위협 %s: 감염 코어 파일 - 몇 초 전 + 위협을 찾았습니다 모두 고치기 + 몇 초 전 + %s분 전 + %s시간 전 + 이 사이트 %1$s에 마지막 젯팩 스캔이 실행되었으며 위험이 발견되지 않았습니다. %2$s - 활동 유형 필터 (%s유형을 선택했습니다) - 백업하기 - 상태 검사하기 아이콘 - 지금 검사하기 - 다시 검사하기 - 걱정하지 마세요 사이트가 위험할 수 있습니다 - 인터넷 연결을 확인하고 다시 시도하기 바랍니다. - 사용할 수 있는 활동이 없습니다 - 선택한 날짜 범위에 활동 기록이 없습니다. - 활동 유형 필터 + 걱정하지 마세요 + 다시 검사하기 + 지금 검사하기 + 상태 검사하기 아이콘 + 백업하기 + 활동 유형 필터 (%s유형을 선택했습니다) %1$s(%2$s 아이템 표시) - 날짜 범위 필터 - 활동 유형 (%s) + 활동 유형 필터 + 선택한 날짜 범위에 활동 기록이 없습니다. + 사용할 수 있는 활동이 없습니다 + 인터넷 연결을 확인하고 다시 시도하기 바랍니다. 연결이 없습니다 - 일치하는 이벤트가 없습니다 + 활동 유형 (%s) + 날짜 범위 필터 날짜 범위나 활동 유형 필터 조정하기 - 다운로드할 수 있는 백업 만들기 - 워드프레스 테마 - 워드프레스 플러그인 - 미디어 업로드 + 일치하는 이벤트가 없습니다 사이트 데이터배이스 (wp-config.php 및 비 WordPress 파일 포함) - 날짜 범위 - 활동 유형 - 이 지점으로 복원하기 - 백업 다운로드하기 - 파일 선택하기 - 오류 + 미디어 업로드 + 워드프레스 플러그인 + 워드프레스 테마 + 다운로드할 수 있는 백업 만들기 + 다운로드할 수 있는 파일 만들기 + 다운로드 할 수 있는 백업 만들기 백업 다운로드하기 백업 다운로드 - 다운로드 할 수 있는 백업 만들기 - 다운로드할 수 있는 파일 만들기 - 복제하기 - 글 동기화 충돌 - 먼저 글 편집하기 - 이 앱으로부터 버전 복사하기 + 오류 + 파일 선택하기 + 백업 다운로드하기 + 이 지점으로 복원하기 + 활동 유형 + 날짜 범위 활동 유형에 따라 필터 + 이 앱으로부터 버전 복사하기 + 먼저 글 편집하기 복사하려고 한 글은 충돌하거나 최근 만들고 저장하지 않은 두 버전이 있습니다.\n충돌을 해결하거나 이 앱으로부터 버전 복사를 진행하려면 먼저 글을 편집하세요. + 글 동기화 충돌 + 복제하기 이야기를 저장하는 중입니다. 기다려 주시기 바랍니다… - 파일 선택하기 - 파일 URL 복사하기 - 파일 편집하기 - 파일 저장을 실패했습니다.\n옵션을 열려면 누르시기 바랍니다. - 파일 업로드를 실패했습니다.\n옵션을 열려면 누르시기 바랍니다. - 파일 블록 설정 파일 이름 - 글에 대한 구독상태를 가져오는데 오류가 있습니다 - 이 글에 대한 댓글을 구독할 수 없습니다 - 이 글에 대한 댓글을 구독 해제할 수 없습니다 - 젯팩 - 도메인 선택하기 - 이메일로 대화 팔로우 - 이메일로 대화 팔로우 중 - 젯팩 설정 - 적용하기 - 지우기 - 받은 응답이 없습니다 + 파일 블록 설정 + 파일 업로드를 실패했습니다.\n옵션을 열려면 누르시기 바랍니다. + 파일 저장을 실패했습니다.\n옵션을 열려면 누르시기 바랍니다. + 파일 편집하기 + 파일 URL 복사하기 + 파일 선택하기 + 도메인 선택하기 + 젯팩 설정 + 젯팩 + 이메일로 대화 팔로우 중 + 이메일로 대화 팔로우 + 이 글에 대한 댓글을 구독 해제할 수 없습니다 + 이 글에 대한 댓글을 구독할 수 없습니다 + 글에 대한 구독상태를 가져오는데 오류가 있습니다 유효하지 않은 반응을 받았습니다 + 받은 응답이 없습니다 + 지우기 + 적용하기 하나 또는 그 이상의 슬라이드는 지금 이야기가 GIF 파일을 지원하지 않기에 이야기에 추가할 수 없습니다. 정지한 이미지 또는 비디오 배경을 대신 선택하시기 바랍니다. - 이 이야기는 다른 장치에서 편집하였고 특정 객체를 편집하는 기능을 제한할 수도 있습니다. - 이야기를 편집할 수 없습니다 - 이 이야기에 대한 미디어를 로드할 수 없습니다. 인터넷 연결을 확인하고 잠시 후 다시 시도하세요. - 이야기를 편집할 수 없습니다 - 사이트의 이 이야기에 대한 미디어를 찾을 수 없습니다. GIF 파일을 지원하지 않습니다 + 사이트의 이 이야기에 대한 미디어를 찾을 수 없습니다. + 이야기를 편집할 수 없습니다 + 이 이야기에 대한 미디어를 로드할 수 없습니다. 인터넷 연결을 확인하고 잠시 후 다시 시도하세요. + 이야기를 편집할 수 없습니다 + 이 이야기는 다른 장치에서 편집하였고 특정 객체를 편집하는 기능을 제한할 수도 있습니다. 제한적인 이야기 편집 중 미디어를 제거했습니다. 이야기 다시 만들기를 시도하세요. - 레이아웃은 오프라인에서 사용할 수 없습니다 - 온라인으로 돌아오면 재시도를 누르세요. - 인터넷 연결을 확인하고 재시도하시기 바랍니다. - 삭제하기 - 다음 - 완료 - 변경사항을 취소하시나요? - 아무 변경사항도 저장하지 않을 것입니다. - 폐기하기 - 본문 배경 + 본문 + 폐기하기 + 아무 변경사항도 저장하지 않을 것입니다. + 변경사항을 취소하시나요? + 완료 + 다음 + 삭제하기 테마를 선택하는 동안 오류가 발생했습니다. - 검색하기 - 환영합니다! - 최근 글이 없습니다 - 폭 넓은 검색을 위해 더 많은 토픽을 따르도록 해보세요 - 토픽 따르기 - 연결한 이메일 찾기 + 인터넷 연결을 확인하고 재시도하시기 바랍니다. + 온라인으로 돌아오면 재시도를 누르세요. + 레이아웃은 오프라인에서 사용할 수 없습니다 상점 자격증명으로 계속하기 - <b>메디슨 루이즈</b>가 글을 좋아합니다 - 오늘 사이트에서 <b>50개의 좋아요</b>를 받았습니다 + 연결한 이메일 찾기 + 토픽 따르기 + 폭 넓은 검색을 위해 더 많은 토픽을 따르도록 해보세요 + 최근 글이 없습니다 + 환영합니다! + 검색하기 <b>요한 브란트</b>가 글에 답했습니다 - 선택하기 - 스크롤 할 수 있는 블록 메뉴를 닫았습니다. + 오늘 사이트에서 <b>50개의 좋아요</b>를 받았습니다 + <b>메디슨 루이즈</b>가 글을 좋아합니다 스크롤 할 수 있는 블록 메뉴가 열렸습니다. 블록을 선택하세요. - 카테고리 - 설정하지 않았습니다 - 카테고리 - 새 카테고리 추가하기 - 카테고리 추가하기 - 오류가 있어 레이아웃을 사용할 수 없습니다 - 재시도를 누르거나 아래의 버튼을 이용하여 빈 페이지를 만드세요. - 레이아웃은 오프라인 중에 사용할 수 없습니다 + 스크롤 할 수 있는 블록 메뉴를 닫았습니다. + 선택하기 온라인으로 돌아가면 재시도를 누르거나 아래의 단추를 이용하여 빈 페이지를 만드세요. - 사진가 카메론 카르스텐의 작업에서 깊은 영감을 받았습니다. 다음 번에 이 기법을 시도할 것입니다 - 파멜라 응우옌 - 웹 뉴스 - 주간 락앤롤 - 예술 - 요리 - 축구 + 레이아웃은 오프라인 중에 사용할 수 없습니다 + 재시도를 누르거나 아래의 버튼을 이용하여 빈 페이지를 만드세요. + 오류가 있어 레이아웃을 사용할 수 없습니다 + 카테고리 추가하기 + 새 카테고리 추가하기 + 카테고리 + 설정하지 않았습니다 + 카테고리 + 런던의 박물관 + 세계 최고의 팬 + 상위 10개 카페 + 정치 음악 원예 - 정치 - 상위 10개 카페 - 세계 최고의 팬 - 런던의 박물관 - 세계적으로 가장 잘 알려진 웹사이트 제작기에 오신 것을 환영합니다. - 강력한 편집기로 이동 중에도 글을 쓸 수 있습니다. - 댓글과 알림을 실시간으로 보세요. - 깊이 있는 분석으로 독자가 늘어나는 것을 확인하세요. + 축구 + 요리 + 예술 + 주간 락앤롤 + 웹 뉴스 + 파멜라 응우옌 + 사진가 카메론 카르스텐의 작업에서 깊은 영감을 받았습니다. 다음 번에 이 기법을 시도할 것입니다 영감받기 좋아하는 사이트를 팔로우하고 새 블로그를 검색하세요. - 팔로우한 사이트 + 깊이 있는 분석으로 독자가 늘어나는 것을 확인하세요. + 댓글과 알림을 실시간으로 보세요. + 강력한 편집기로 이동 중에도 글을 쓸 수 있습니다. + 세계적으로 가장 잘 알려진 웹사이트 제작기에 오신 것을 환영합니다. 미디어 로딩을 실패했습니다 + 팔로우한 사이트 매 릴리즈마다 더 많은 블록을 추가하려고 열심히 일하고 있습니다. \'%s\'은(는) 완전히 지원되지 않음 - 사이트에 새 블로그 글로 발행되기에 독자들이 하나도 놓치지 않습니다. - 스토리 글 만들기 - 이미지 고르기 - 웹 편집기를 이용하여 편집하기 도움 버튼 - 사진, 비디오, 그리고 문제를 혼합하여 방문자가 좋아하여 매력적이고 탭할 수 이는 이야기 글을 만드세요. + 웹 편집기를 이용하여 편집하기 + 이미지 고르기 + 스토리 글 만들기 + 사이트에 새 블로그 글로 발행되기에 독자들이 하나도 놓치지 않습니다. 스토리 글이 보이지 않습니다 - %1$s의 사진 접근이 거부되었습니다. 이를 고치려면, 권한을 고치고 %2$s와(과) %3$s을(를) 켜세요. - 페이지를 만들었습니다 - 빈 페이지를 만들었습니다 - 이야기 글 소개 - 이야기 글을 만드는 방법 - 이야기 제목의 예제 + 사진, 비디오, 그리고 문제를 혼합하여 방문자가 좋아하여 매력적이고 탭할 수 이는 이야기 글을 만드세요. 이제 모두를 위한 이야기가 있습니다 - 워드프레스 미디어 라이브러리에서 선택하기 - 미디어 삽입을 실패했습니다: %s + 이야기 제목의 예제 + 이야기 글을 만드는 방법 + 이야기 글 소개 + 빈 페이지를 만들었습니다 + 페이지를 만들었습니다 미디어 삽입을 실패했습니다. + 미디어 삽입을 실패했습니다: %s + 워드프레스 미디어 라이브러리에서 선택하기 돌아가기 - 작성자 시작하기 새 블로그를 발견할 수 있는 토픽을 팔로우하세요 - 미디어 업로드 중 - 스톡 미디어 업로드 중 - gif 미디어 업로드 중 - 웹사이트 열기 - 스팸으로 표시하기 - 스팸 표시 해제하기 + 작성자 이 리퍼러는 스팸으로 표시할 수 없습니다. + 스팸 표시 해제하기 + 스팸으로 표시하기 + 웹사이트 열기 + gif 미디어 업로드 중 + 스톡 미디어 업로드 중 + 미디어 업로드 중 검색하거나 URL 입력하기 이 전화 링크 추가하기 - 이 이메일 링크 추가하기 이 링크 추가하기 + 이 이메일 링크 추가하기 인터넷에 연결되지 않았습니다.\n제안을 사용할 수 없습니다. - %s 선택했습니다 - %s - 비디오를 기록하려면 앱 오디오 기록 권한을 승인해야 합니다 - 캐쥬얼 - 클래식 - 스트롱 - 발랄 - 모던 굵게 - 항목 탐색하기 - 이 댓글을 볼 수 없습니다 - 마이크 - 음, 이 이메일 주소와 연결된 워드프레스닷컴 계정을 찾을 수 없습니다. + 모던 + 발랄 + 스트롱 + 클래식 + 캐쥬얼 + 비디오를 기록하려면 앱 오디오 기록 권한을 승인해야 합니다 + %s + %s 선택했습니다 이메일로 로그인 링크 얻기 + 음, 이 이메일 주소와 연결된 워드프레스닷컴 계정을 찾을 수 없습니다. + 마이크 + 이 댓글을 볼 수 없습니다 + 항목 탐색하기 이 글 신고하기 - %1$s 추가 항목 - 동작을 허용하지 않습니다 - 내부 서버 오류가 생겼습니다 리더에 오신 것을 환영합니다. 손끝에서 백만의 블로그를 발견하세요. + 내부 서버 오류가 생겼습니다 + 동작을 허용하지 않습니다 + %1$s 추가 항목 레이아웃 선택하기 안내: 컬럼 레이아웃은 테마와 화면 크기에 따라 달라질 수도 있습니다 + 글 또는 스토리 만들기 페이지 만들기 글 만들기 - 글 또는 스토리 만들기 - 숨기기 좋아할 수도 있습니다 - 저장소 보기 - 저장소 슬라이드를 찾을 수 없습니다 - 작업을 진행하고 있습니다. 나중에 다시 시도하세요 - 이미지를 저장하는 중에 오류가 있습니다 - 비디오를 저장할 수 없습니다 - 이 장치는 카메라2 API를 지원하지 않습니다. - 비디오를 재생하는 중에 오류가 생겼습니다 - 페이지 제목입니다. 비었습니다 - 페이지 제목입니다. %s - 이후에 블록 붙여넣기 - 제목을 업데이트합니다. + 숨기기 비디오 캡션입니다. 비었습니다 - 1개의 슬라이드에 조치가 필요합니다 - %1$d개의 슬라이드에 조치가 필요합니다 - 관리하기 - 1개의 슬라이드를 저장할 수 없습니다 - %1$d개의 슬라이드를 저장할 수 없습니다 - 슬라이드를 저장하거나 지우기를 다시 시도한 뒤에, 스토리 발행을 다시 시도하세요. - 장치 저장소가 충분하지 않습니다 + 제목을 업데이트합니다. + 이후에 블록 붙여넣기 + 페이지 제목입니다. %s + 페이지 제목입니다. 비었습니다 + 비디오를 재생하는 중에 오류가 생겼습니다 + 이 장치는 카메라2 API를 지원하지 않습니다. + 비디오를 저장할 수 없습니다 + 이미지를 저장하는 중에 오류가 있습니다 + 작업을 진행하고 있습니다. 나중에 다시 시도하세요 + 저장소 슬라이드를 찾을 수 없습니다 + 저장소 보기 발행하기 전에 장치에 스토리를 저장해야 합니다. 저장소 설정을 검토하고 공간을 확보하려면 파일을 제거하세요. - “%1$s”(을)를 업로드 중… - “%1$s”(을)를 발행했습니다 - “%1$s”(을)를 업로드할 수 없습니다 + 장치 저장소가 충분하지 않습니다 + 슬라이드를 저장하거나 지우기를 다시 시도한 뒤에, 스토리 발행을 다시 시도하세요. + %1$d개의 슬라이드를 저장할 수 없습니다 + 1개의 슬라이드를 저장할 수 없습니다 + 관리하기 + %1$d개의 슬라이드에 조치가 필요합니다 + 1개의 슬라이드에 조치가 필요합니다 “%1$s”(을)를 업로드할 수 없습니다 - “%1$s”(을)를 저장하는 중… - 몇 개의 스토리 - 1개의 슬라이드가 남았습니다 + “%1$s”(을)를 업로드할 수 없습니다 + “%1$s”(을)를 발행했습니다 + “%1$s”(을)를 업로드 중… %1$d개의 슬라이드가 남았습니다 - 선택하지 않았습니다 - 선택하였습니다 - 오류가 있습니다 - 본문 정렬 변경하기 - 본문 색상 변경하기 - 스토리 슬라이드를 지우시겠어요? - 이 슬라이드는 스토리에서 지워질 것입니다. - 이 슬라이드는 아직 저장하지 않았습니다. 이 슬라이드를 지웠다면, 편집한 내용을 읽게 될 것입니다. - 지우기 - 스토리 글을 폐기하시겠어요? - 스토리 글을 임시글로 저장하지 않을 것입니다. - 폐기하였습니다 + 1개의 슬라이드가 남았습니다 + 몇 개의 스토리 + “%1$s”(을)를 저장하는 중… 제목이 없습니다 - %1$s 만들기를 %2$s탭하세요. 다음 <b>블로그 글</b>을 선택하세요 - 글, 페이지 또는 스토리를 만들기 - 글 또는 스토리 만들기 - 스토리에 제목을 부여하기 - 레이아웃을 선택하기 - 넓고 다양한 이전에 만들어진 페이지 레이아웃에서 선택하여 시작하세요. 또는 빈 페이지로 시작하세요. - 빈 페이지 만들기 - 페이지 만들기 - 미리보기 - 갈무리하기 - 카메라 뒤집기 - 플래시 - 스티커 - 문자 - 소리 - 뒤집기 - 플래시 - 저장하는 중 - 저장했습니다 - 다시 시도하기 - 사진에 저장했습니다 - 공유하기 - 공유할 대상 - 닫기 - 저장했습니다 - 다시 시도하기 + 폐기하였습니다 + 스토리 글을 임시글로 저장하지 않을 것입니다. + 스토리 글을 폐기하시겠어요? + 지우기 + 이 슬라이드는 아직 저장하지 않았습니다. 이 슬라이드를 지웠다면, 편집한 내용을 읽게 될 것입니다. + 이 슬라이드는 스토리에서 지워질 것입니다. + 스토리 슬라이드를 지우시겠어요? + 본문 색상 변경하기 + 본문 정렬 변경하기 + 오류가 있습니다 + 선택하였습니다 + 선택하지 않았습니다 슬라이드 - 저장소 한도를 초과했습니다 - 파일을 업로드할 수 없습니다.\n저장소 한도를 초과했습니다. - 연결한 페이지 점프를 찾을 수 없습니다 - 자체 호스팅 워드프레스 사이트의 사이트 아이콘 편집은 잿펙 플러그인이 필요합니다. - 스토리 글 + 다시 시도하기 + 저장했습니다 + 닫기 + 공유할 대상 + 공유하기 + 사진에 저장했습니다 + 다시 시도하기 + 저장했습니다 + 저장하는 중 + 플래시 + 뒤집기 + 소리 + 문자 + 스티커 + 플래시 + 카메라 뒤집기 + 갈무리하기 + 미리보기 + 페이지 만들기 + 빈 페이지 만들기 + 넓고 다양한 이전에 만들어진 페이지 레이아웃에서 선택하여 시작하세요. 또는 빈 페이지로 시작하세요. + 레이아웃을 선택하기 + 스토리에 제목을 부여하기 + 글 또는 스토리 만들기 + 글, 페이지 또는 스토리를 만들기 + %1$s 만들기를 %2$s탭하세요. 다음 <b>블로그 글</b>을 선택하세요 장치에서 선택하기 + 스토리 글 + 자체 호스팅 워드프레스 사이트의 사이트 아이콘 편집은 잿펙 플러그인이 필요합니다. + 연결한 페이지 점프를 찾을 수 없습니다 + 파일을 업로드할 수 없습니다.\n저장소 한도를 초과했습니다. + 저장소 한도를 초과했습니다 파일 추가하기 - 이미지나 비디오를 대체하기 비디오 대체하기 - 워드프레스닷컴 계정이 없는 상태에서 Google로 계속하기를 선택하는 경우, 계정을 만들고 %1$s서비스 약관%2$s에 동의하는 것으로 간주됩니다. - 등록 확인 - 블록을 제거했습니다 - 이미지 고르기 - 이미지나 비디오 고르기 + 이미지나 비디오를 대체하기 + 링크로 변환 비디오 고르기 + 이미지나 비디오 고르기 + 이미지 고르기 + 블록을 제거했습니다 이미 있는 사이트 주소 입력하기 - 링크로 변환 + 등록 확인 + 워드프레스닷컴 계정이 없는 상태에서 Google로 계속하기를 선택하는 경우, 계정을 만들고 %1$s서비스 약관%2$s에 동의하는 것으로 간주됩니다. 계속하면 %1$s서비스 약관%2$s에 동의하는 것입니다. 이 이메일 주소를 사용하여 새 워드프레스닷컴 계정을 만들 것입니다. 워드프레스닷컴 계정을 만드는 등록 링크를 이메일 하였습니다. 이 장치에서 이메일을 확인하고, 워드프레스닷컴에서 받은 이메일에 있는 링크를 누르세요. %1$s에 대한 계정 정보를 입력합니다. - 이 장치에서 이메일을 확인하여 워드프레스닷컴에서 받은 이메일의 링크를 누르세요. - 이메일이 보이지 않으세요? 스팸이나 정크 메일 폴더를 확인하세요. - 완료 - 사이트 주소 찾기 - Google로 계속하기 또는 + Google로 계속하기 + 사이트 주소 찾기 + 완료 + 이메일이 보이지 않으세요? 스팸이나 정크 메일 폴더를 확인하세요. + 이 장치에서 이메일을 확인하여 워드프레스닷컴에서 받은 이메일의 링크를 누르세요. 비밀번호가 필요하지 않고 바로 로그인할 수 있는 링크를 이메일 할 것입니다. - 비밀번호 재설정하기 - 이메일로 링크 보내기 - 계정 만들기 - 또는 비밀번호 입력하기 - 이메일 주소를 입력하여 로그인하거나 워드프레스닷컴 계정을 만드세요. - 시작하기 이메일 확인하기 + 시작하기 + 이메일 주소를 입력하여 로그인하거나 워드프레스닷컴 계정을 만드세요. + 또는 비밀번호 입력하기 + 계정 만들기 + 이메일로 링크 보내기 + 비밀번호 재설정하기 요청을 처리하는 중에 문제가 있었습니다. 나중에 다시 시도하시기 바랍니다. - <b>%1$s</b>(을)를 눌러 새 제목 설정하기 사이트 제목 확인 + <b>%1$s</b>(을)를 눌러 새 제목 설정하기 이 글을 휴지통에 버리면 로컬 변경사항도 취소될 것입니다. 계속하기 원하시는게 맞나요? %s 블록 옵션 - 사이트 제목은 사용자가 관리자 역할일 때만 바꿀 수 있습니다. - 블록을 잘라내었습니다 - 블록을 복제하였습니다 - 블록을 복사하였습니다 - 블록을 붙여넣었습니다 - 복사된 블록 - 블록 복사하기 - 블록 복제하기 블록 제거 - 저장하지 않은 변경사항 - 사이트 제목을 업데이트할 수 없습니다. 네트워크 연결을 확인하고 다시 시도하세요. - 주제 + 블록 복제하기 + 블록 복사하기 + 복사된 블록 + 블록을 붙여넣었습니다 + 블록을 복제하였습니다 + 블록을 잘라내었습니다 + 블록을 복사하였습니다 + 사이트 제목은 사용자가 관리자 역할일 때만 바꿀 수 있습니다. 사이트 제목은 웹브라우저의 제목표시줄에 보이고 대부분의 테마에서 헤더에 보입니다. - 이전 내용 시트로 탐색하기 + 주제 + 사이트 제목을 업데이트할 수 없습니다. 네트워크 연결을 확인하고 다시 시도하세요. + 저장하지 않은 변경사항 브라우저에서 링크 열기 - 그라디언트 사용자 정의하기 - 옵션을 선택하려면 두 번 탭하기 - 뒤로 가기 - 그라디언트 유형 - 사용자 정의 색상 선택기로 탐색하기 + 이전 내용 시트로 탐색하기 사용자 정의 그라디언트로 탐색하기 - - 모두 - 콘텐츠 구조 + 사용자 정의 색상 선택기로 탐색하기 + 그라디언트 유형 + 뒤로 가기 + 옵션을 선택하려면 두 번 탭하기 + 그라디언트 사용자 정의하기 페이지 작성자 미디어 썸네일을 불러 올 수 없습니다 - 설정하지 않음 + 콘텐츠 구조 + 모두 + 해제 - 휴지통에 있는 글은 편집할 수 없습니다. 이 글의 상태를 “초안”으로 변경하여 작업할 수 있기를 원하십니까? - 취소 - 발행일 - 태그 - 다음으로 발행 - 지금 예약하기 - 지금 제출하기 - 지금 저장하기 - 뒤로 - 태그 추가하기 + 설정하지 않음 태그는 독자들에게 이 글이 무엇에 관한 글인지 말하도록 돕습니다. 발행일 + 태그 추가하기 + 뒤로 + 지금 저장하기 + 지금 제출하기 + 지금 예약하기 + 다음으로 발행 + 태그 + 발행일 + 취소 초안으로 이동하기 - 캘리포니아 소비자 개인정보 보호법(“CCPA”)이 수집하고 공유하는 개인 정보의 범주에 대한 개인 정보를 어떤 것을 얻고, 어떻게 왜 쓰는 지에 대한 일부 추가 정보를 캘리포니아 거주자에게 제공할 것을 요구합니다. - CCPA 개인정보보호 고지 읽기 - 발행일 - 예약됨 - 버려짐 - 발행됨 - 계속하기 위해 몇 가지 더 선택하기 - 완료 + 휴지통에 있는 글은 편집할 수 없습니다. 이 글의 상태를 “초안”으로 변경하여 작업할 수 있기를 원하십니까? 글을 초안으로 옮깁니까? - 주제 선택 주제 선택 - 지금 업데이트하기 - 상태 & 가시성 + 주제 선택 + 완료 + 계속하기 위해 몇 가지 더 선택하기 + 발행됨 + 버려짐 + 예약됨 + 발행일 + CCPA 개인정보보호 고지 읽기 + 캘리포니아 소비자 개인정보 보호법(“CCPA”)이 수집하고 공유하는 개인 정보의 범주에 대한 개인 정보를 어떤 것을 얻고, 어떻게 왜 쓰는 지에 대한 일부 추가 정보를 캘리포니아 거주자에게 제공할 것을 요구합니다. 캘리포니아 사용자를 위한 개인정보보호 고지 + 상태 & 가시성 + 지금 업데이트하기 블록 동작 메뉴 열기 위로 이동하기 - 두 번 눌러 사용 가능한 선택지가 있는 동작 시트를 열기 - 두 번 눌러 사용 가능한 선택지가 있는 하단 시트 열기 언급 넣기 - 선택된 홈페이지와 글에 대한 페이지는 같을 수 없습니다. - 고전 블로그 - 글 페이지 - 페이지 선택 - 정적 홈페이지 - 홈페이지로 설정 - 글 페이지로 설정 + 두 번 눌러 사용 가능한 선택지가 있는 하단 시트 열기 + 두 번 눌러 사용 가능한 선택지가 있는 동작 시트를 열기 지금은 페이지를 열 수 없습니다. 나중에 다시 시도하시기 바랍니다 + 글 페이지로 설정 + 홈페이지로 설정 %1$s은(는) 유효한 %2$s이(가) 아님 - 홈페이지 설정 - 최근 글(고전 블로그) 또는 고정/정적 페이지를 보여주는 홈페이지에서 선택하기 - 페이지의 부르기를 실패하였습니다 - 홈페이지 설정을 저장하지 못했습니다 - 페이지를 부르기 전에 홈페이지 설정을 저장할 수 없습니다 + 페이지 선택 + 글 페이지 + 정적 홈페이지 + 고전 블로그 + 선택된 홈페이지와 글에 대한 페이지는 같을 수 없습니다. 홈페이지 설정 업데이트가 실패하면, 인터넷 연결을 확인하십시오 + 페이지를 부르기 전에 홈페이지 설정을 저장할 수 없습니다 + 홈페이지 설정을 저장하지 못했습니다 승인 - 사이트 설정에서 홈페이지를 “정적 홈페이지” 활성화로 설정하기 - 사이트 설정에서 글 페이지를 “정적 홈페이지” 활성화로 설정하기 - 홈페이지를 성공적으로 업데이트하였습니다 - 홈페이지 업데이트가 실패했습니다 - 글 페이지를 성공적으로 업데이트하였습니다 - 홈페이지 업데이트가 실패했습니다 + 페이지의 부르기를 실패하였습니다 + 최근 글(고전 블로그) 또는 고정/정적 페이지를 보여주는 홈페이지에서 선택하기 + 홈페이지 설정 홈페이지 + 홈페이지 업데이트가 실패했습니다 + 글 페이지를 성공적으로 업데이트하였습니다 + 홈페이지 업데이트가 실패했습니다 + 홈페이지를 성공적으로 업데이트하였습니다 + 사이트 설정에서 글 페이지를 “정적 홈페이지” 활성화로 설정하기 + 사이트 설정에서 홈페이지를 “정적 홈페이지” 활성화로 설정하기 색상 선택 색상 설정으로 이동하려면 두 번 탭하세요. 사이트를 팔로우하면 여기에 해당 콘텐츠가 표시됩니다. - 사이트를 선택할 수 없습니다. 다시 시도해 보시기 바랍니다. - 비디오 선택 - 미디어 선택 - 이 비디오 사용하기 - 이 미디어 사용하기 - 이미지 썸네일 미리보기 - 자르기 - %d 삽입 더 알아보기 - 파일을 로드하지 못했습니다. 다시 시도하십시오. %s의 새로운 기능 - 복사 - 계속 - 삽입 - 링크 주소 복사 - 링크 주소가 복사되었습니다 - 새로운 기능 - 이용 가능한 WordPress.com이 없습니다 - WordPress.com 사이트를 만들면, 다른 사이트의 좋아하는 내용을 리블로그할 수 있습니다. - 사이트 관리 - 리블로그에 실패하였습니다 + %d 삽입 + 자르기 + 파일을 로드하지 못했습니다. 다시 시도하십시오. + 이미지 썸네일 미리보기 + 이 미디어 사용하기 + 이 비디오 사용하기 + 미디어 선택 + 비디오 선택 + 사이트를 선택할 수 없습니다. 다시 시도해 보시기 바랍니다. 계속 + 리블로그에 실패하였습니다 + 사이트 관리 + WordPress.com 사이트를 만들면, 다른 사이트의 좋아하는 내용을 리블로그할 수 있습니다. + 이용 가능한 WordPress.com이 없습니다 + 새로운 기능 + 링크 주소가 복사되었습니다 + 링크 주소 복사 공유 경로 공유 실패 + 삽입 + 계속 + 복사 열 수 - 블록을 왼쪽으로 움직이려면 두 번 탭 - 블록을 오른쪽으로 움직이려면 두 번 탭 - 블록 왼쪽으로 움직이기 - 블록을 %1$s 위치에서 %2$s (으)로 이동 - 블록 오른쪽으로 움직이기 블록을 %1$s 위치에서 %2$s(으)로 이동 - 씬난다!\n거의 다 됐습니다 - 사이트가 금새 준비될 것입니다 - 사이트 URL 확보 - 사이트 기능 추가 중 - 테마를 설정 중 - 알림판을 만드는 중 + 블록 오른쪽으로 움직이기 + 블록을 %1$s 위치에서 %2$s (으)로 이동 + 블록 왼쪽으로 움직이기 + 블록을 오른쪽으로 움직이려면 두 번 탭 + 블록을 왼쪽으로 움직이려면 두 번 탭 블록 설정 - 요청을 다루는데 문제가 있습니다 + 알림판을 만드는 중 + 테마를 설정 중 + 사이트 기능 추가 중 + 사이트 URL 확보 + 사이트가 금새 준비될 것입니다 + 씬난다!\n거의 다 됐습니다 업로드 취소 - 일요일 - 월요일 - 화요일 - 수요일 - 목요일 - 금요일 - 토요일 - 테너에서 선택 + 요청을 다루는데 문제가 있습니다 테너 제공 - 비공개 사이트의 내용에 접근 중 + 테너에서 선택 + 토요일 + 금요일 + 목요일 + 수요일 + 화요일 + 월요일 + 일요일 비공개 사이트의 내용에 접근이 실패하였습니다. 일부 미디어가 이용 가능하지 않을 수 있습니다. + 비공개 사이트의 내용에 접근 중 이미지를 잘라서 저장하는데 실패하였습니다, 다시 시도해보시기 바랍니다. + 이미지를 로드하지 못했습니다.\n눌러서 다시 시도하세요. 이미지 미리보기 알 수 없는 페이지 형식 + 이 동작을 완료할 수 없고, 검토를 위해 이 페이지를 제출하지 않았습니다. 이 동작을 완료할 수 없고, 이 페이지를 예약하지 않았습니다. 이 동작을 완료할 수 없고, 이 비공개 페이지를 발행하지 않았습니다. - 이 동작을 완료할 수 없고, 검토를 위해 이 페이지를 제출하지 않았습니다. - 이미지를 로드하지 못했습니다.\n눌러서 다시 시도하세요. 이 동작을 완료할 수 없고, 이 페이지를 발행하지 않았습니다. 검토를 위해 이 페이지를 제출할 수 없으나, 나중에 다시 시도할 것입니다. 이 페이지를 예약할 수 없으나, 나중에 다시 시도할 것입니다. @@ -1489,8 +1565,8 @@ Language: ko_KR 삭제된 예약됨 게시됨 - 연결되지 않음 페이스북 연결에서 페이지를 찾을 수 없습니다. 젯팩 소셜은 페이스북 프로필에 연결할 수 없으며 발행한 페이지에만 연결할 수 있습니다. + 연결되지 않음 좋아요 팔로우 댓글 @@ -1506,19 +1582,19 @@ Language: ko_KR 태그 또는 사이트, 팝업 창을 선택합니다. 사이트 또는 태그를 선택하여 글을 필터링합니다. 현재 필터 제거 - 워드프레스닷컴에 로그인 토픽과 사이트 관리하기 - 워드프레스닷컴에 로그인하여 내가 팔로우하는 사이트의 최신 글을 확인하세요. + 워드프레스닷컴에 로그인 워드프레스닷컴에 로그인하여 팔로우하는 토픽의 최신 글을 확인하세요 + 워드프레스닷컴에 로그인하여 내가 팔로우하는 사이트의 최신 글을 확인하세요. 현재 블록 대체하기 끝에 추가하기 처음에 추가하기 이전에 블록 추가 다음에 블록 추가 - 사이트 팔로우 - 팔로우하고 있는 사이트의 최신 글을 보십시오 토픽 추가하기 + 사이트 팔로우 토픽을 추가하여 특정 주제의 글을 팔로우할 수 있습니다 + 팔로우하고 있는 사이트의 최신 글을 보십시오 팔로우 중 필터 비디오 캡션. %s @@ -1563,22 +1639,23 @@ Language: ko_KR 사이트에서 <b>XMLRPC 파일</b>에 액세스할 수 없습니다. 이 문제를 해결하려면 호스트에게 연락해야 합니다. 마지막 부분입니다! 젯팩에 연결된 이메일 주소 <b>%1$s</b>만 확인하면 됩니다. %1$s 사이트 자격 증명으로 로그인 - 팔로우 중 사이트 페이지 - 지금 이 글을 열 수 없습니다. 나중에 다시 시도해 주세요. - %sk - %sB - %sM + 팔로우 중 + 좋아요 + 검색 + 저장됨 + 토픽 + 사이트 + %sQi %sQa %sT - %sQi - 사이트 - 저장됨 - 검색 - 좋아요 + %sB + %sM + %sk + 지금 이 글을 열 수 없습니다. 나중에 다시 시도해 주세요. 사이트의 데이터를 현재 로드할 수 없습니다. 나중에 다시 시도해 주세요. - 토픽 워드프레스 미디어 라이브러리 + 지원되지 않음 그룹 해제 키보드를 숨기려면 탭 도움말을 보려면 여기를 탭 @@ -1586,7 +1663,6 @@ Language: ko_KR 사진 찍기 또는 비디오 촬영 사진 촬영 글쓰기 시작… - 지원되지 않음 %s 블록. 이 블록에는 잘못된 내용이 있습니다. %s 블록. 비어 있음 블록 잘라내기 @@ -1603,12 +1679,12 @@ Language: ko_KR 블록을 위로 이동 블록을 아래로(%1$s 행에서 %2$s 행으로) 이동 블록을 아래로 이동 + 링크 텍스트 링크 삽입됨 이미지 캡션. %s 키보드 숨기기 도움말 아이콘 마지막 변경을 취소하려면 두 번 탭하세요. - 링크 텍스트 설정을 전환하려면 두 번 탭하세요. 이미지를 선택하려면 두 번 탭하세요. 비디오를 선택하려면 두 번 탭하세요. @@ -1625,10 +1701,11 @@ Language: ko_KR 대체 텍스트 비디오 추가 URL 추가 + 대체 텍스트 추가 이미지 또는 비디오 추가 이미지 추가 블록을 여기에 추가 - 대체 텍스트 추가 + 설명 추가 “글을 목록에 저장하려면 글 저장 버튼을 탭합니다.” \"목록에 %1$d개 항목이 로드되었습니다. \" 알림 @@ -1666,11 +1743,11 @@ Language: ko_KR 이 글에 저장되지 않은 변경 사항이 있습니다. 이 앱의 버전 다른 기기의 버전 + 이 앱에서\n%1$s\n에 저장됨\n다른 기기에서\n%2$s\n에 저장됨\n 최근에 이 글을 변경했지만 저장하지 않았습니다. 로드할 버전을 선택하세요.\n\n 어떤 버전을 편집하시겠어요? 영구적으로 삭제하기 임시글에 대한 최신 변경 사항은 저장되지 않습니다. - 이 앱에서\n%1$s\n에 저장됨\n다른 기기에서\n%2$s\n에 저장됨\n 이러한 변경은 예약되지 않습니다. 이러한 변경 사항은 리뷰를 위해 제출되지 않습니다. 이러한 변경 사항은 발행되지 않습니다. @@ -1716,9 +1793,9 @@ Language: ko_KR 공유 뒤로 가기 앞으로 가기 + %3$s 앱에서 \"%2$s\"에 \"%1$s\" 발행 예약됨 \n %4$s 예약된 워드프레스 글: \"%s\" \"%s\"이(가) 10분 후에 게시됩니다. - %3$s 앱에서 \"%2$s\"에 \"%1$s\" 발행 예약됨 \n %4$s \"%s\"이(가) 1시간 후에 게시됩니다. \"%s\"이(가) 게시되었습니다. 예약된 글: 10분 알림 @@ -1759,13 +1836,13 @@ Language: ko_KR Open link in a new window/tab 통계를 보려면 워드프레스닷컴 계정으로 로그인하세요. 검색어와 일치하는 글 없음 + 글 검색 사람들이 인터넷에서 회원님을 찾을 수 있는 곳입니다. 프리미엄 도메인 네임 선택 모든 워드프레스닷컴 요금제에는 사용자 정의 도메인 네임이 포함됩니다. 지금 무료 프리미엄 도메인을 등록하세요. 둘러보기 오늘 모든 기간 - 글 검색 이번 주 보기 위젯을 추가하려면 워드프레스 앱에 로그인하세요. 지원되는 네트워크 없음 @@ -1823,8 +1900,8 @@ Language: ko_KR 미디어를 삽입하지 못했습니다.\n눌러서 다시 시도하세요. 임시글을 업로드하고 있습니다. 임시글 업로드 중 - 글을 복원하는 중 오류 발생 임시글 + 글을 복원하는 중 오류 발생 소급 적용: %s 가장 관련성 높은 통계만 확인하세요. 아래 인사이트를 추가 및 구성하세요. 소셜 @@ -1834,7 +1911,6 @@ Language: ko_KR 추가 아이디어에 대한 키워드 입력 제안을 찾을 수 없음 도메인 등록 - 젯팩 설치되었으므로 설정해야 합니다. 이 작업은 1분 정도 소요됩니다. 인사이트에서 제거 아래로 이동 위로 이동 @@ -1845,13 +1921,13 @@ Language: ko_KR 글이 휴지통으로 이동 중 이 글을 휴지통에 버리면 로컬 변경 사항이 취소됩니다. 계속하시겠습니까? 로컬 변경 + 임시글로 이동하기 목록 보기로 전환 카드 보기로 전환 휴지통에 버린 글이 없습니다. 임시글이 없습니다. 예약한 글이 없습니다. 아직 글을 발행하지 않았습니다. - 임시글로 이동하기 사용자 이름과 비밀번호를 사용하여 로그인하세요. 이메일 주소가 아닌 워드프레스닷컴 사용자 이름을 사용하여 로그인하세요. 평균 단어 수/글 수 @@ -1869,6 +1945,7 @@ Language: ko_KR 도메인 등록 플러그인을 설치하려면 사이트와 연결된 사용자 정의 도메인이 있어야 합니다. 플러그인 설치 + 나중에 사이트의 모양과 느낌을 사용자 지정할 수 있습니다. 공개하기: %s 예약됨: %s 공개됨: %s @@ -1879,7 +1956,6 @@ Language: ko_KR 기간 월 및 연도 더 보기 - 나중에 사이트의 모양과 느낌을 사용자 지정할 수 있습니다. 오늘 최고의 시간 안녕히 계세요. @@ -1906,27 +1982,27 @@ Language: ko_KR 블록 편집기로 새 글과 페이지 편집 블록 편집기 사용 종료 - 다음 단계 - 방문자의 브라우저에 아이콘이 표시됩니다. 깔끔하고 전문적인 모양을 만들려면 사용자 정의 아이콘을 추가하세요. 방문자 늘리기 사이트를 사용자 정의 하기 + 다음 단계 고유한 사이트 아이콘 선택 + 방문자의 브라우저에 아이콘이 표시됩니다. 깔끔하고 전문적인 모양을 만들려면 사용자 정의 아이콘을 추가하세요. + 사이트의 성과를 참조하려면 %1$s 통계 %2$s을(를) 선택합니다. 새로 업로드하려면 %1$s사이트 아이콘%2$s을 탭합니다. 임시글을 작성하여 글을 게시하세요. - 사이트의 성과를 참조하려면 %1$s 통계 %2$s을(를) 선택합니다. 글 공유 설정 새 글을 소셜 미디어 계정에 자동으로 공유합니다. 사이트 통계 확인 + 사이트의 성과를 최신 상태로 유지하세요. 다음 단계를 제거하면 이 사이트의 모든 둘러보기가 숨겨집니다. 이 작업은 되돌릴 수 없습니다. 다음 단계 제거 제거 - 사이트의 성과를 최신 상태로 유지하세요. 작업 건너뛰기 알림 다음 기간 선택 이전 기간 선택 - 가장 인기 있는 시간 조회수 %1$s + 가장 인기 있는 시간 %1$s(%2$s) +%1$s(%2$s) 사이트 미리보기 표시 @@ -1956,12 +2032,14 @@ Language: ko_KR 글 업데이트 중 웹 취소 로컬 취소 + 로컬\n%1$s\n에 저장됨\n웹\n%2$s\n에 저장됨\n 이 글에 있는 두 가지 버전이 서로 충돌합니다. 취소할 버전을 선택하세요.\n\n 동기화 충돌 문제 해결 - 로컬\n%1$s\n에 저장됨\n웹\n%2$s\n에 저장됨\n 이 기간에 데이터 없음 미디어에서 위치 제거 지금은 통계를 열 수 없습니다. 나중에 다시 시도해 주세요. + 검색과 일치하는 미디어 없음 + GIF를 검색하여 미디어 라이브러리에 추가하세요! 조회수 글쓴이 글쓴이 @@ -1991,8 +2069,6 @@ Language: ko_KR 게시글 공유 게시글 작성 %2$s 발행 후 %1$s이(가) 지났습니다. 지금까지 게시글의 성과입니다 - 검색과 일치하는 미디어 없음 - GIF를 검색하여 미디어 라이브러리에 추가하세요! %2$s이(가) 발행된 후 %1$s일이 지났습니다. 발행한 후에는 글을 공유하여 조회수를 높이세요 태그 및 카테고리 모든-기간 @@ -2045,8 +2121,8 @@ Language: ko_KR 보통 썸네일 내역 - 검토 대기 중 선택한 페이지를 사용할 수 없습니다. + 검토 대기 중 삭제한 페이지가 없습니다. 예약한 페이지가 없습니다. 임시 페이지가 없습니다. @@ -2054,8 +2130,8 @@ Language: ko_KR 페이지 검색 검색어와 일치하는 페이지 없음 영구적으로 삭제하기 - 임시글로 이동하기 휴지통으로 이동하기 + 임시글로 이동하기 상위 항목 설정 보기 삭제된 @@ -2083,11 +2159,11 @@ Language: ko_KR 사이트 가동 및 작동 목록에서 항목을 지우는 것이 좋지 않나요? 사이트 보기 + 새 사이트를 미리 보며 방문자에게 표시되는 내용을 참조하세요. 사이트 공유 %1$s 공유 %2$s를 탭하여 계속하기 %1$s 연결 %2$s을 탭하여 소셜 미디어 계정 추가 소셜 미디어 계정을 연결하세요. 그러면 사이트에서 새 글을 자동으로 공유합니다. - 새 사이트를 미리 보며 방문자에게 표시되는 내용을 참조하세요. 글 게시 %1$s 게시물 작성 %2$s을 탭하여 새 게시물 작성하기 안 함 @@ -2095,31 +2171,22 @@ Language: ko_KR 이동 취소 지금 안 함 - 사이트가 없습니다 더 보기 + 사이트가 없습니다 팔로우하고 있는 토픽이 없습니다 즐겨 찾는 토픽에 대한 글을 찾으려면 여기에 토픽을 추가하세요 젯팩에 연결하는 데 사용한 워드프레스닷컴 계정으로 로그인하세요. - 다시 시도 - 문제가 발생했습니다 - 젯팩이 설치됨 - 젯팩을 설치하는 중 - 웹사이트 자격 증명은 저장되지 않으며 젯팩을 설치하는 용도로만 사용됩니다. - 젯팩 설치 젯팩 젯팩 자주 찾는 질문 - 설정 - 지금은 젯팩을 설치할 수 없습니다 - 사이트에 젯팩을 설치하는 중입니다. 완료하는 데 몇 분 정도 걸릴 수 있습니다. 워드프레스 사이트에서 통계를 사용하려면 젯팩 플러그인을 설치해야 합니다. 검색과 일치하는 테마 없음 무엇을 찾길 원하세요? 검색과 일치하는 태그 없음 태그가 없음 + 글에 태그를 지정할 때 빠르게 선택할 수 있도록 자주 사용하는 태그를 추가하세요. 태그 만들기 검색과 일치하는 미디어 없음 워드프레스에서 로그아웃하시겠습니까? - 글에 태그를 지정할 때 빠르게 선택할 수 있도록 자주 사용하는 태그를 추가하세요. 게시글에 대한 변경 사항이 아직 사이트에 업로드되지 않았습니다. 지금 로그아웃하면 장치에서 변경한 내용이 삭제됩니다. 그래도 로그아웃하시겠습니까? 아직 방문자 없음 아직 이메일 팔로워 없음 @@ -2157,9 +2224,9 @@ Language: ko_KR %1$s %2$s(으)로 되돌리는 중 현재 사이트 복원 중 사이트가 성공적으로 복원되었습니다. - 활동 로그 작업 버튼 사이트를 성공적으로 복원했습니다.\n%1$s %2$s(으)로 되돌렸습니다 사이트 복원 중\n%1$s %2$s(으)로 되돌리는 중 + 활동 로그 작업 버튼 자동 관리 이 글을 저장하고 원할 때마다 돌아와서 다시 읽으세요. 글은 이 기기에서만 읽을 수 있습니다. 저장된 글은 다른 기기와 동기화되지 않습니다. 나중에 보기 위해 글 저장 @@ -2236,8 +2303,8 @@ Language: ko_KR 팔로우한 사이트 기기에서 알림을 읽고 있는 사람 그래프와 차트를 보는 사람들 - 이 글을 영구적으로 삭제하시겠습니까? %2$s의 %1$s + 이 글을 영구적으로 삭제하시겠습니까? 중요 일반 이 사진 사용 @@ -2258,6 +2325,7 @@ Language: ko_KR 사진 삭제 비디오 재생 + 특성 비디오 재생 플러그인 로고 플러그인 배너 워드프레스 미디어에서 선택 @@ -2276,7 +2344,6 @@ Language: ko_KR %s의 프로필 사진 확인 표시 Google로 가입하는 중… - 특성 비디오 재생 젯팩에 연결 실패: %s 이미 젯팩에 연결되어 있습니다. 비주얼 모드 @@ -2312,15 +2379,15 @@ Language: ko_KR 알림 리더 - 알림 설정 내 사이트 + 알림 설정 고객님의 아바타가 업로드되었으며 곧 사용할 수 있습니다. 이 기능에 대한 필수 권한이 해제되어 있는 것 같습니다.<br/><br/>변경하려면 권한을 편집하고 <strong>%s</strong>이(가) 활성화되어 있는지 확인하세요. 권한 + 특성 공유 젯팩 모듈이 비활성화되었기 때문에 공유 설정에 액세스할 수 없습니다. 공유 모듈이 비활성화됨 %s 버전 - 특성 선택한 사운드의 경로가 잘못되었습니다. 다른 항목을 선택하세요. QP %s %1$d개의 페이지/글 남음 @@ -2374,13 +2441,13 @@ Language: ko_KR 닫기 이메일을 보내는 중에 문제가 발생했습니다. 지금 다시 시도하거나 닫고 나중에 다시 시도할 수 있습니다. 사용자 이름 + 방금 사용한 것과 같은 링크로 항상 로그인할 수 있지만, 원하는 경우 비밀번호를 설정할 수도 있습니다. 암호(옵션) 대화명 다시 시도 되돌리기 계정을 업데이트하는 중에 문제가 발생했습니다. 다시 시도하거나 변경 내용을 되돌려 계속할 수 있습니다. 아바타를 업데이트하는 중에 문제가 발생했습니다. - 방금 사용한 것과 같은 링크로 항상 로그인할 수 있지만, 원하는 경우 비밀번호를 설정할 수도 있습니다. 업데이트 필요 플러그인 검색 신규 @@ -2423,11 +2490,11 @@ Language: ko_KR 제작자: %s 사진 변경 플러그인을 로드할 수 없음 + 페이지 사이트의 태그 관리 저장 중 삭제 중 \'%s\'을(를) 영구 삭제하시겠습니까? - 페이지 이 이름의 태그가 이미 존재합니다. 새 태그 추가 설명 @@ -2546,8 +2613,8 @@ Language: ko_KR 파일 이름 URL 대체 텍스트 - 표시등이 깜박임 사이트 연결하기 + 표시등이 깜박임 기기 진동 사운드 선택 보기 및 사운드 @@ -2562,8 +2629,8 @@ Language: ko_KR 알림 활성화 알림 비활성화 - 최대 동영상 크기 + 최대 동영상 크기 최대 이미지 크기 다음 글에서 미디어를 업로드하는 동안 오류가 발생했습니다. %s 다음 글을 업로드하는 동안 오류가 발생했습니다. %s @@ -2603,8 +2670,8 @@ Language: ko_KR 연결하려는 워드프레스 사이트의 주소를 입력하세요. 워드프레스닷컴에 이미 로그인되어 있습니다. 계속 - 워드프레스닷컴 비밀번호를 입력합니다. 다른 사이트 연결하기 + 워드프레스닷컴 비밀번호를 입력합니다. 로그인 이메일 요청 중 이 비밀번호가 올바르지 않은 것 같습니다. 정보를 확인하고 다시 시도하세요. SMS를 통해 확인 코드를 요청 중입니다. @@ -2662,7 +2729,7 @@ Language: ko_KR 문서 이미지 모두 - %1$s의 사진 액세스가 거부되었습니다. 이를 해결하려면 권한을 편집하고 %2$s을(를) 켜세요. + %1$s의 미디어 파일 접근이 거부되었습니다. 이 문제를 해결하려면 권한을 편집하고 %2$s을(를) 켜세요. 댓글 보기 비디오 품질. 값이 클수록 비디오가 고품질이라는 것을 의미합니다. 글의 비디오를 이 크기로 조정 @@ -2810,61 +2877,61 @@ Language: ko_KR 연결 중… 처리 중… 조치가 수행되었습니다. + 댓글에 좋아요 표시를 함 로그아웃 WordPress.com에 로그인 - 댓글에 좋아요 표시를 함 워드프레스닷컴 상세 정보 더 보기: %s 기기 설정 열기 %s: 잘못된 이메일주소 %s : 사용자가 초대를 막았음 %s: 이미 팔로중임 - %s: 사용자가 없습니다. %s: 이미 회원임 + %s: 사용자가 없습니다. 댓글이 수락됨 좋아함. 지금 - 팔로워 독자 + 팔로워 인터넷에 연결되어 있지 않아 프로필을 저장할 수 없습니다. - 없음 - 왼쪽 오른쪽 + 왼쪽 + 없음 %1$d 선택 사이트 사용자를 가져올 수 없습니다. - 사용자를 가져오는 중… - 팔로워 이메일 팔로워 - 이메일 팔로워 + 팔로워 + 사용자를 가져오는 중… 방문자 + 이메일 팔로워 팔로워 최대 10개의 이메일 주소 및/또는 워드프레스닷컴 사용자명을 초대하세요. 사용자명이 필요한 사람들은 사용자명 생성 방법에 관한 지침을 받게 됩니다. 이 방문자를 제거하면 해당 방문자가 이 사이트에 방문할 수 없게 됩니다.\n\n이 방문자를 제거하시겠습니까? 이 팔로워가 제거된 경우 해당 팔로워는 다시 팔로우하지 않는 한 이 사이트에 대한 알림을 수신할 수 없게 됩니다.\n\n이 팔로워를 제거하시겠습니까? %1$s 이후 - 팔로워를 제거할 수 없습니다. 방문자를 제거할 수 없습니다. + 팔로워를 제거할 수 없습니다. 사이트 이메일 팔로워를 가져올 수 없습니다. 사이트 팔로워를 가져올 수 없습니다. 일부 미디어 업로드에 실패했습니다. 이 상태에서는 HTML 모드로 전환할 수\n 없습니다. 실패한 업로드를 모두 제거하고 계속할까요? - 비주얼 편집기 이미지 썸네일 - 변경사항 저장됨 - 캡션 - 대체 텍스트 - 링크 연결 대상: + 비주얼 편집기 가로 + 링크 연결 대상: + 대체 텍스트 + 캡션 + 변경사항 저장됨 저장되지 않은 변경 사항을 취소할까요? 업로드를 중지할까요? 미디어를 삽입하는 동안 오류가 발생했습니다. 미디어를 업로드 중입니다. 완료될 때까지 기다려 주세요. HTML 모드에서 직접 미디어를 삽입할 수 없습니다. 비주얼 모드로 전환하세요. 갤러리 업데이트 중… - 초대를 전송했으나 오류가 발생했습니다! - 초대를 전송했습니다. 눌러서 다시 시도하세요! + 초대를 전송했습니다. %1$s: %2$s + 초대를 전송했으나 오류가 발생했습니다! 초대를 전송하는 동안 오류가 발생했습니다! 전송하지 못한 항목: 유효하지 않은 사용자명 또는 이메일이 있습니다. 전송하지 못한 항목: 유효하지 않은 사용자명 또는 이메일입니다. @@ -2872,8 +2939,8 @@ Language: ko_KR 사용자 정의 메시지 초대 사용자명 또는 이메일 - 외부 사용자 초대 + 외부 검색 내역 지우기 검색 내역을 지울까요? 사용자 언어로 된 %s에 대한 글을 찾을 수 없습니다. @@ -2881,33 +2948,33 @@ Language: ko_KR 관련 글 미리보기 화면에서 링크가 비활성화되어 있습니다. 보내기 - %1$s을(를) 제거하면 해당 사용자는 이 사이트에 더 이상 액세스할 수 없지만 %1$s 님이 작성한 콘텐츠는 사이트에 남습니다.\n\n그래도 이 사용자를 제거하시겠습니까? \@%1$s이(가) 성공적으로 제거됨 + %1$s을(를) 제거하면 해당 사용자는 이 사이트에 더 이상 액세스할 수 없지만 %1$s 님이 작성한 콘텐츠는 사이트에 남습니다.\n\n그래도 이 사용자를 제거하시겠습니까? %1$s 제거 - 이 목록에 있는 사이트에서 최근 게시된 글이 없습니다. - 사람 역할 + 사람 + 이 목록에 있는 사이트에서 최근 게시된 글이 없습니다. 사용자를 제거할 수 없습니다. 사용자 역할을 업데이트할 수 없습니다. 사이트 사용자를 가져올 수 없습니다. Gravatar를 업데이트하는 중 오류가 발생했습니다. - 잘린 이미지를 찾는 중 오류가 발생했습니다. Gravatar를 다시 로드하는 중 오류가 발생했습니다. + 잘린 이미지를 찾는 중 오류가 발생했습니다. 이미지를 자르는 중 오류가 발생했습니다. 이메일 확인 중 현재 사용할 수 없습니다. 비밀번호를 입력하세요. 로그인 중 댓글을 달 때 공개적으로 표시합니다. 사진 캡처 또는 선택 - 글, 페이지, 설정을 %s로 발송할 것입니다. - 계획 계획 + 계획 + 글, 페이지, 설정을 %s로 발송할 것입니다. 콘텐츠 내보내기 - 콘텐츠를 내보내는 중… 전송된 이메일을 내보내세요! - 회원님의 사이트에서 프리미엄 업그레이드가 활성화되었습니다. 사이트를 삭제하기 전에 업그레이드를 취소하세요. - 구매 항목 표시 + 콘텐츠를 내보내는 중… 구매 항목 확인 중 + 구매 항목 표시 + 회원님의 사이트에서 프리미엄 업그레이드가 활성화되었습니다. 사이트를 삭제하기 전에 업그레이드를 취소하세요. 프리미엄 업그레이드 문제가 생겼습니다. 구매를 요청할 수 없습니다. 사이트를 삭제하는 중… @@ -2916,50 +2983,50 @@ Language: ko_KR 주 도메인 사이트를 삭제하는 동안 오류가 발생했습니다. 도움이 필요하면 사용자지원에 요청하세요. 사이트 삭제 오류 - 확인을 위해 아래 칸에 %1$s를 입력하세요. 사이트가 영원히 사라질 것입니다. 내용 내보내기 + 확인을 위해 아래 칸에 %1$s를 입력하세요. 사이트가 영원히 사라질 것입니다. 사이트 삭제 확인 지원팀에 문의하기 사이트를 유지하되 현재 글과 페이지를 원하지 않는 경우 지원팀에서 글, 페이지, 미디어, 댓글을 삭제해드립니다.\n\n사이트와 URL은 활성 상태를 유지하지만 콘텐츠 생성을 처음부터 시작할 수 있게 됩니다. 현재 콘텐츠를 삭제하려면 지원팀에 문의해주세요. - 사이트 시작하기 Let Us Help - 앱 설정 + 사이트 시작하기 시작하기 + 앱 설정 실패한 업로드 제거 - 삭제된 댓글 없음 고급 + 삭제된 댓글 없음 대기중인 댓글 없음 승인한 댓글 없음 넘어가기 연결할 수 없습니다. 필수 XML-RPC 함수가 서버에 없습니다. - 상태 - 비디오 가운데 - 채팅 - 갤러리 - 이미지 - 링크 - 인용 + 비디오 + 상태 기본 - 워드프레스닷컴 강의 및 이벤트에 대한 정보(온라인 및 직접 만남). - 추가 정보 + 인용 + 링크 + 이미지 + 갤러리 + 채팅 오디오 + 추가 정보 + 워드프레스닷컴 강의 및 이벤트에 대한 정보(온라인 및 직접 만남). 워드프레스닷컴 리서치 및 설문 조사에 참여할 기회. 워드프레스닷컴을 최대한 활용하기 위한 팁. 커뮤니티 - 내 댓글에 대한 답글 - 제안 리서치 - 사이트 작성 글 + 제안 + 내 댓글에 대한 답글 사용자명 멘션 - 내 글의 좋아요 + 사이트 작성 글 사이트 팔로우 + 내 글의 좋아요 내 댓글의 좋아요 내 사이트의 댓글 %d개 항목 1개의 항목 - 알려진 사용자의 댓글 모든 사용자 + 알려진 사용자의 댓글 댓글 없음 페이지당 댓글 %d개 페이지당 댓글 1개 @@ -2969,11 +3036,11 @@ Language: ko_KR 모든 사람의 댓글을 자동으로 승인합니다. 사용자가 이전에 댓글을 승인한 경우 자동으로 승인합니다. 모든 사람의 댓글에 수동 승인이 필요합니다. - 1일 %d일 - 새 주소를 확인하려면 %1$s(으)로 전송된 이메일에 있는 확인 링크를 클릭하세요. - 기본 사이트 + 1일 웹 주소 + 기본 사이트 + 새 주소를 확인하려면 %1$s(으)로 전송된 이메일에 있는 확인 링크를 클릭하세요. 미디어를 업로드 중입니다. 완료될 때까지 기다려 주세요. 지금은 댓글을 새로 고칠 수 없습니다. 이전 댓글을 표시 중입니다. 특성 이미지 설정 @@ -2982,13 +3049,13 @@ Language: ko_KR 이 댓글을 영구 삭제하시겠습니까? 이 댓글을 영구 삭제하시겠습니까? 삭제 - 댓글 삭제됨 복원 + 댓글 삭제됨 스팸 댓글 없음 - 페이지를 로드할 수 없음 모두 - 인터페이스 언어 + 페이지를 로드할 수 없음 + 인터페이스 언어 앱 정보 계정 설정을 저장할 수 없습니다. 계정 설정을 검색할 수 없습니다. @@ -2996,20 +3063,20 @@ Language: ko_KR 언어 코드가 인식되지 않음 스레드에서 댓글이 계층화되도록 허용합니다. 최대 스레드 - 제거 - 검색 해제 + 검색 + 제거 원래 크기 사이트가 본인과 본인이 승인한 사용자에게만 공개됩니다. 사이트가 모든 사람에게 공개되지만 검색 엔진에 사이트가 검색되지 않도록 요청합니다. 사이트가 모든 사람에게 공개되며 검색 엔진이 검색할 수 있습니다. 사용자에 대한 몇 가지 정보… - 대화명을 설정하지 않을 경우 기본적으로 사용자명으로 설정됩니다. 내 소개 + 대화명을 설정하지 않을 경우 기본적으로 사용자명으로 설정됩니다. 공개 이름 - 내 프로필 - 이름 + 이름 + 내 프로필 관련 글 미리보기 이미지 사이트 정보를 저장할 수 없습니다. 사이트 정보를 가져올 수 없습니다. @@ -3064,13 +3131,13 @@ Language: ko_KR %d개 레벨 비공개 숨김 - 사이트 삭제 공개 + 사이트 삭제 중재를 위해 보관 댓글의 링크 자동으로 승인 - 스레딩 페이징 + 스레딩 정렬 기준 반드시 로그인을 해야 합니다. 이름과 이메일을 포함해야 합니다. @@ -3080,22 +3147,22 @@ Language: ko_KR 기본 형식 기본 카테고리 주소 - 사이트 제목 태그라인 + 사이트 제목 새 글에 대한 기본값 - 계정 쓰기 - 최근 댓글 먼저 + 계정 일반 - 토론 - 개인 정보 - 관련 글 - 댓글 - 다음 시간 후에 닫기: + 최근 댓글 먼저 오래된 항목 먼저 + 다음 시간 후에 닫기: + 댓글 + 관련 글 + 개인 정보 + 토론 미디어를 사이트에 업로드하는 권한이 없습니다. - 절대 알 수 없음 + 절대 이 글은 더 이상 존재하지 않습니다. 이 글을 볼 권한이 없습니다. 이 글을 가져올 수 없습니다. @@ -3107,22 +3174,22 @@ Language: ko_KR 문제가 생겼습니다. 테마를 활성화할 수 없음 작성자: %1$s %1$s을(를) 선택해 주셔서 감사합니다. - 사용 및 사용자 정의하기 - 보기 - 세부사항 - 지원 - 완료 사이트 관리 + 완료 + 지원 + 세부사항 + 보기 + 사용 및 사용자 정의하기 활성화 - 현재 테마 - 사용자 정의 - 세부사항 - 지원 활성 - 글이 발행됨 - 페이지가 발행됨 - 글이 업데이트됨 + 지원 + 세부사항 + 사용자 정의 + 현재 테마 페이지가 업데이트됨 + 글이 업데이트됨 + 페이지가 발행됨 + 글이 발행됨 죄송합니다. 테마를 찾을 수 없습니다. 더 많은 글 로드 \'%s\'에 일치하는 사이트 없음 @@ -3155,279 +3222,279 @@ Language: ko_KR 알림 설정을 로드할 수 없습니다. 댓글 좋아요 앱 알림 - 알림 탭 이메일 + 알림 탭 사용자 계정에 관한 중요 이메일을 항상 보내 드리며, 유용한 정보도 받으실 수 있습니다. 최신 글 요약 연결 없음 휴지통으로 이동된 글 - 통계 휴지통 + 통계 미리 보기 보기 - 편집 발행 + 편집 이 블로그에 접근할 권한이 없습니다. 블로그를 찾을 수 없습니다. 실행 취소 요청이 만료되었습니다. 다시 시도하려면 WordPress.com에 로그인하십시요. - 최고 조회수 무시 + 최고 조회수 오늘의 통계 최고 게시글, 조회수 및 방문자 인사이트 WordPress.com에서 로그아웃 - 로그인/로그아웃 WordPress.com에 로그인 - \"%s\"은(는) 현재 사이트이기 때문에 숨길 수 없습니다. + 로그인/로그아웃 계정 설정 + \"%s\"은(는) 현재 사이트이기 때문에 숨길 수 없습니다. 워드프레스닷컴 사이트 생성 - 사이트 추가 독립 호스트 사이트 추가 + 사이트 추가 사이트 표시/숨기기 - 관리자 보기 - 사이트 보기 사이트 선택 + 사이트 보기 + 관리자 보기 사이트 전환 - 모양과 느낌 - 게시 사이트 설정 + 게시 + 모양과 느낌 설정 눌러서 표시 모두 선택 해제 - 표시 - 숨기기 모두 선택 - 언어 - 확인 코드 - 잘못된 확인 코드 + 숨기기 + 표시 계속하려면 다시 로그인하세요. - 미디어를 가져오는 중… - 페이지를 가져오는 중… - 글을 가져오는 중… + 잘못된 확인 코드 + 확인 코드 + 언어 글을 가져올 수 없음 + 알림을 열 수 없음 알 수 없는 검색어 - 글쓴이 검색어 - 알림을 열 수 없음 - 업로드 + 글쓴이 + 페이지를 가져오는 중… + 글을 가져오는 중… + 미디어를 가져오는 중… 애플리케이션 로그가 클립보드에 복사되었습니다. - 새 게시물 이 블로그는 비었습니다. + 새 게시물 텍스트를 클립보드에 복사하는 동안 오류가 발생했습니다. - 테마를 가져오는 중… - %1$d개월 - 1년 + 업로드 %1$d년 + 1년 + %1$d개월 1개월 - %1$d분 - 1시간 전 - %1$d시간 - 1일 %1$d일 + 1일 + %1$d시간 + 1시간 전 + %1$d분 1분 전 몇 초 전 - 게시물 또는 페이지 - 비디오 팔로워 + 비디오 + 게시물 또는 페이지 국가 좋아요 - - 조회수 방문자 + 조회수 + + 테마를 가져오는 중… 상세 %d개 선택함 FAQ 찾아보기 아직 댓글이 없음 - 원본 기사 보기 + 이 토픽의 글이 없습니다 좋아요 + 원본 기사 보기 댓글을 남길 수 없습니다. %1$d/%2$d 빈 게시물은 게시할 수 없습니다. 게시물을 확인 또는 편집할 권한이 없습니다. 페이지를 확인 또는 편집할 권한이 없습니다. - 1개월 이전 - 2일 이전 + 1개월 이전 1주일 이전 + 2일 이전 + 도움 및 지원 좋아함 댓글 휴지통으로 버려진 댓글 - 아직 글이 없습니다. 하나 만들어보세요! %s에 응답 + 아직 글이 없습니다. 하나 만들어보세요! 로그아웃… - 이 토픽의 글이 없습니다 - 도움 및 지원 이 작업을 실행할 수 없습니다 이 블로그를 차단할 수 없습니다 이 블로그에서 글 올리기는 더 이상 보이지 않을 것입니다 이 블로그 차단 - 업데이트 예약 - 팔로우된 블로그 - 팔로우한 블로그 - 이 블로그를 표시할 수 없습니다. - 이 블로그를 이미 팔로우하고 있습니다. - 이 블로그를 팔로우할 수 없습니다. - 이 블로그에 대한 팔로우를 취소할 수 없습니다. + 업데이트 추천 블로그가 없습니다. - 리더 블로그 - 팔로우한 토픽 + 이 블로그에 대한 팔로우를 취소할 수 없습니다. + 이 블로그를 팔로우할 수 없습니다. + 이 블로그를 이미 팔로우하고 있습니다. + 이 블로그를 표시할 수 없습니다. + 팔로우한 블로그 URL이나 팔로우할 토픽을 입력하세요 - 도우미 - 잘못된 SSL 인증서 + 팔로우된 블로그 + 팔로우한 토픽 + 리더 블로그 일반적으로 문제없이 이 사이트에 연결하는 경우, 이 오류는 누군가가 사이트를 사칭하는 것을 의미 할 수 있습니다, 그렇다면 연결하면 안됩니다. 그래도 인증서를 신뢰 하시겠습니까? - 블로그에 접속하는 동안 에러가 발생하였습니다. - 스팸이 아닙니다. - 게시물을 다시로드 할 수 없습니다. - 댓글을 다시로드 할 수 없습니다 . - 고정 페이지를 다시로드 할 수 없습니다. + 잘못된 SSL 인증서 + 도우미 입력된 사용자 이름이나 암호가 잘못되었습니다. - 알림이 없습니다. 유효한 이메일 주소를 입력 하십시오. 잘못된 이메일 주소입니다. - 미디어 항목을 로드 할 수 없습니다. 이미지를 다운로드하는 중에 오류가 발생했습니다. 다시 시도하십시오. - 테마 정보를 가져오는데 실패했습니다. 다시 시도하십시오. 코멘트 를 로드 할 수 없습니다. 다시 시도 하십시오. - 오류가 발생 했습니다. 나중에 다시 시도 하십시오. 코멘트 편집 할 때 오류가 발생 했습니다. 나중에 다시 시도 하십시오. - 사용 가능한 네트워크가 없습니다. 네트워크에 연결하고 다시 시도하십시오 . + 진행중에 에러가 발생했습니다. + 오류가 발생 했습니다. 나중에 다시 시도 하십시오. + 댓글을 다시로드 할 수 없습니다 . + 고정 페이지를 다시로드 할 수 없습니다. + 게시물을 다시로드 할 수 없습니다. + 를 삭제하는 동안 에러가 발생했습니다. + 알림이 없습니다. 미디어를 업로드하려면 SD카드가 필요합니다. 카테고리 이름은 필수 항목입니다. 카테고리를 추가했습니다. 카테고리 추가에 실패했습니다. - 진행중에 에러가 발생했습니다. - 를 삭제하는 동안 에러가 발생했습니다. - 미디어 라이브러리를 볼 수 있는 권한이 없습니다 - 페이지 설정 - 작성 설정 - 이 블로그는 비공개 설정이되어 로드 할 수 없습니다. 설정 화면에서 다시 활성하고 다시 시도하십시오. - 추가 정보 - 일부 미디어를 다시로드 할 수 없습니다. 나중에 다시 시도하십시오. - 인증이 필요합니다 + 스팸이 아닙니다. + 테마 정보를 가져오는데 실패했습니다. 다시 시도하십시오. + 블로그에 접속하는 동안 에러가 발생하였습니다. + 미디어 항목을 로드 할 수 없습니다. + 사용 가능한 네트워크가 없습니다. 네트워크에 연결하고 다시 시도하십시오 . + 이 토픽을 삭제할 수 없습니다 + 이 토픽을 추가할 수 없습니다. 응용 프로그램 로그 - 썸네일 그리드 + 앱 데이터베이스를 생성하는 동안 에러가 발생했습니다. 앱을 다시 설치하시기 바랍니다. + 이 블로그는 비공개 설정이되어 로드 할 수 없습니다. 설정 화면에서 다시 활성하고 다시 시도하십시오. + 현재 미디어를 새로 고칠 수 없습니다 + WordPress 블로그 + 이미지 설정 + 로컬 변경 새 미디어 - 코멘트를 편집 + 새 게시물 + 알림이 없습니다. + 인증이 필요합니다 + 입력한 사이트 URL이 올바른지 확인합니다. 미디어 업로드시에 임시 파일을 만들 수 없습니다 . 장치에 여유 공간이 있는지 확인 하십시오. 카테고리 이름 - 올라간 미디어 파일을 찾을 수 없습니다. 삭제 또는 이동 된 것 같습니다. - 가로 - 이미지 설정 - WordPress 블로그 - 이 코멘트 수정을 취소하시겠습니까? - 로컬 변경 + 새로운 카테고리를 추가 + 브라우저에서 표시 + 사이트 제거 + 코멘트를 변경할 수 없습니다. 댓글 본문은 필수 항목입니다. + 이 코멘트 수정을 취소하시겠습니까? 변경 사항을 저장 중 + 휴지통 + 휴지통에 보내겠습니까? + 휴지통 + 스팸 + 미승인 + 승인 + 코멘트를 편집 + 휴지통 + 스팸 + 대기중 + 승인 + 페이지를 제거 + 게시물 삭제 + 작성 설정 + 올라간 미디어 파일을 찾을 수 없습니다. 삭제 또는 이동 된 것 같습니다. + 가로 + 로컬 초안 + 페이지 설정 링크 만들기 링크 텍스트(옵션) : - 로컬 초안 - 브라우저에서 표시 - 게시물 삭제 - 페이지를 제거 - 새로운 카테고리를 추가 - 카테고리를 선택 - 새 게시물 - 연결 오류 - 편집을 취소 + 일부 미디어를 다시로드 할 수 없습니다. 나중에 다시 시도하십시오. + 미디어 라이브러리를 볼 수 있는 권한이 없습니다 + 썸네일 그리드 + 추가 정보 게시물을 로딩중 에러가 발생했습니다. 새로고침 후 다시 시도하세요. - 승인 - 대기중 - 스팸 - 휴지통 - 승인 - 미승인 - 앱 데이터베이스를 생성하는 동안 에러가 발생했습니다. 앱을 다시 설치하시기 바랍니다. - 스팸 - 휴지통 - 휴지통에 보내겠습니까? - 휴지통 - 코멘트를 변경할 수 없습니다. - 사이트 제거 - 알림이 없습니다. - 입력한 사이트 URL이 올바른지 확인합니다. - 현재 미디어를 새로 고칠 수 없습니다 이 플러그인에 접속하는 동안 오류가 발생했습니다. - 이 토픽을 추가할 수 없습니다. - 이 토픽을 삭제할 수 없습니다 + 편집을 취소 + 연결 오류 + 카테고리를 선택 링크 공유 리뷰 가져 오는 중 … - 댓글이 불승인 됐습니다 - 댓글이 스팸으로 처리됨 - 공개 블로그가 없기 때문에 WordPress에 공유 할 수 없습니다 당신에게 %d명이 \"좋아요\"를 하고 있습니다. %d명이 \"좋아요\"를 눌렀습니다. - 이미지를 선택 - 동영상을 선택 + 공개 블로그가 없기 때문에 WordPress에 공유 할 수 없습니다 + 댓글이 스팸으로 처리됨 + 댓글이 불승인 됐습니다 이 게시물 을 로드 할 수 없습니다 당신과 다른 1명이 \"좋아요\"를 눌렀습니다. - 댓글 없음. - 공유 - 팔로우 + 동영상을 선택 + 이미지를 선택 + 가입 %s를 열 수 없습니다 - 이 목록에는 아무것도 포함되어 있지 않습니다. - (제목 없음) - 리블로그 - 팔로잉 - %s를 추가 - %s를 제거 - \"좋아요\"를 클릭 했습니다 이미지를 표시 할 수 없습니다 공유에 실패 - 1명이 \"좋아요\"를 달았습니다 + 유효한 토픽이 아닙니다 + 이미 이 토픽을 팔로우하고 있습니다 의견을 게시 할 수 없습니다 + \"좋아요\"를 클릭 했습니다 + 1명이 \"좋아요\"를 달았습니다 + %s를 제거 + %s를 추가 답변 달기 - 가입 - 이미 이 토픽을 팔로우하고 있습니다 - 유효한 토픽이 아닙니다 - 테마 - 원형 - 제목 - 캡션 - 설명 - 활성화 - 클릭 수 - 태그 & 카테고리 - 오늘 - 어제 + 팔로잉 + 팔로우 + 공유 + 리블로그 + (제목 없음) + 댓글 없음. + 이 목록에는 아무것도 포함되어 있지 않습니다. + - - 공유 - 통계 - 사각 타일 - 타일 모자이크 - 슬라이드 쇼 + 어제 + 오늘 리퍼러 + 태그 & 카테고리 + 클릭 수 + 통계 + 공유 + 활성화 업데이트에 실패 - 관리 + 설명 + 캡션 + 제목 + 슬라이드 쇼 + 원형 + 타일 모자이크 + 사각 타일 + 테마 제거 - 로그인 - 팔로우 - 답장을 공개했습니다 + 관리 %d개. %d개의 새로운 알림 + 팔로우 + 답장을 공개했습니다 + 로그인 로드 중입니다… - HTTP 사용자 이름 HTTP 암호 + HTTP 사용자 이름 미디어를 업로드할때 오류가 발생했습니다 사용자 이름이나 암호가 올바르지 않습니다 . 로그인 - 암호 사용자 이름 + 암호 가입 블로그 - 페이지 - 익명 - 같은 기능을 갖춘 이미지를 사용 게시물 본문에 이미지를 포함 - 사용 가능한 네트워크가 없습니다 - 캡션(옵션) + 같은 기능을 갖춘 이미지를 사용 가로 + 캡션(옵션) + 페이지 작성 - 확인 + 익명 + 사용 가능한 네트워크가 없습니다 완료 + 확인 URL 업로드 중… 배치 @@ -3440,27 +3507,27 @@ Language: ko_KR 바로가기 이름을 입력하세요 비공개 제목 - 카테고리 태그(쉼표로 구분) + 카테고리 SD카드가 필요합니다 미디어 카테고리 업데이트 성공 승인 제거 - 없음 카테고리 업데이트 실패 - 아니오 - - 오류 - 취소 - 저장 - 추가 - 미리보기 + 없음 + 지금 공개 회신 선택 + 미리보기 카테고리의 다시 읽기 오류 + 오류 + 아니오 + 알림 설정 - 지금 공개 + 추가 + 저장 + 취소 한 번 두 번 diff --git a/WordPress/src/main/res/values-land/styles.xml b/WordPress/src/main/res/values-land/styles.xml new file mode 100644 index 000000000000..3c20b362b377 --- /dev/null +++ b/WordPress/src/main/res/values-land/styles.xml @@ -0,0 +1,7 @@ + + + + diff --git a/WordPress/src/main/res/values-lv/strings.xml b/WordPress/src/main/res/values-lv/strings.xml index cb81ede78fa5..3c4ef424243f 100644 --- a/WordPress/src/main/res/values-lv/strings.xml +++ b/WordPress/src/main/res/values-lv/strings.xml @@ -1,11 +1,93 @@ + <b>%1$s</b> izmanto %2$s atsevišķus Jetpack spraudņus + <b>%1$s</b> izmanto spraudni <b>%2$s</b> + WordPress lietotne neatbalsta vietnes ar atsevišķiem Jetpack spraudņiem. + <b>%1$s</b> izmanto atsevišķus Jetpack spraudņus, kurus neatbalsta WordPress lietotne. + <b>%1$s</b> izmanto spraudni <b>%2$s</b>, ko neatbalsta WordPress lietotne. + Nevar piekļūt dažām jūsu vietnēm + Nevar piekļūt vienai no jūsu vietnēm + Lūdzu, pārslēdzieties uz lietotni Jetpack, kur mēs jums palīdzēsim pievienot visu Jetpack spraudni, lai izmantotu šo vietni ar lietotni. + Pārslēdzieties uz Jetpack lietotni + %1$s izmanto %2$s, kas vēl neatbalsta visas lietotnes funkcijas. Lūdzu, instalējiet %3$s, lai izmantotu lietotni šajā vietnē. + Šī vietne + %1$s izmanto %2$s, kas vēl neatbalsta visas lietotnes funkcijas. Lūdzu, instalējiet %3$s. + %1$s izmanto %2$s, kas vēl neatbalsta visas lietotnes funkcijas. Lūdzu, instalējiet %3$s. + Pāreja uz Jetpack lietotni pēc dažām dienām. + Pārslēgšanās ir bezmaksas un aizņem tikai minūti. + Statistika, lasītājs, paziņojumi un citas Jetpack darbināmas funkcijas ir noņemtas no WordPress lietotnes, un tagad tās var atrast tikai Jetpack lietotnē. + Uzziniet vairāk vietnē Jetpack.com + Pārslēdzieties uz Jetpack lietotni + %s ir pārcēlušies uz Jetpack lietotni. + %s ir pārcēlies uz Jetpack lietotni. + WP administrators + Pārvaldīt + Satiksme + Saturs + Uzstādīt + Gatavs + Tagad, kad Jetpack ir instalēts, mums ir tikai jāveic iestatīšana. Tas prasīs tikai minūti. + Izgaismojiet šo ziņu tūlīt + Izgaismojiet šo lapu + Izgaismojiet šo ziņu + Sekojiet veiktspējai, iedarbiniet un apturiet savu Blaze jebkurā laikā. + Jūsu saturs parādīsies miljoniem WordPress un Tumblr vietņu. + Popularizējiet jebkuru ziņu vai lapu tikai dažu minūšu laikā par dažiem dolāriem dienā. + Palieliniet datplūsmu uz savu vietni, izmantojot Blaze + Blaze + Šis domēns jau ir reģistrēts + Izpārdošana + Ieteicams + Labākā alternatīva + Palīdzība + Atbildes uz bieži uzdotajiem jautājumiem skatiet mūsu BUJ. + Paldies, ka pārslēdzāties uz Jetpack lietotni! + Žurnāli + Biļetes + Bezmaksas + Palīdzība + Bloku izvēlne + Slēpt šo + Parādiet savu darbu miljoniem vietņu. + Reklamēt savu saturu, izmantojot Blaze + Aizvērt + Sazināties ar atbalsta dienestu + Instalēt pilnu spraudni + Noteikumi un nosacījumi + Iestatot Jetpack, jūs piekrītat mūsu + pilns Jetpack spraudnis + atsevišķi Jetpack spraudņi + spraudnis %1$s + %1$s izmanto %2$s, kas vēl neatbalsta visas lietotnes funkcijas. Lūdzu, instalējiet %3$s, lai izmantotu lietotni šajā vietnē. + Lūdzu, instalējiet pilno Jetpack spraudņa versiju + Ir pieejama tikai viena vietne, tāpēc jūs nevarat mainīt savu galveno vietni. + Sazinieties ar atbalsta dienestu + Mēģiniet vēlreiz + Jetpack pašlaik nevarēja instalēt. + Radās problēma + Kļūdas ikona + Gatavs šīs vietnes izmantošanai ar lietotni. + Instalēts Jetpack + Jetpack instalēšana jūsu vietnē. Tas var ilgt dažas minūtes. + Jetpack instalēšana + Turpināt + Jūsu vietnes akreditācijas dati netiks saglabāti un tiek izmantoti tikai Jetpack instalēšanai. + Instalēt Jetpack + Jetpack ikona + Reklamējiet ar Blaze + Atbrīvojiet visu savas vietnes potenciālu. Iegūstiet statistiku, paziņojumus un daudz ko citu, izmantojot Jetpack. + Jūsu vietnei ir spraudnis Jetpack + Jetpack mobilā lietotne ir paredzēta darbam kopā ar Jetpack spraudni. Pārslēdziet tagad, lai piekļūtu statistikai, paziņojumiem, lasītājam un daudz kam citam. + Saņemiet paziņojumus par jauniem komentāriem, atzīmēm Patīk, skatījumiem un daudz ko citu. + Atrodiet un sekojiet savām iecienītākajām vietnēm un kopienām, kā arī kopīgojiet savu saturu. + Vērojiet, kā jūsu datplūsma pieaug, izmantojot noderīgus ieskatus un visaptverošu statistiku. + Statistika un ieskati Jetpack ļauj paveikt vairāk savā WordPress vietnē. Pārslēgšanās ir bezmaksas un aizņem tikai minūti. Sniedziet atbalstu WordPress ar Jetpack Emuāru rakstīšanas uzvednes un atgādinājumus varat kontrolēt katrā laikā sadaļā Mana vietne > Iestatījumi > Emuāri @@ -94,9 +176,6 @@ Language: lv Atveriet saites pakalpojumā Jetpack Vajadzīga palīdzība? Sapratu - Lūdzu, <b>izdzēsiet WordPress lietotni</b>, lai izvairītos no datu konfliktiem. - Šķiet, ka jums joprojām ir instalēta WordPress lietotne. Mēs iesakām dzēst WordPress lietotni, lai izvairītos no datu konfliktiem. - Jums vairs nav nepieciešama WordPress lietotne Mēs nevaram pārsūtīt jūsu datus un iestatījumus bez tīkla savienojuma. Lūdzu, pārbaudiet, vai tīkla savienojums darbojas, un mēģiniet vēlreiz. Nevar izveidot savienojumu ar internetu. @@ -106,13 +185,11 @@ Language: lv Mēģini vēlreiz Pabeigt Noņemiet WordPress lietotnes ikonu - Lūdzu, <b>izdzēsiet WordPress lietotni</b>, lai izvairītos no datu konfliktiem. Mēs esam pārsūtījuši visus jūsu datus un iestatījumus. Viss ir tieši tur, kur tas tika atstāts. Paldies, ka pārgājāt uz Jetpack! Mēs izslēgsim paziņojumus no WordPress lietotnes. Jūs saņemsiet visus tos pašus paziņojumus, taču tagad tie tiks saņemti no lietotnes Jetpack. Paziņojumi tagad nāk no Jetpack - Lūdzu, izdzēsiet WordPress lietotni WordPress palīdzības centrs Atbalsts Ļauj lietotnei atspējot WordPress paziņojumus. @@ -1115,7 +1192,6 @@ Language: lv Iepazīstinām ar stāstu ziņām Izveidota tukša lapa Lapa ir izveidota - %1$s tika liegta piekļuve jūsu fotoattēliem. Lai to labotu, rediģējiet savas atļaujas un ieslēdziet %2$s un %3$s. Multivides ievietošana neizdevās. Multivides ievietošana neizdevās: %s Izvēlieties no WordPress multivides bibliotēkas @@ -1800,7 +1876,6 @@ Language: lv Ierakstiet atslēgvārdu, lai iegūtu vairāk ideju Ieteikumi netika atrasti Reģistrēt domēnu - Tagad, kad Jetpack ir instalēts, mums ir jāveic iestatīšana. Tas prasīs tikai minūti. Noņemt no ieskatiem Pārvietojieties uz leju Pārvietojieties uz augšu @@ -2066,15 +2141,6 @@ Language: lv Nav tēmas, kurām sekoju Pievienojiet tematus šeit, lai atrastu ziņas par iecienītākajām tēmām Piesakieties WordPress.com kontā, kuru izmantojāt, lai izveidotu savienojumu ar Jetpack. - Mēģiniet vēlreiz - Uzstādīt - Jetpack pašlaik nebija iespējams instalēt. - Radās problēma - Jetpack uzstādīts - Notiek Jetpack instalēšana jūsu vietnē. Tas var aizņemt dažas minūtes. - Sociālā kopīgošana - Jūsu vietnes akreditācijas dati netiks saglabāti un tiks izmantoti tikai Jetpack instalēšanai. - Instalējiet Jetpack Jetpack Jetpack BUJ Lai savā WordPress vietnē izmantotu statistiku, jums būs jāinstalē Jetpack spraudnis. @@ -2628,7 +2694,6 @@ Language: lv Dokumenti Attēli Viss - %1$s tika liegta piekļuve jūsu fotoattēliem. Lai to novērstu, rediģējiet savas atļaujas un ieslēdziet %2$s. Skatīt komentārus Video kvalitāte. Augstākas vērtības nozīmē labākas kvalitātes videoklipus. Maina videoklipu izmērus ziņās līdz šim izmēram diff --git a/WordPress/src/main/res/values-ms/strings.xml b/WordPress/src/main/res/values-ms/strings.xml index ac4946723498..4a2cb8a004d9 100644 --- a/WordPress/src/main/res/values-ms/strings.xml +++ b/WordPress/src/main/res/values-ms/strings.xml @@ -181,7 +181,6 @@ Language: ms Dokumen Imej Video - %1$s telah menolak capaian ke gambar-gambar anda. Untuk membaikinya, sunting keizinan anda dan hidupkan %2$s. Lihat ulasan Kualiti video. Nilai lebih tinggi bermakna kualiti video yang lebih baik. Bolehkan untuk saiz semula dan mempatkan video diff --git a/WordPress/src/main/res/values-nb/strings.xml b/WordPress/src/main/res/values-nb/strings.xml index 256989dd3db1..dc87982129ce 100644 --- a/WordPress/src/main/res/values-nb/strings.xml +++ b/WordPress/src/main/res/values-nb/strings.xml @@ -975,7 +975,6 @@ Language: nb_NO Skriv inn et nøkkelord for flere detaljer Ingen forslag funnet Registrer domene - Nå som Jetpack er installert trenger vi bare å få satt deg opp. Dette vil bare ta et minutt. Fjern fra innsikter Flytt ned Flytt opp @@ -1237,15 +1236,6 @@ Language: nb_NO Ingen fulgte emner Legg til emner for å finne innlegg om dine favoritt-emner Logg inn på dem WordPres.com-kontoen du brukte til koble til Jetpack. - Prøv på nytt - Sett opp - Jetpack kunne ikke installeres nå. - Det oppsto et problem - Jetpack installert - Installerer Jetpack på ditt nettsted. Dette kan ta opp til et par minutter å fullføre - Installerer Jetpack - Din innlogging til nettstedet må lagres og vil kun bli brukt for å installere Jetpack. - Installer Jetpack Jetpack Jetpack FAQ For å se statistikk på ditt nettsted trenger du å installere utvidelsen Jetpack. @@ -1775,7 +1765,6 @@ Language: nb_NO Dokumenter Bilder Alle - %1$s ble nektet tilgang til bildene dine. For å fikse dette, rediger tillatelsene dine og skru på %2$s. Vis kommentarer Kvaliteten til videoer. Høyere verdier = bedre kvalitet. Endrer størrelsen til videoer i innlegg til denne størrelsen diff --git a/WordPress/src/main/res/values-night/colors.xml b/WordPress/src/main/res/values-night/colors.xml index de8c86e12deb..12704f93fd0e 100644 --- a/WordPress/src/main/res/values-night/colors.xml +++ b/WordPress/src/main/res/values-night/colors.xml @@ -99,6 +99,7 @@ #3d3d3d + @color/white_translucent_20 #E6121212 diff --git a/WordPress/src/main/res/values-nl/strings.xml b/WordPress/src/main/res/values-nl/strings.xml index 261f0ad768d6..7bf73ca1c84b 100644 --- a/WordPress/src/main/res/values-nl/strings.xml +++ b/WordPress/src/main/res/values-nl/strings.xml @@ -1,35 +1,110 @@ + Blokken verwijderen + Privacy en beoordeling + Handmatig + Beschrijf het doel van de afbeelding. Laat dit veld leeg als de afbeelding decoratief is. + Ga aan de slag met op maat gemaakte lay-outs die geschikt zijn voor mobiele apparaten + Nog een pagina aanmaken + Voeg pagina\'s toe aan je site + Dit verbergen + Eis je eigen hoekje van het internet op met een site-adres dat eenvoudig te vinden, te delen en te volgen is. + Bemachtig je eigen online identiteit met een aangepast domein + Als je blog herinneringen wilt gebruiken, moet je berichtgeving inschakelen. + Domein aanschaffen + Foto’s en video\'s + Muziek en audio + %s heeft toestemming nodig om je audio te openen + %s heeft toestemming nodig om je video\'s te openen + %s heeft toestemming nodig om je foto\'s te openen + %s heeft toestemming nodig om je foto\'s en video\'s te openen + %s heeft toestemming nodig om je muziek, audio, foto\'s en video\'s te openen + Ga naar Instellingen &rarr; Meldingen &rarr; App-instellingen, en zet %1$s aan om direct op de hoogte te worden gesteld. + Je moet de app openen om meldingen te kunnen zien. + Pushmeldingen zijn uitgeschakeld + Pushmeldingen zijn uitgeschakeld. + Toestemmingswaarschuwing voor meldingen negeren. + Oplossing + <b>%1$s</b> gebruikt %2$s individuele Jetpack-plugins + <b>%1$s</b> gebruikt de <b>%2$s</b>-plugin + Sites met individuele Jetpack-plugins worden niet ondersteund door de WordPress-app. + <b>%1$s</b> gebruikt individuele Jetpack-plugins, deze worden niet ondersteund door de WordPress-app. + <b>%1$s</b> gebruikt de <b>%2$s</b>-plugin, deze wordt niet ondersteund door de WordPress-app. + Kan een van je sites niet openen + Schakel over naar de Jetpack-app waarin we je helpen de volledige Jetpack-plugin te koppelen, zodat je deze site kunt gebruiken met de app. + Schakel over naar de Jetpack-app + %1$s gebruikt %2$s. Deze ondersteunt nog niet alle functies van de app.\n\nInstalleer de %3$s om de app met deze site te gebruiken. + Deze site + %1$s gebruikt %2$s. Deze ondersteunen nog niet alle functies van de app. Installeer de %3$s. + %1$s gebruikt %2$s. Deze ondersteunt nog niet alle functies van de app. Installeer de %3$s. + We stappen binnen een paar dagen over naar Jetpack. + Overstappen is gratis en duurt maar een minuut. + Statistieken, Reader, meldingen en andere functies die door Jetpack aangedreven worden, zijn verwijderd uit de WordPress-app en kunnen nu alleen in de Jetpack-app gevonden worden. + Ga naar Jetpack.com voor meer informatie + Schakel over naar de Jetpack-app + %s zijn naar de Jetpack-app verplaatst + %s is naar de Jetpack-app verplaatst + WP Admin + Beheren + Bezoekersaantallen + Inhoud + Instellen + Gereed + Nu Jetpack is geïnstalleerd, hoeven we er alleen nog voor te zorgen dat alles goed ingesteld is. Dit duurt maar heel even. + Blaze nu een bericht + Blaze deze pagina + Blaze dit bericht + Volg prestaties, start en stop je Blaze op elk gewenst moment. + Je content zal op miljoenen WordPress en Tumblr sites verschijnen. + Promoot in een paar minuten elke post of pagina voor slechts enkele euro’s per dag. + Zorg dat meer mensen je site bezoeken met Blaze + Blaze + Dit domein is al geregistreerd + Aanbieding! + Aanbevolen + Beste alternatief + Hulp + Bekijk onze pagina met veelgestelde vragen om mogelijke antwoorden te vinden op jouw vragen. + Bedankt voor het overschakelen naar de Jetpack-app! + Logbestanden + Tickets + Gratis + Hulp Blokkenmenu Dit verbergen Stel je werk tentoon op miljoenen sites. + Promoot je content met Blaze + Sluiten Neem contact op met de ondersteuning De volledige plug-in installeren + Voorwaarden en condities Door Jetpack in te stellen, ga je akkoord met onze volledige Jetpack-plug-in individuele Jetpack-plug-ins de %1$s-plug-in - %1$s gebruikt %2$s. Deze ondersteunen nog niet alle functies van de app.\n\nInstalleer de %3$s om de app met deze site te gebruiken. + %1$s gebruikt %2$s. Deze ondersteunen nog niet alle functies van de app.\n\nInstalleer de %3$s om de app met deze site te gebruiken. Installeer de volledige Jetpack-plug-in - Deze site gebruikt een afzonderlijke plug-in. Deze plug-in ondersteunt nog niet alle functies van de app. Installeer de volledige Jetpack-plug-in. - Neem contact op met de ondersteuning - Opnieuw proberen - Er is een probleem opgetreden - Foutpictogram - Gereed - Gereed om deze site met de app te gebruiken. - Jetpack installeren - Doorgaan - De inloggegevens voor je website zullen niet worden opgeslagen en worden alleen gebruikt om Jetpack te installeren. - Jetpack installeren - Jetpack-pictogram - Ontgrendel de volledige functionaliteit van je site. Krijg statistieken, meldingen en meer met Jetpack. + Er is maar één site beschikbaar, dus je kunt je primaire site niet wijzigen. + Neem contact op met de ondersteuning + Opnieuw proberen + Jetpack kan momenteel niet geïnstalleerd worden. + Er is een probleem opgetreden + Foutpictogram + Gereed om deze site met de app te gebruiken. + Jetpack geïnstalleerd + Jetpack installeren op je site. Het kan een aantal minuten duren voordat de installatie is afgerond. + Jetpack installeren + Doorgaan + De inloggegevens voor je website zullen niet worden opgeslagen en worden alleen gebruikt om Jetpack te installeren. + Jetpack installeren + Jetpack pictogram + Promoten met Blaze + Ontgrendel de volledige functionaliteit van je site. Krijg Statistieken, Lezer, Meldingen en meer met Jetpack. Je site heeft de Jetpack-plugin De Jetpack mobiele app is ontworpen om samen met de Jetpack-plugin te werken. Stap nu over om toegang te krijgen tot Statistieken, Meldingen, Reader en meer. Krijg meldingen over nieuwe reacties, likes, weergaven en meer. @@ -37,6 +112,7 @@ Language: nl Laat het aantal bezoekers van je site groeien via waardevolle inzichten en uitgebreide statistieken. Statistieken en inzichten Met Jetpack kun je meer doen met je WordPress-site. Overstappen is gratis en duurt maar een minuut. + Geef WordPress een boost met Jetpack Je kunt blogmeldingen en -herinneringen te allen tijde beheren via Mijn site > Instellingen > Bloggen Meldingen bevatten een woord of korte zin ter inspiratie Ga naar <b>Site-instellingen</b> om weer in te schakelen @@ -46,7 +122,9 @@ Language: nl Communityforums Blogherinneringen Meldingen weergeven + Bloggen Installeer Google Play Store om de Jetpack-app te downloaden + Doe dit later Overstappen naar Jetpack Statistieken, Reader, meldingen en andere functies die door Jetpack aangedreven worden, zijn verwijderd uit de WordPress-app. Jetpack-functies zijn verplaatst. @@ -62,7 +140,10 @@ Language: nl Je hebt de afgelopen 7dagen %1$s meer bezoekers gehad dan de vorige 7 dagen. Je hebt de afgelopen 7dagen %1$s minder weergaven gehad dan de vorige 7 dagen. Je hebt de afgelopen 7dagen %1$s meer weergaven gehad dan de vorige 7 dagen. + Vorige 7 dagen + Afgelopen 7 dagen %d weken + 1 week Vanaf <b>DayOne</b> Dit verbergen Herinner mij hier later aan @@ -80,19 +161,32 @@ Language: nl Controleer je netwerkverbinding en probeer het nogmaals. Deze inhoud kan momenteel niet geladen worden Er is een fout opgetreden bij het laden van opdrachten. + Oeps Er zijn nog geen opdrachten + %d antwoorden + 1 antwoord + 0 antwoorden ✓ Beantwoord + Prompts + sluiten + Als alternatief, kun je dit blok ook losmaken en afzonderlijk bewerken door op \'Converteren naar normaal blok\' te tikken. + Categorie \'%s\' permanent verwijderen? Categorie is verwijderd Verwijderen van categorie mislukt Categorie verwijderen Categorie bijwerken Categorie bijwerken + Berichten van deze gebruiker zullen niet meer getoond worden Gebruiker blokkeren Deze gebruiker rapporteren + Links openen in WordPress Het lijkt erop dat je de Jetpack-app geïnstalleerd hebt.\n\nWil je links in de toekomst in de Jetpack-app openen?\n\nJe kan dit altijd veranderen in App-instellingen > Links openen in Jetpack + Links openen in Jetpack? + Doorgaan zonder Jetpack Jetpack biedt statistieken, meldingen en meer om je te helpen de WordPress-site van je dromen te bouwen.\n\nDe WordPress-app ondersteunt het maken van een nieuwe site niet meer. Jetpack biedt statistieken, meldingen en meer om je te helpen de WordPress-site van je dromen te bouwen. Maak een nieuwe WordPress-site met de Jetpack-app + weblinks uri-links Schakel over naar de Jetpack-app om realtime meldingen te blijven ontvangen op je apparaat. Schakel over naar de Jetpack-app om je favoriete sites en berichten te vinden, te volgen en een like te geven met Reader. @@ -103,24 +197,24 @@ Language: nl Kan links openen in Jetpack niet uitschakelen Kan links openen in Jetpack niet inschakelen Links openen in Jetpack + Hulp nodig? Duidelijk - <b>Verwijder de WordPress-app</b> om gegevensconflicten te vermijden. - Het lijkt erop dat je de WordPress-app nog steeds geïnstalleerd hebt. We raden je aan om de WordPress-app te verwijderen om gegevensconflicten te vermijden. - Je hebt de WordPress-app niet meer nodig We kunnen je gegevens en instellingen niet overdragen zonder netwerkverbinding. Controleer of je een netwerkverbinding hebt en probeer het opnieuw. + Kan geen verbinding maken met het internet. Probeer het later opnieuw of neem contact op met de klantenservice. Er is iets misgegaan. Je gegevens zijn veilig, maar we kunnen ze op dit moment niet overdragen. Oeps, er is iets fout gegaan… Opnieuw proberen Afronden Verwijder pictogram WordPress-app - <b>Verwijder de WordPress-app</b> om gegevensconflicten te vermijden. + We hebben al je gegevens en instellingen overgedragen. Alles staat precies waar je het gelaten hebt. Bedankt dat je bent overgestapt naar Jetpack! We zullen meldingen van de WordPress-app uitschakelen. + Je krijgt dezelfde meldingen, maar ze komen nu van de Jetpack-app. Je krijgt nu meldingen van Jetpack - Verwijder de WordPress-app WordPress help centrum + Ondersteuning Staat de app toe om meldingen van WordPress uit te schakelen. meldingen van WordPress uitschakelen Hulp nodig? @@ -366,6 +460,7 @@ Language: nl Bijv. mode, poëzie, politiek Onderwerp van de site Tik op <b>%1$s</b> om door te gaan. + Sla deze prompt over Bekijk meer prompts %d antwoorden Blog-opdracht delen @@ -1120,7 +1215,6 @@ Language: nl Introductie verhalen Blanco pagina aangemaakt Pagina aangemaakt - %1$s heeft geen toegang tot je foto\'s. Om dit op te lossen, bewerk je je rechten en schakel je %2$s en %3$s in. Media invoegen mislukt. Invoegen media mislukt: %s Kies uit WordPress mediabibliotheek @@ -1805,7 +1899,6 @@ Language: nl Type een trefwoord voor meer ideeën Geen suggesties gevonden Registreer domein - Nu Jetpack is geïnstalleerd, hoeven we er alleen nog voor te zorgen dat alles goed ingesteld is. Dit duurt maar heel even. Verwijderen uit inzichten Omlaag verplaatsen Omhoog verplaatsen @@ -1883,7 +1976,7 @@ Language: nl Kies een unieke site pictogram Je bezoekers zien je pictogram in hun browser. Voeg een aangepast pictogram toe voor een stijlvolle, professionele look. Selecteer %1$s Statistieken %2$s om te zien hoe je site presteert. - Tik op %1$s Jouw favicon %2$s om een nieuwe te uploaden + Tik op %1$s je site pictogram %2$s om een nieuwe te uploaden Stel een bericht op en publiceer het. Berichten delen inschakelen Deel nieuwe berichten automatisch op je socialmedia-accounts. @@ -2071,15 +2164,6 @@ Language: nl Geen gevolgde onderwerpen Voeg hier onderwerpen toe om berichten te vinden over je favoriete onderwerpen Log in op het WordPress.com-account dat je gebruikte om verbinding te maken met Jetpack. - Opnieuw proberen - Instellen - Jetpack kan momenteel niet geïnstalleerd worden. - Er is een probleem opgetreden - Jetpack geïnstalleerd - Jetpack installeren op je site. Het kan een aantal minuten duren voordat de installatie is afgerond. - Jetpack installeren - Je site-referenties zullen niet worden opgeslagen en worden alleen gebruikt om Jetpack te installeren. - Jetpack installeren Jetpack Veelgestelde vragen Jetpack Installeer de Jetpack-plugin om statistieken voor je WordPress-site te gebruiken. @@ -2633,7 +2717,7 @@ Language: nl Documenten Afbeeldingen Alles - %1$s kreeg geen toegang tot je foto\'s. Om dit te verhelpen, pas de rechten aan en schakel %2$s in. + %1$s kon geen toegang krijgen tot je mediabestanden. Bewerk je toestemmingen en schakel %2$s in om dit op te lossen. Reacties bekijken Kwaliteit van video\'s. Hoe hoger de waarde, hoe beter de kwaliteit van je video\'s. De afmeting van video\'s in berichten wijzigen naar dit formaat diff --git a/WordPress/src/main/res/values-pl/strings.xml b/WordPress/src/main/res/values-pl/strings.xml index 91df4d05b24e..429ae5d1f0a5 100644 --- a/WordPress/src/main/res/values-pl/strings.xml +++ b/WordPress/src/main/res/values-pl/strings.xml @@ -825,7 +825,6 @@ Language: pl Nowe relacje są dla każdego Utworzono pustą stronę Utworzono stronę - %1$s nie ma dostępu do twoich zdjęć. Aby to naprawić, edytuj uprawnienia i włącz %2$s i %3$s. Nie udało się osadzić plików mediów. Nie udało się osadzić plików mediów: %s Wybierz z biblioteki mediów WordPressa @@ -1504,7 +1503,6 @@ Language: pl Suma obserwujących Datowany wstecznie dla: %s Zarejestruj domenę - Jetpack jest już zainstalowany, czas go skonfigurować. Zajmie to chwilę. Nie można było wczytać sugestii dla nazwy domeny Wpisz słowo kluczowe aby uzyskać więcej pomysłów Nie znaleziono sugestii @@ -1775,17 +1773,8 @@ Language: pl Dodaj tematy aby odnaleźć wpisy dotyczące tych które ciebie interesują Nie masz żadnych witryn Zaloguj się na stronę konto WordPress.com które używałeś do podłączenia Jetpacka. - Nie można było zainstalować Jetpacka - Wystąpił problem - Jetpack zainstalowany - Instalowanie Jetpacka - Zainstaluj Jetpacka Jetpack - Dane logowania do witryny nie będą przechowywane i są użyte jedynie do celów instalacji Jetpacka. - Instalowanie Jetpacka na twojej witrynie. To może zająć do kilku minut. - Spróbuj ponownie Najczęściej zadawane pytania Jetpacka - Skonfiguruj Aby korzystać ze statystyk na swojej witrynie, musisz zainstalować wtyczkę Jetpack. Brak motywów spełniających kryteria wyszukiwania Co chciał(a)byś wyszukać? @@ -2337,7 +2326,6 @@ Language: pl Filmy Dokumenty Plik dźwiękowy - %1$s nie ma dostępu do twoich zdjęć. Aby to naprawić, edytuj uprawnienia i włącz %2$s. Zobacz komentarze Jakość filmów. Wyższa wartość oznacza lepszą jakość filmów. Zmień rozmiar filmów we wpisie do tego rozmiaru diff --git a/WordPress/src/main/res/values-pt-rBR/strings.xml b/WordPress/src/main/res/values-pt-rBR/strings.xml index 6ba153d2ea09..62da08c8c68e 100644 --- a/WordPress/src/main/res/values-pt-rBR/strings.xml +++ b/WordPress/src/main/res/values-pt-rBR/strings.xml @@ -1,15 +1,146 @@ + Recomendamos que você <b>desinstale o aplicativo WordPress</b> de seu dispositivo para evitar conflitos de dados. + Parece que você ainda tem o aplicativo WordPress instalado. + Você não precisa mais do aplicativo WordPress em seu dispositivo + Recomendamos que você <b>desinstale o aplicativo WordPress</b> de seu dispositivo para evitar conflitos de dados. + Boas-vindas ao aplicativo Jetpack. Agora você pode desinstalar o aplicativo WordPress. + Remover blocos + Privacidade e classificação + Configurações de reprodução + Cor da barra de reprodução + Manual + Dinâmico + Descreva o objetivo da imagem. Deixe vazio caso seja decorativa. + Comece com layouts personalizados e compatíveis com dispositivos móveis + Criar outra página + Adicionar páginas ao seu site + Esconder isso + Marque sua presença na Web com um endereço de site fácil de encontrar, compartilhar e seguir. + Tenha sua identidade online com um domínio personalizado + Para usar lembretes para publicação, será necessário ativar as notificações por push. + Ativar notificações por push + Continuar com subdomínio + Comprar domínio + Fotos e vídeos e músicas e áudios + Músicas e áudios + Fotos e vídeos + %s precisa de permissão para acessar seus áudios + %s precisa de permissão para acessar seus vídeos + %s precisa de permissão para acessar suas fotos + %s precisa de permissão para acessar suas fotos e vídeos + %s precisa de permissão para acessar suas músicas, áudios, fotos e vídeos + Ativar notificações + Acesse Configurações &rarr; Notificações &rarr; Configurações do aplicativo e ative %1$s para ser notificado de imediato. + Você precisará abrir o aplicativo para ver as notificações. + As notificações por push estão desativadas + As notificações por push estão desativadas. + Ignorar aviso de permissão de notificação. + Corrigir + <b>%1$s</b> está usando %2$s plugins individuais do Jetpack + <b>%1$s</b> está usando o plugin <b>%2$s</b> + Sites com plugins individuais do Jetpack não são suportados pelo aplicativo WordPress. + <b>%1$s</b> está usando plugins individuais do Jetpack. Algo que não é compatível com o aplicativo do WordPress. + <b>%1$s</b> está usando o plugin <b>%2$s</b>, que não é suportado pelo aplicativo do WordPress. + Não foi possível acessar alguns de seus sites + Não foi possível acessar um de seus sites + Mude para o aplicativo Jetpack onde poderemos te ajudar a conectar a versão completa do plugin Jetpack ao seu site para usar esse aplicativo. + Mudar para o aplicativo do Jetpack + %1$s está usando %2$s, que ainda não é compatível com todas as funcionalidades do aplicativo.\n\nInstale o %3$s para usar o aplicativo com este site. + Este site + %1$s está usando %2$s, que ainda não são compatíveis com todas as funcionalidades do aplicativo. Instale o %3$s. + %1$s está usando %2$s, que ainda não é compatível com todas as funcionalidades do aplicativo. Instale o %3$s. + Indo para o aplicativo Jetpack em alguns dias. + A mudança é gratuita e leva apenas um minuto. + As estatísticas, o leitor, as notificações e outras funcionalidades com tecnologia Jetpack foram removidas do aplicativo WordPress. Agora elas ficarão apenas no aplicativo Jetpack. + Saiba mais em br.jetpack.com + Mudar para o aplicativo do Jetpack + %s migraram para o aplicativo do Jetpack. + %s migrou para o aplicativo do Jetpack. + Painel WP Admin + Gerenciar + Tráfego + Conteúdo + Configurar + Concluído + Agora que o Jetpack está instalado, só precisamos configurá-lo. Isso levará apenas um minuto. + Promover um post com Blaze agora + Promover a página com Blaze + Promover o post com Blaze + Acompanhe o desempenho, inicie e pare o Blaze a qualquer momento. + Seu conteúdo aparecerá em milhões de sites do WordPress e do Tumblr. + Promova qualquer post ou página em apenas alguns minutos por um preço muito acessível. + Atraia mais tráfego para seu site com Blaze + Blaze + Este domínio já está registrado + Promoção + Recomendado + Melhor alternativa + Ajuda + Nossas Perguntas Frequentes podem responder algumas de suas dúvidas. + Agradecemos pela preferência pelo aplicativo Jetpack. + Registros + Chamados + Gratuito + Ajuda + Menu de blocos + Esconder isso + Exiba seu trabalho em milhões de sites. + Promova seu conteúdo com Blaze + Fechar + Contatar o suporte + Instalar plugin completo + Termos e condições + Ao configurar o Jetpack, você concorda com nossos + plugin completo do Jetpack + plugins individuais do Jetpack + o plugin %1$s + %1$s está usando %2$s, que ainda não são compatíveis com todas as funcionalidades do aplicativo.\n\nInstale o %3$s para usar o aplicativo com este site. + Instale o plugin completo do Jetpack + Há apenas um site disponível, portanto não é possível alterar seu site principal. + Contatar o suporte + Tentar novamente + Não foi possível instalar o Jetpack no momento. + Ocorreu um problema + Ícone de erro + Pronto para usar este site com o aplicativo. + Jetpack instalado + Instalando o plugin do Jetpack no seu site. Isso pode levar alguns minutos para ser concluído. + Instalando Jetpack + Continuar + As credenciais do seu site não serão armazenadas e serão usadas apenas para a instalação do Jetpack. + Instalar Jetpack + Ícone do Jetpack + Promova com Blaze + Libere todo o potencial do seu site. Veja estatísticas, notificações e muito mais com o Jetpack. + Seu site possui o plugin Jetpack + O aplicativo Jetpack foi projetado para trabalhar junto ao plugin Jetpack. Mude agora e tenha acesso a estatísticas, notificações, leitor e muito mais. + Receba notificações sobre novos comentários, curtidas, visualizações, e muito mais. + Encontre e siga seus sites e comunidades favoritos, e compartilhe seu conteúdo. + Acompanhe o crescimento de seu site com análises úteis e estatísticas fáceis de entender. + Estatísticas e informações + O Jetpack permite que você faça ainda mais com seu site WordPress. A mudança é gratuita e leva apenas um minuto. + Melhore o WordPress com o Jetpack + Você pode controlar as sugestões e os lembretes de publicação a qualquer momento em Meu site > Configurações > Blog + A notificação incluirá uma palavra ou uma frase curta, para inspiração + Vá às <b>configurações do site</b> para reativar as sugestões + Sugestões de publicação ocultas + Desativar as sugestões + Receba ajuda do nosso grupo de voluntários. + Fórum da comunidade + Lembretes de publicação + Mostrar sugestões + Blog Instale o Google Play Store para obter o aplicativo Jetpack Fazer isso depois Mudar para o Jetpack - Estatísticas, Leitor, Notificações e outros recursos fornecidos pelo Jetpack foram removidos do aplicativo WordPress. + Estatísticas, Leitor, Notificações e outras funcionalidades com tecnologia Jetpack foram removidas do aplicativo WordPress. Recursos do Jetpack foram movidos. %1$s serão movidos em %2$s %1$s será movido em %2$s @@ -30,11 +161,12 @@ Language: pt_BR Do <b>DayOne</b> Esconder isso Lembre-me depois + As estatísticas, o Leitor, as notificações e outras funcionalidades em breve irão para o aplicativo móvel do Jetpack. Mudar para o aplicativo do Jetpack Saiba mais em br.jetpack.com A migração é gratuita e leva apenas um minuto. - As estatísticas, o Leitor, as notificações e outras funcionalidades com tecnologia Jetpack serão removidas do aplicativo do WordPress em breve. - As estatísticas, o Leitor, as notificações e outras funcionalidades com tecnologia Jetpack serão removidas do aplicativo do WordPress em %s. + As estatísticas, o Leitor, as notificações e outras funcionalidades com tecnologia Jetpack serão removidas do aplicativo WordPress em breve. + As estatísticas, o Leitor, as notificações e outras funcionalidades com tecnologia Jetpack serão removidas do aplicativo WordPress em %s. Recursos do Jetpack serão movidos em breve. As notificações estão migrando para o aplicativo Jetpack O Leitor está migrando para o aplicativo Jetpack @@ -81,9 +213,6 @@ Language: pt_BR Abrir links no Jetpack Precisa de ajuda? Entendi - <b>Exclua o aplicativo WordPress</b> para evitar conflitos de dados. - Você ainda tem o aplicativo WordPress instalado. Exclua o aplicativo para evitar conflitos de dados. - O aplicativo WordPress não é mais necessário Não é possível transferir seus dados e configurações sem uma conexão de rede. Confira se sua conexão de rede está funcionando e tente novamente. Não é possível se conectar à internet. @@ -93,13 +222,11 @@ Language: pt_BR Tente novamente Concluir Remover ícone do aplicativo WordPress - <b>Exclua o aplicativo WordPress</b> para evitar conflitos de dados. Transferimos todos os seus dados e configurações. Está tudo certinho. Agradecemos por mudar para o Jetpack. Vamos desativar as notificações do aplicativo WordPress. Você receberá as mesmas notificações, mas pelo aplicativo Jetpack. As notificações agora vêm do Jetpack - Exclua o aplicativo WordPress Central de ajuda do WordPress Suporte Permite que o aplicativo desative as notificações do WordPress. @@ -309,7 +436,9 @@ Language: pt_BR Nota: Mostraremos um novo aviso diariamente em seu painel para ajudar a manter a criatividade rolando! A melhor forma de aprimorar sua escrita é fazer dela um hábito e compartilhá-la com outras pessoas. É aí que entram as sugestões! + Apresentando\nas sugestões Definir lembretes + Incluir sugestão diária Publicar com frequência atrai novos leitores. Informe quando você quer escrever e enviaremos um lembrete. Melhore sua escrita fazendo dela um hábito Literatura e poesia @@ -615,6 +744,7 @@ Language: pt_BR Suporte do WordPress para Android Gerenciar as categorias de seu site Categorias + Lembretes O conteúdo da sua página de posts mais recentes é automaticamente gerado e não pode ser editada. Configurações de borda Não mostrar novamente @@ -675,6 +805,7 @@ Language: pt_BR Tentar novamente GIF Um + Adicionar título Nenhuma visualização disponível Cor do texto Recuo @@ -683,6 +814,7 @@ Language: pt_BR URL personalizado Criar mídia incorporada Coluna %d + Mais Descreva brevemente o link para ajudar pessoas que usam leitores de tela Adicionar blocos Nenhum site Jetpack foi encontrado @@ -1099,7 +1231,6 @@ Language: pt_BR Apresentando os posts de Story Página vazia criada Página criada - %1$s teve o acesso negado às suas fotos. Para corrigir, edite suas permissões e habilite %2$s e %3$s. Inserção de imagem falhou. Inserção de imagem com falha: %s Escolher da biblioteca de mídias do WordPress @@ -1579,6 +1710,7 @@ Language: pt_BR Adicionar imagem ou vídeo Adicionar imagem Adicionar um bloco aqui + Adicionar descrição \"Toque no botão Adicionar para salvar posts para adicionar um post à sua lista\" \"A lista foi carregada com %1$d itens.\" Notificações @@ -1784,7 +1916,6 @@ Language: pt_BR Digite uma palavra-chave para mais ideias Nenhuma sugestão encontrada Registrar domínio - Agora que o Jetpack está instalado, só precisamos configurá-lo. Isso levará apenas um minuto. Remover do resumo Mover para baixo Mover para cima @@ -2050,15 +2181,6 @@ Language: pt_BR Nenhum tópico seguido Adicione tópicos aqui para encontrar posts sobre seus assuntos favoritos Acesse a conta do WordPress.com usada para conectar o Jetpack. - Tentar novamente - Continuar a configuração - Não foi possível instalar o Jetpack no momento. - Houve um problema - Jetpack instalado - Instalando o Jetpack em seu site. Isso pode levar alguns minutos para ser concluído. - Instalando Jetpack - As informações de seu site não serão salvas conosco e serão usadas apenas para instalar o Jetpack. - Instalar Jetpack Jetpack Perguntas frequentes sobre o Jetpack Para usar estatísticas em seu site WordPress é necessário instalar o plugin Jetpack. @@ -2612,7 +2734,7 @@ Language: pt_BR Documentos Imagens Todos - %1$s teve o acesso às suas fotos negado. Edite suas permissões e habilite %2$s para corrigir. + %1$s teve o acesso negado aos seus arquivos de mídia. Para corrigir isso, edite suas permissões e ative %2$s. Ver comentários Qualidade dos vídeos. Valores maiores significam vídeos com mais qualidade. Redimensiona vídeos nos posts para este tamanho diff --git a/WordPress/src/main/res/values-ro/strings.xml b/WordPress/src/main/res/values-ro/strings.xml index 82ddfd1dd559..8814ef0521ab 100644 --- a/WordPress/src/main/res/values-ro/strings.xml +++ b/WordPress/src/main/res/values-ro/strings.xml @@ -1,11 +1,75 @@ + Bine ai venit la aplicația Jetpack. Acum poți să dezinstalezi aplicația WordPress. + Recomandăm <b>să dezinstalezi aplicația WordPress</b> pe dispozitivul tău ca să eviți conflictele. + Se pare că încă ai instalată aplicația WordPress. + Nu mai ai nevoie de aplicația WordPress pe dispozitivul tău + Recomandăm <b>să dezinstalezi aplicația WordPress</b> pe dispozitivul tău ca să eviți conflictele. + Confidențialitate și evaluare + Înlătură blocurile + Adaugă pagini pe site-ul tău + Ascunde asta + Rezervă-ți partea ta pe web cu o adresă de site ușor de găsit, partajat și urmărit. + Cu un domeniu personalizat, ai o identitate proprie online + Ca să folosești reamintirile pentru publicare, trebuie să activezi notificările imediate. + Descrie scopul imaginii. Lasă gol dacă imaginea este doar decorativă. + Creează o altă pagină + Manual + Dinamic + Setări redare + Culoare pentru bara de redare + Începe cu aranjamente personalizate, prietenoase pentru dispozitivele mobile + Continuă cu un subdomeniu + Cumpără domeniul + Fotografii și videouri plus muzică și audio + Muzică și audio + Fotografii și videouri + %s are nevoie de permisiuni pentru a-ți accesa fișierele audio + %s are nevoie de permisiuni pentru a-ți accesa videourile + %s are nevoie de permisiuni pentru a-ți accesa fotografiile + %s are nevoie de permisiuni pentru a-ți accesa fotografiile și videourile + %s are nevoie de permisiuni pentru a-ți accesa fotografiile și videourile plus muzica și fișierele audio + Activează notificările + Activează notificările imediate + Mergi la Setări → Notificări → Setări aplicație și activează %1$s ca să primești notificări imediat. + Corectează + Respinge avertizarea pentru permisiuni notificări. + Pentru a vedea notificările, trebuie să deschizi aplicația. + Notificările imediate sunt dezactivate + Notificările imediate sunt dezactivate. + <b>%1$s</b> folosește %2$s module Jetpack individuale + Site-urile cu module Jetpack individuale nu sunt acceptate de aplicația WordPress. + <b>%1$s</b> folosește modulul <b>%2$s</b> + <b>%1$s</b> folosește module Jetpack individuale care nu sunt acceptate de aplicația WordPress. + <b>%1$s</b> folosește modulul <b>%2$s</b> care nu este acceptat de aplicația WordPress. + Nu pot să accesez unele dintre site-urile tale + Nu pot să accesez unul dintre site-urile tale + Te rog să comuți la aplicația Jetpack, te vom ghida la conectarea integrală a modulului Jetpack ca să poți să folosești acest site cu aplicația. + Comută la aplicația Jetpack + %1$s folosește %2$s care nu suportă încă toate funcționalitățile aplicației. \n\nPentru a folosi aplicația cu acest site, te rog să instalezi %3$s. + Acest site + %1$s folosește %2$s, care încă nu suportă toate funcționalitățile aplicației. Te rog instalează %3$s. + %1$s folosește %2$s, care încă nu suportă toate funcționalitățile aplicației. Te rog instalează %3$s. + Statistici, Cititor, Notificări și alte funcționalități propulsate de Jetpack au fost înlăturate din aplicația WordPress. Pot fi găsite acum doar în aplicația Jetpack. + Comutarea este gratuită și durează numai un minut. + Mutăm funcționalitatea în aplicația Jetpack peste câteva zile. + Acum, după ce ai instalat Jetpack, trebuie doar să te pregătim pentru inițializare. Nu va dura decât un minut. + Gata + Inițializează + Conținut + Trafic + Administrează + Administrare WP + %s s-a mutat în aplicația Jetpack. + %s s-au mutat în aplicația Jetpack. + Comută la aplicația Jetpack + Află mai multe la Jetpack.com Urmărești performanța, pornești și oprești Blaze în orice moment. Promovează acest articol Promovează această pagină @@ -17,7 +81,6 @@ Language: ro Îți mulțumim că ai comutat la aplicația Jetpack! Vezi întrebările frecvente pentru răspunsuri la întrebările pe care le ai. Ajutor - %s/an Cea mai bună alternativă Recomandat Reducere @@ -39,24 +102,22 @@ Language: ro modulul %1$s module individuale Jetpack modulul complet Jetpack - Icon eroare - A fost o problemă - Jetpack nu a putut fi instalat în acest moment. - Reîncearcă - Contactează suportul - Acest site folosește un modul individual, care încă nu suportă toate funcționalitățile aplicației. Te rog instalează modulul complet Jetpack. + Icon eroare + A fost o problemă + Jetpack nu a putut fi instalat în acest moment. + Reîncearcă + Contactează suportul Este disponibil un singur site, așadar nu poți modifica site-ul principal. Te rog instalează modulul complet Jetpack Promovează cu Blaze - Icon Jetpack - Instalează Jetpack - Datele de conectare ale site-ului tău web nu vor fi stocate și sunt folosite numai în scopul de a instala Jetpack. - Continuă - Instalez Jetpack - Instalez Jetpack pe site-ul tău. Poate dura câteva minute până la finalizare. - Jetpack a fost instalat - Gata pentru a folosi acest site în aplicație. - Gata + Icon Jetpack + Instalează Jetpack + Continuă + Instalez Jetpack + Instalez Jetpack pe site-ul tău. Poate dura câteva minute până la finalizare. + Jetpack a fost instalat + Gata pentru a folosi acest site în aplicație. + Datele de conectare ale site-ului tău web nu vor fi stocate și sunt folosite numai cu scopul de a instala Jetpack. Deblochează întregul potențial al site-ului tău. Cu Jetpack, ai statistici, notificări și multe altele. Urmărești creșterea traficului cu informații generale utile și statistici comprehensive. Găsește, urmărește site-urile și comunitățile preferate și partajează conținutul tău. @@ -151,11 +212,8 @@ Language: ro Deschide legăturile în Jetpack Cu aplicația Jetpack, primești notificări Cu aplicația Jetpack, poți să urmărești orice site - Te rog <b>șterge aplicația WordPress</b> pentru a evita conflictele de date. Cu aplicația Jetpack, îți vezi statisticile Nu mă pot conecta la internet. - Nu mai ai nevoie de aplicația WordPress - Se pare că încă ai aplicația WordPress instalată. Îți recomandăm să ștergi aplicația WordPress pentru a evita conflictele de date. Ne pare rău, dar ceva nu a mers conform planului. Datele tale sunt în siguranță, dar nu le putem transfera momentan. Te rog contactează suportul sau încearcă din nou mai târziu. Te rog verifică dacă ai o conexiunea la rețea care funcționează și încearcă din nou. @@ -164,14 +222,12 @@ Language: ro Încearcă din nou Înlătură iconul aplicației WordPress Of nu, ceva nu a mers bine… - Te rog <b>șterge aplicația WordPress</b> pentru a evita conflictele de date. Am transferat toate datele și setările tale. Toate sunt acolo, exact unde erau. Vei primi aceleași notificări, dar acum vor veni din aplicația Jetpack. Îți mulțumim că ai comutat la Jetpack! Vom dezactiva notificările din aplicația WordPress. Suport Centru de ajutor WordPress - Te rog șterge aplicația WordPress dezactivează notificările WordPress Permite aplicației să dezactiveze notificările WordPress. Notificările vin acum de la Jetpack @@ -529,7 +585,7 @@ Language: ro Scrie primul tău articol Fără titlu Articole programate în viitor - Lucrează într-un articol ciornă + Lucrează la un articol ciornă <span style=\"color:#008000;\">Gratuit în primul an </span><span style=\"color:#50575e;\"><s>%s/an</s></span> Domeniile tale vor fi redirecționate la site-ul tău %s Creează o legătură @@ -750,6 +806,7 @@ Language: ro GIF Unu Nu este disponibilă nicio previzualizare + Adaugă titlu Culoare text Distanțare Reprezentativ @@ -757,6 +814,7 @@ Language: ro Creează înglobare URL personalizat Coloana %d + Mai mult Descrie pe scurt legătura pentru a ajuta utilizatorii care folosesc cititorul de ecran Adaugă blocuri Nu am găsit niciun site Jetpack @@ -1173,7 +1231,6 @@ Language: ro Prezentăm Articole narațiune A fost creată o pagină goală A fost creată o pagină - %1$s a fost refuzat accesul la fotografiile tale. Pentru a corecta asta, editează-ți permisiunile și pornește %2$s și %3$s. Inserarea media a eșuat. Inserarea media a eșuat: %s Alege din biblioteca Media din WordPress @@ -1653,6 +1710,7 @@ Language: ro Adaugă URL A apărut o eroare necunoscută. Te rog încearcă din nou. Adaugă text alternativ + Adaugă o descriere „Lista a fost încărcată și are %1$d elemente.” Pentru a salva un articol în listă atinge butonul Adaugă în articole salvate. Notificări @@ -1849,7 +1907,6 @@ Language: ro Încarc ciorna Ciorne A apărut o eroare la restaurarea articolului - Acum, după ce ai instalat Jetpack, trebuie doar să te pregătim pentru inițializare. Nu va dura decât un minut. Înregistrează domeniul Nicio sugestie găsită Introdu un cuvânt cheie pentru mai multe idei @@ -2123,18 +2180,9 @@ Language: ro Adaugă subiecte aici pentru a găsi articole cu subiectele tale preferate Nu urmărești niciun subiect Nu ai niciun site - Instalează Jetpack Jetpack Întrebări frecvente Jetpack - Jetpack instalat - Instalez Jetpack - A fost o problemă - Reîncearcă Autentifică-te în contul WordPress.com pe care l-ai folosit pentru a conecta Jetpack. - Inițializează - Jetpack nu a putut fi instalat în acest moment. - Instalez Jetpack pe site-ul tău. Poate dura câteva minute până la finalizare. - Datele de conectare ale site-ului tău web nu vor fi stocate și vor fi folosite numai pentru instalarea Jetpack. Pentru a folosi Statisticile pe site-ul tău WordPress va trebui să instalezi modulul Jetpack. Nu s-a potrivit nicio temă cu căutarea ta Ce vrei să găsești? @@ -2686,7 +2734,7 @@ Language: ro Documente Imagini Toată - %1$s a fost refuzat accesul la fotografiile tale. Pentru a corecta asta, editează-ți permisiunile și pornește %2$s. + Accesul la fișierele tale Media a fost refuzat pentru %1$s. Pentru a corecta asta, editează-ți permisiunile și activează %2$s. Vezi comentariile Calitatea videourilor. Valorile mai mari înseamnă videouri de calitate mai bună. Redimensionează videourile în articole la această dimensiune diff --git a/WordPress/src/main/res/values-ru/strings.xml b/WordPress/src/main/res/values-ru/strings.xml index 0eb34aa7148a..11ddd26eb0f4 100644 --- a/WordPress/src/main/res/values-ru/strings.xml +++ b/WordPress/src/main/res/values-ru/strings.xml @@ -1,11 +1,89 @@ + Удалить блоки + Конфиденциальность и рейтинг + Настройки воспроизведения + Цвет панели воспроизведения + Вручную + Динамический + Опишите назначение изображения. Оставьте поле пустым, если элемент декоративный. + Начните с макетов для мобильных устройств + Создать еще страницу + Добавление новых страниц на сайт + Скрыть это + Займите себе место в Интернете: зарегистрируйте адрес сайта, который будет легко запомнить и порекомендовать друзьям. + Чтобы выделиться, вам нужен уникальный домен + Для использования напоминаний следует включить push-уведомления. + Включить push-уведомления + Продолжить с поддоменом + Купить домен + Фотографии и видеозаписи, музыка и аудиозаписи + Музыка и аудиозаписи + Фотографии и видео + %s нужно разрешение для доступа к вашим аудиозаписям. + %s нужно разрешение для доступа к вашим видеозаписям. + %s нужно разрешение для доступа к вашим фотографиям. + %s нужно разрешение для доступа к вашим фотографиям и видеозаписям. + %s нужно разрешение для доступа к вашей музыке, аудиозаписям, фотографиям и видеозаписям. + Включить уведомления + Перейдите в \"Настройки\" &rarr; \"Уведомления\" &rarr; \"Настройки приложения\" и включите %1$s, чтобы сразу получать уведомления. + Для просмотра уведомлений нужно открыть приложение. + Push-уведомления отключены + Push-уведомления отключены + Скрывать предупреждение о разрешении на уведомления. + Исправить + На <b>%1$s</b> используются отдельные плагины Jetpack (%2$s) + На <b>%1$s</b> используется плагин <b>%2$s</b> + Приложение WordPress не поддерживает сайты с отдельными плагинами Jetpack. + На <b>%1$s</b> используются отдельные плагины Jetpack, которые не поддерживаются приложением WordPress. + На <b>%1$s</b> используется плагин <b>%2$s</b>, который не поддерживается приложением WordPress. + Не удается получить доступ к некоторым сайтам + Не удается получить доступ к сайту + Перейдите в приложение Jetpack и следуйте пошаговым инструкциям по подключению полнофункционального плагина Jetpack, который позволит использовать этот сайт в приложении. + Перейти в приложение Jetpack + На %1$s используется плагин %2$s, который пока поддерживает не все функции приложения.\n\nУстановите %3$s, чтобы использовать приложение с этим сайтом. + Этот сайт + На %1$s используется плагин %2$s, который пока поддерживает не все функции приложения. Установите %3$s. + На %1$s используется плагин %2$s, который пока поддерживает не все функции приложения. Установите %3$s. + Через несколько дней мы переходим на приложение Jetpack. + Переход бесплатен и занимает всего минуту. + Статистика, Обозреватель, уведомления и другие функции на базе Jetpack удалены из приложения WordPress и теперь будут только в приложении Jetpack. + Дополнительные сведения см. на Jetpack.com + Перейти в новое приложение Jetpack + %s перемещены в приложение Jetpack. + %s перемещен в приложение Jetpack. + Консоль + Управление + Посещаемость + Содержимое + Настроить + Готово + Jetpack установлен, осталось его настроить. Это займет всего минуту. + Выделить запись с помощью Blaze сейчас + Выделить эту страницу с помощью Blaze + Выделить эту запись с помощью Blaze + Отслеживайте производительность, запускайте и останавливайте Blaze в любое время. + Содержимое вашего сайта появится на огромном количестве сайтов WordPress и Tumblr. + Продвигайте любую запись или страницу за несколько минут всего за несколько долларов в день. + Повысьте посещаемость своего сайта с помощью Blaze + Blaze + Этот домен уже зарегистрирован + Распродажа + Рекомендуемые + Лучшая альтернатива + Справка + Ищите ответы на типовые вопросы в разделе \"Часто задаваемые вопросы\". + Благодарим за переход на приложение Jetpack! + Журналы + Билеты + Бесплатно + Справка Меню \"Блоки\" Свернуть Демонстрируйте свою работу на миллионах сайтов. @@ -18,26 +96,24 @@ Language: ru Полнофункциональный плагин Jetpack Отдельные плагины Jetpack плагин %1$s - На %1$s используется плагин %2$s, который пока поддерживает не все функции приложения.\n\nУстановите %3$s, чтобы использовать приложение с этим сайтом. + На %1$s используется плагин %2$s, который пока поддерживает не все функции приложения.\n\nУстановите %3$s, чтобы использовать приложение с этим сайтом. Установите полнофункциональный плагин Jetpack Доступен только один сайт, поэтому изменить основной сайт невозможно. - На этом сайте используется отдельный плагин, который пока поддерживает не все функции приложения. Установите полнофункциональный плагин Jetpack. - Обратитесь в службу поддержки - Повторить - Невозможно установить Jetpack в настоящий момент. - Возникла проблема - Значок ошибки - Готово - Сайт готов к использованию в приложении. - Jetpack установлен - Jetpack устанавливается на вашем сайте. Это может занять несколько минут. - Выполняется установка Jetpack - Продолжить - Учётные данные вашего сайта не будут сохранены и будут использованы только для безопасной установки Jetpack. - Установить Jetpack - Значок Jetpack + Обратитесь в службу поддержки + Повторить + Невозможно установить Jetpack в настоящий момент. + Возникла проблема + Значок ошибки + Сайт готов к использованию в приложении. + Jetpack установлен + Jetpack устанавливается на вашем сайте. Это может занять несколько минут. + Выполняется установка Jetpack + Продолжить + Учётные данные вашего сайта не будут сохранены и будут использованы только для безопасной установки Jetpack. + Установить Jetpack + Значок Jetpack Продвигайте содержимое с помощью Blaze - Используйте возможности сайта максимально эффективно. Установите Jetpack и получите доступ к статистике, уведомлениям и многим другим функциям. + Используйте возможности сайта максимально эффективно. Установите Jetpack и получите доступ к статистике, уведомлениям и другим функциям. На вашем сайте есть плагин Jetpack. Мобильное приложение Jetpack создано работать в паре с плагином Jetpack. Перейдите в приложение, чтобы использовать статистику, уведомления, Чтиво и другие возможности. Получайте уведомления о новых комментариях, отметках «Нравится», просмотрах и других событиях. @@ -132,9 +208,6 @@ Language: ru Открывать ссылки в Jetpack Нужна помощь? Понятно - <b>Удалите приложение WordPress</b>, чтобы <br>избежать конфликтов данных. - Кажется, у вас всё ещё установлено приложение WordPress. Рекомендуем удалить приложение WordPress, чтобы избежать конфликтов данных. - Вам больше не требуется приложение WordPress Не удаётся перенести ваши данные и настройки без подключения к сети. Убедитесь, что сетевое подключение работает, и повторите попытку. Не удаётся подключиться к Интернету. @@ -144,13 +217,11 @@ Language: ru Повторить попытку Завершить Удаление значка приложения WordPress - <b>Удалите приложение WordPress</b>, чтобы <br>избежать конфликтов данных. Все данные и настройки перенесены. Всё осталось по-прежнему. Благодарим за переход в Jetpack! Уведомления из приложения WordPress будут отключены. Вам будут приходить те же уведомления, но теперь из приложения Jetpack. Теперь уведомления будут поступать из Jetpack - Удалите приложение WordPress Справочный центр WordPress Поддержка Позволяет отключить уведомления WordPress в приложении. @@ -729,6 +800,7 @@ Language: ru Повторить GIF Один + Добавить заголовок Предварительный просмотр недоступен Цвет текста Отступ @@ -737,6 +809,7 @@ Language: ru Произвольный URL Создать встроенный объект Столбец %d + Далее Кратко опишите ссылку, чтобы помочь пользователям с программой чтения с экрана Добавить блоки Нет сайтов с Jetpack @@ -1153,7 +1226,6 @@ Language: ru Представляем записи-истории Создана пустая страница Страница создана - %1$s было отказано в доступе к вашим фотографиям. Чтобы исправить это, измените настройки доступа и включите %2$s и %3$s. Вставка мультимедиа не удалась. Вставка мультимедиа не удалась: %s Выберите из медиатеки WordPress @@ -1633,6 +1705,7 @@ Language: ru ДОБАВИТЬ ИЗОБРАЖЕНИЕ ИЛИ ВИДЕО ДОБАВИТЬ ИЗОБРАЖЕНИЕ ДОБАВИТЬ БЛОК ЗДЕСЬ + Добавить описание Нажмите на кнопку добавления в сохраненные записи для сохранения записи в ваш список. \"Загружен список из %1$d элементов.\" Уведомления @@ -1838,7 +1911,6 @@ Language: ru Напишите ключевое слово для получения предложений Нет предложений Зарегистрировать домен - Теперь, когда Jetpack установлен, нам нужно настроить, это займет всего минуту. Удалить с вкладки Вниз Вверх @@ -2104,15 +2176,6 @@ Language: ru Нет подписок на темы Добавьте любимые темы здесь, чтобы найти по ним записи Войдите в учетную запись WordPress.com, с которой был подключен Jetpack. - Повторить - Продолжить установку - Сейчас Jetpack не может быть установлен. - Возникла проблема - Jetpack установлен - Установка Jetpack на сайт. Это может занять пару минут. - Устанавливается Jetpack - Данные для входа на ваш сайт не будут сохранены и будут использоваться только для установки Jetpack. - Установить Jetpack Jetpack Jetpack FAQ Для использования Статистики на вашем сайте WordPress требуется установить плагин Jetpack. @@ -2666,7 +2729,7 @@ Language: ru Документы Изображения Все - %1$s было отказано в доступе к вашим фотографиям. Чтобы исправить это, измените настройки доступа и запустите %2$s. + %1$s было отказано в доступе к вашим медиафайлам. Чтобы исправить это, измените настройки доступа и включите %2$s. Посмотреть комментарии Качество видео. Чем выше значение, тем выше качество видео. Приведение видео в записях к этому размеру diff --git a/WordPress/src/main/res/values-sk/strings.xml b/WordPress/src/main/res/values-sk/strings.xml index 60be996dcdc6..c6d24997e2c9 100644 --- a/WordPress/src/main/res/values-sk/strings.xml +++ b/WordPress/src/main/res/values-sk/strings.xml @@ -1,6 +1,6 @@ + Hiqi blloqet + Privatësi dhe Vlerësim + Rregullime Luajtjeje + Ngjyrë Shtylle Luajtjeje + Dorazi + Dinamike + Përshkruani qëllimin e figurës. Lëreni të zbrazët, në qoftë dekorative. + Fillojani me skema të qepura me porosi, të përshtatshme për celular + Krijoni Një Faqe Tjetër + Shtoni Faqe te sajti juaj + Fshihe këtë + Afirmoni pretendimin tuaj për qoshen tuaj në internet, me një adresë sajti që është kollaj të gjendet, jepet dhe ndiqet. + Bëhuni zot i identitetin tuaj internetor, përmes një përkatësie vetjake + Që të përdorni kujtues blogimi, do t’ju duhet të aktivizoni njoftime push. + Aktivizo njoftime push + Vazhdo me nënpërkatësi + Blini përkatësi + Foto dhe video & Muzikë dhe audio + Muzikë dhe audio + Foto dhe video + %s lyp leje hyrjeje te pjesët tuaja audio + %s lyp leje hyrjeje te videot tuaja + %s lyp leje hyrjeje te fotot tuaja + %s lyp leje hyrjeje te fotot dhe videot tuaja + %s lyp leje hyrjeje te muzika, pjesët audio, fotot dhe videot tuaja + Aktivizo njoftime + Kaloni te Rregullime → Njoftime → Rregullime Aplikacioni dhe aktivizoni %1$s, që të njoftoheni menjëherë. + Do t’ju duhet të hapni aplikacionin, që të shihni njoftime. + Njoftimet push janë çaktivizuar + Njoftimet Push janë çaktivizuar. + Mos merr parasysh sinjalizim mbi lejet. + Ndreqe + <b>%1$s</b> po përdor %2$s shtojca Jetpack individuale + <b>%1$s</b> po përdor shtojcën <b>%2$s</b> + Sajtet me shtojca Jetpack individuale nuk mbulohen nga aplikacioni WordPress. + <b>%1$s</b> po përdor shtojca Jetpack individuale, çka nuk mbulohet nga aplikacioni WordPress. + <b>%1$s</b> po përdor shtojcën <b>%2$s</b>, e cila nuk mbulohet nga aplikacioni WordPress. + S’arrihet të hyhet në disa nga sajtet tuaj + S’arrihet të hyhet në një nga sajtet tuaj + Ju lutemi, kaloni te aplikacioni Jetpack, prej nga do t’ju drejtojmë nëpër lidhjen e shtojcës së plotë Jetpack, për të përdorur këtë sajt me aplikacionin. + Kaloni te aplikacioni Jetpack + %1$s po përdor %2$s, i cili nuk i mbulon ende krejt veçoritë e aplikacionit.\n\nJu lutemi, që të përdorni aplikacionin me këtë sajt, instaloni %3$s. + Këtë sajt + %1$s po përdor %2$s, të cilat nuk i mbulojnë ende krejt veçoritë e aplikacionit. Ju lutemi, instaloni %3$s. + %1$s po përdor %2$s, i cili nuk i mbulon ende krejt veçoritë e aplikacionit. Ju lutemi, instaloni %3$s. + Kalim në aplikacionin Jetpack pas pak ditësh. + Kalimi është falas dhe do vetëm një minutë. + Statistikat, Lexuesi, Njoftimet dhe veçori të tjera të bazuara në Jetpack tani janë hequr prej aplikacionit WordPress dhe tani mund të gjenden vetëm në aplikacionin Jetpack. + Mësoni më tepër, te Jetpack.com + Kaloni te aplikacioni Jetpack + %s kanë kaluar te aplikacioni Jetpack. + %s ka kaluar te aplikacioni Jetpack. + Përgjegjës WP-je + Administrojini + Trafik + Lëndë + Ujdise + U bë + Tani që Jetpack-u është instaluar, na duhet vetëm t’ju bëjmë gati. Kjo do të hajë vetëm një minutë. + Promovoni një Postim në Blaze që tani + Promovojeni këtë Faqe në Blaze + Promovojeni këtë Postim në Blaze + Ndiqni punimin, nisni dhe ndalni Blaze-in tuaj në çfarëdo kohe. + Lënda juaj do të shfaqet në miliona sajte WordPress dhe Tumblr. + Promovoni çfarëdo postimi ose faqeje, brenda pak minutash, për vetëm pak dollarë në ditë. + Shpini më tepër trafik te sajti juaj, përmes Blaze-it + Kjo përkatësi është e regjistruar tashmë + Ulje çmimesh + E rekomanduar + Alternativa Më e Mirë + Ndihmë + Për përgjigje pyetjesh të rëndomta që mund të keni, shihni PBR-të tona. + Faleminderit që kaloni te aplikacioni Jetpack! + Regjistra + Bileta + Falas + Ndihmë Menu blloqesh Fshihe këtë Shfaqeni punën tuaj nëpër miliona sajte. @@ -18,26 +95,24 @@ Language: sq_AL shtojcë Jetpack e plotë shtojca individuale Jetpack shtojca %1$s - %1$s po përdor %2$s, që s’mbulon ende krejt veçoritë e aplikacionit.\n\nQë të përdorni aplikacionin në këtë sajt, ju lutemi, instaloni %3$s. + %1$s po përdor %2$s, që s’mbulon ende krejt veçoritë e aplikacionit.\n\nQë të përdorni aplikacionin në këtë sajt, ju lutemi, instaloni %3$s. Ju lutemi, instaloni të plotë shtojcën Jetpack Ka vetëm një sajt, ndaj s’mund të ndryshoni sajtin tuaj parësor. - Ky sajt përdor një shtojcë individuale, e cila s’mbulon ende krejt veçoritë e aplikacionit. Ju lutemi, instaloni të plotë shtojcën Jetpack. - Lidhuni me Asistencën - Riprovoni - Jetpack-u s’u instalua dot këtë herë. - Pati një problem - Ikonë gabimi - U bë - Gati për përdorim të këtij sajti me aplikacionin. - Jetpack-u u instalua - Po instalohet Jetpack-u në sajtin tuaj. Kjo mund të marrë ca minuta për t’u plotësuar. - Instalim i Jetpack-ut - Vazhdo - Kredencialet tuaja për në sajt nuk do të depozitohen dhe përdoren vetëm për qëllimin e instalimit të Jetpack-ut. - Instaloni Jetpack-un - Ikonë Jetpack + Lidhuni me Asistencën + Riprovo + Jetpack-u s’u instalua dot këtë herë. + Pati një problem + Ikonë gabimi + Gati për përdorim të këtij sajti me aplikacionin. + Jetpack-u u instalua + Po instalohet Jetpack-u në sajtin tuaj. Kjo mund të marrë ca minuta për t’u plotësuar. + Instalim i Jetpack-ut + Vazhdo + Kredencialet tuaja për në sajt nuk do të depozitohen dhe përdoren vetëm për qëllimin e instalimit të Jetpack-ut. + Instaloni Jetpack-un + Ikonë Jetpack Promovojeni me Blaze - Çlironi potencialin e plotë të sajtit tuaj. Merrni statistika, njoftime, etj me Jetpack-un. + Çlironi potencialin e plotë të sajtit tuaj. Merrni statistika, njoftime, etj, me Jetpack-un. Sajti juaj e ka shtojcën Jetpack Aplikacioni Jetpack për celular është menduar të punojë tok me shtojcën Jetpack. Kaloni që tani në të, që të mund të përdorni statistikat, njoftimet, lexuesin, etj. Merrni njoftime për komente, pëlqime, parje të reja, etj. @@ -120,6 +195,7 @@ Language: sq_AL Jetpack-u furnizon statistika, njoftime, etj, për t’ju ndihmuar të ngrini dhe fuqizoni sajtin tuaj WordPress që keni ëndërruar. Krijoni një sajt të ri WordPress me aplikacionin Jetpack lidhje web + lidhje URI Që të vazhdoni të merrni në pajisjen tuaj njoftime të atypëratyshme, kaloni te aplikacioni Jetpack. Që të gjeni, ndiqni dhe pëlqeni krejt sajtet dhe postimet tuaj të parapëlqyer me Lexuesin, kaloni te aplikacioni Jetpack. Që të shihni shtim trafiku në sajtin tuaj, përmes statistikash dhe prirjesh, kaloni te aplikacioni Jetpack. @@ -131,9 +207,6 @@ Language: sq_AL Lidhjet hapi në Jetpack Ju duhet ndihmë? E kuptova - Ju lutemi, <b>fshini aplikacionin WordPress</b>, që të shmangen përplasje të dhënash. - Duket sikur keni ende të instaluar aplikacionin WordPress. Rekomandojmë ta fshini aplikacionin WordPress, për të shmangur përplasje të dhënash. - S’ju duhet më aplikacioni WordPress S’jeni në gjendje të shpërngulim të dhënat dhe rregullimet tuaja pa një lidhje në rrjet. JU lutemi, kontrolloni për t’u siguruar se lidhja juaj në rrjet funksionon dhe riprovoni. S’arrihet të lidhet në internet. @@ -143,13 +216,11 @@ Language: sq_AL Riprovoni Përfundoje Hiqni ikonë aplikacioni WordPress - Ju lutemi, <b>fshini aplikacionin WordPress</b>, që të shmangen përplasje të dhënash. Kemi shpërngulur krejt të dhënat dhe rregullimet tuaja. Gjithçka është mu atje ku e latë. Faleminderit që kaloni në Jetpack! Do t’i çaktivizojmë njoftimet prej aplikacionit WordPress. Do të merrni të njëjtat njoftime, por tani do të vijnë nga aplikacioni Jetpack. Njoftimet tani vijnë prej Jetpack-ut - Ju lutemi, fshini aplikacionin WordPress Qendër ndihme WordPress Asistencë E lejon aplikacionin të çaktivizojë njoftimet nga WordPress-i. @@ -722,6 +793,7 @@ Language: sq_AL Riprovoni GIF Një + Shtoni titull S’ka paraparje gati Ngjyrë teksti Mbushje @@ -730,6 +802,7 @@ Language: sq_AL URL Vetjake Krijo trupëzim Shtylla %d + Më tepër Përshkruajeni shkurt lidhjen, që të ndihmoni përdoruesin e lexuesit të ekranit Shtoni blloqe S’u gjetën sajte Jetpack @@ -1145,7 +1218,6 @@ Language: sq_AL Ju Paraqesim Postime Shkrimesh U krijua faqe e zbrazët Faqja u krijua - %1$s-it iu mohua hyrja te fotot tuaj. Që të ndreqet kjo, përpunoni lejet tuaj dhe aktivizoni %2$s dhe %3$s. Dështoi futje medie. Dështoi futje medie: %s Zgjidhni prej Mediatekës WordPress @@ -1625,6 +1697,7 @@ Language: sq_AL SHTONI FIGURË OSE VIDEO SHTONI FIGURË SHTONI BLLOK KËTU + Shtoni përshkrim Prekni butonin “Shtoje te Postime të Ruajtur” që të ruhet një postim te lista juaj. \"Lista është ngarkuar me %1$d zëra.\" Njoftime @@ -1830,7 +1903,6 @@ Language: sq_AL Për më tepër ide, shtypni një fjalëkyç S’u gjetën sugjerime Regjistroni Përkatësi - Tani që Jetpack-u është instaluar, na duhet vetëm t’ju bëjmë gati. Kjo do të hajë vetëm një minutë. Hiqe prej tendencave Ule poshtë Ngjite sipër @@ -2096,15 +2168,6 @@ Language: sq_AL Pa tema të ndjekura Shtoni këtu tema që të gjeni postime rreth temave tuaja të parapëlqyera Bëni hyrjen te llogaria WordPress.com që përdorët për t’ia përshoqëruar Jetpack-ut. - Riprovoni - Ujdise - Jetpack-u s’u instalua dot këtë herë. - Pati një problem - Jetpack-u u instalua - Po instalohet Jetpack-u në sajtin tuaj. Kjo mund të dojë ca minuta të plotësohet. - Instalim i Jetpack-ut - Kredencialet e hyrjes në sajtin tuaj nuk do të depozitohen, dhe përdoren vetëm për qëllimin e instalimit të Jetpack-ut. - Instaloni Jetpack-un Jetpack PBR për Jetpack Që të përdorni Statistikat te sajti juaj WordPress, do t’ju duhet të instaloni shtojcën Jetpack. @@ -2658,7 +2721,7 @@ Language: sq_AL Dokumente Figura Krejt - %1$s-it iu mohua hyrja te fotot tuaj. Që të ndreqet kjo, përpunoni lejet tuaj dhe aktivizoni %2$s. + %1$s-it iu mohua hyrja te kartelat tuaja media. Që të ndreqet kjo, përpunoni lejet tuaj dhe aktivizoni %2$s. Shihni komentet Cilësi videosh. Vlera më të mëdha do të thotë cilësi më e lartë videosh. Ripërmason video në postime sa kjo madhësi diff --git a/WordPress/src/main/res/values-sr/strings.xml b/WordPress/src/main/res/values-sr/strings.xml index 56f2761b23d7..d62cee284535 100644 --- a/WordPress/src/main/res/values-sr/strings.xml +++ b/WordPress/src/main/res/values-sr/strings.xml @@ -1,6 +1,6 @@ + Vi rekommenderar att du <b>avinstallerar WordPress-appen</b> på din enhet för att undvika datakonflikter. + Det ser ut som att du fortfarande har WordPress-appen installerad. + Du behöver inte längre WordPress-appen på din enhet + Vi rekommenderar att du <b>avinstallerar WordPress-appen</b> på din enhet för att undvika datakonflikter. + Välkommen till Jetpack-appen. Du kan avinstallera WordPress-appen. + Ta bort block + Integritet och klassificering + Uppspelningsinställningar + Färg på uppspelningsfältet + Manuellt + Dynamisk + Beskriv syftet med bilden. Lämna blankt om det är för dekorativa ändamål. + Kom igång med skräddarsydda, mobilvänliga layouter + Skapa en ny sida + Lägg till sidor på din webbplats + Dölj detta + Gör anspråk på din del av webben med en webbadress som är lätt att hitta, dela och följa. + Äg din onlineidentitet med en anpassad domän + För att kunna använda bloggningspåminnelser behöver du aktivera push-meddelanden. + Slå på push-meddelanden + Fortsätt med underdomän + Köp domän + Bilder och videoklipp & Musik och ljud + Musik och ljud + Foton och videoklipp + %s behöver behörighet för att öppna dina ljudfiler + %s behöver behörighet för att öppna dina videoklipp + %s behöver behörighet för att öppna dina foton + %s behöver behörighet för att öppna dina foton och videoklipp + %s behöver behörighet för att öppna din musik, dina ljudfiler, foton och videoklipp + Slå på aviseringar + Navigera till Inställningar &rarr; Aviseringar &rarr; Appinställningar och slå på %1$s för att få omedelbar information. + Du måste öppna appen för att se aviseringar. + Push-notiser är avstängda + Push-notiser är avstängda. + Avfärda varning för behörighetsavisering. + Åtgärda + <b>%1$s</b> använder %2$s enskilda Jetpack-tillägg + <b>%1$s</b> använder <b>%2$s</b>-tillägget + Webbplatser med enskilda Jetpack-tillägg stöds inte av WordPress-appen. + <b>%1$s</b> använder sig av enskilda Jetpack-tillägg vilket inte stöds av WordPress-appen. + <b>%1$s</b> använder sig av <b>%2$s</b>-tillägget vilket inte stöds av WordPress-appen. + Det går inte att komma åt några av dina webbplatser + Det går inte att komma åt en av dina webbplatser + Gå till Jetpack-appen där vi guidar dig igenom hur du ansluter det fullständiga Jetpack-tillägget så att du kan använda den här webbplatsen med appen. + Byt till Jetpack-appen + %1$s använder %2$s, som inte stöder alla funktioner i appen än.\n\nInstallera %3$s för att använda appen med den här webbplatsen. + Denna webbplats + %1$sanvänder %2$s, som inte stöder alla funktioner i appen än. Installera %3$s. + %1$s använder %2$s, som inte stöder alla funktioner i appen än. Installera %3$s. + Flyttar till Jetpack-appen om några dagar. + Att byta är gratis och tar bara en minut. + Statistik, Tittaren, Notiser och andra Jetpack-drivna funktioner har tagits bort från WordPress-appen och finns nu bara i Jetpack-appen. + Lär dig mer på Jetpack.com + Byt till Jetpack-appen + %s har flyttat till Jetpack-appen. + %s har flyttat till Jetpack-appen. + WP-Adminpanel + Hantera + Trafik + Innehåll + Ställ in + Klar + Nu när Jetpack är installerat behöver vi bara konfigurera det. Detta tar bara en minut. + Marknadsför ett inlägg nu + Marknadsför den här sidan + Marknadsför det här inlägget + Håll koll på prestandan samt start och stoppa din marknadsföring när som helst. + Ditt innehåll kommer att visas på miljontals WordPress- och Tumblr-webbplatser. + Marknadsför valfritt inlägg eller valfri sida på bara några minuter, för endast några kronor om dagen. + Få mer trafik till din webbplats med Blaze + Blaze + Denna domän är redan registrerad + Rea + Rekommenderat + Bästa alternativ + Hjälp + Se våra vanliga frågor för svar på vanliga frågor du kan ha. + Tack för att du byter till Jetpack-appen! + Loggar + Biljetter + Gratis + Hjälp Block-meny Dölj detta Visa ditt arbete på miljontals webbplatser. @@ -18,24 +101,22 @@ Language: sv_SE fullständigt Jetpack-tillägg enskilda Jetpack-tillägg tillägget %1$s - %1$sanvänder %2$s, som inte stöder alla funktioner i appen än.\n\nInstallera %3$s för att använda appen med den här webbplatsen. + %1$s använder %2$s, som inte stöder alla funktioner i appen än.\n\n Installera %3$s för att använda appen med denna webbplats. Installera det fullständiga Jetpack-tillägget Endast en webbplats är tillgänglig så du kan inte ändra din primära webbplats. - Den här webbplatsen använder ett enskilt tillägg, som inte stöder alla funktioner i appen än. Installera det fullständiga Jetpack-tillägget. - Kontakta supporten - Försök igen - Jetpack kunde inte installeras just nu. - Det var ett problem - Felikon - Klar - Redo att använda denna webbplats med appen. - Jetpack har installerats - Installerar Jetpack på din webbplats. Detta kan ta upp till några minuter att slutföra. - Installerar Jetpack - Fortsätt - Din webbplats autentiseringsuppgifter används bara för att installera Jetpack säkert och kommer inte att sparas - Installera Jetpack - Jetpack-ikon + Kontakta supporten + Försök igen + Jetpack kunde inte installeras just nu. + Det var ett problem + Felikon + Redo att använda denna webbplats med appen. + Jetpack har installerats + Installerar Jetpack på din webbplats. Detta kan ta upp till några minuter att slutföra. + Installerar Jetpack + Fortsätt + Din webbplats autentiseringsuppgifter används bara för att installera Jetpack säkert och kommer inte att sparas + Installera Jetpack + Jetpack-ikon Marknadsför med Blaze Lås upp din webbplats fulla potential. Få statistik, aviseringar och mer med Jetpack. Din webbplats har Jetpack-tillägget @@ -132,9 +213,6 @@ Language: sv_SE Öppna länkar i Jetpack Behöver du hjälp? Jag förstår - <b>Ta bort WordPress-appen</b> för att undvika datakonflikter. - Det verkar som att du fortfarande har WordPress-appen installerad. Vi rekommenderar att du tar bort WordPress-appen för att undvika datakonflikter. - Du behöver inte längre WordPress-appen Vi kan inte överföra dina data och inställningar utan en nätverksanslutning. Kontrollera att din internetanslutning fungerar och försök igen. Det går inte att ansluta till Internet. @@ -144,13 +222,11 @@ Language: sv_SE Försök igen Slutför Ta bort WordPress-appens ikon - <b>Ta bort WordPress-appen</b> för att undvika datakonflikter. Vi har flyttat över alla data och dina inställningar, så allt är precis som förut. Tack för att du bytt till Jetpack! Aviseringar från WordPress-appent kommer att stängas av. Du kommer att få samma aviseringar som tidigare, men nu från Jetpack-appen. Aviseringar kommer nu från Jetpack - Ta bort WordPress-appen WordPress hjälpcenter Support Tillåter att appen inaktiverar WordPress-aviseringar. @@ -729,6 +805,7 @@ Language: sv_SE Försök igen GIF Ett + Lägg till rubrik Ingen förhandsgranskning tillgänglig Textfärg Padding @@ -737,6 +814,7 @@ Language: sv_SE Anpassad URL Skapa inbäddning Kolumn %d + Mer Beskriv kortfattat länken för den som använder skärmläsare. Lägg till block Inga Jetpack-webbplatser hittades @@ -1153,7 +1231,6 @@ Language: sv_SE Lär känna berättelseinlägg En blank sida har skapats Sidan har skapats - %1$s nekades åtkomst till dina bilder. Korrigera genom att redigera dina rättigheter och aktivera %2$s och %3$s. Infogning av media misslyckades. Infogning av media misslyckades: %s Välj ur mediabiblioteket i WordPress @@ -1633,6 +1710,7 @@ Language: sv_SE LÄGG TILL BILD ELLER VIDEO LÄGG TILL BILD LÄGG TILL BLOCK HÄR + Lägg till beskrivning Tryck på knappen ”Lägg till i sparade inlägg” för att lägga till inlägget i din lista. ”Listan har lästs in med %1$d objekt.” Aviseringar @@ -1838,7 +1916,6 @@ Language: sv_SE Skriv in ett nyckelord för fler idéer Inga förslag hittades Registrera domän - Nu när Jetpack är installerat behöver vi hjälpa dig igång. Det tar bara någon minut. Ta bort från Insikter Flytta ned Flytta upp @@ -2104,15 +2181,6 @@ Language: sv_SE Inga följda ämnen Lägg till ämnen här för att hitta inlägg om dina favoritämnen Logga in på WordPress.com-kontot du använde för att ansluta Jetpack. - Försök igen - Konfigurera - Jetpack kunde inte installeras just nu. - Det var ett problem - Jetpack installerat - Installerar Jetpack på din webbplats. Detta kan ta några minuter att slutföra. - Installerar Jetpack - Dina webbplatsuppgifter kommer inte att lagras och används endast för att installera Jetpack. - Installera Jetpack Jetpack Jetpack FAQ För att använda statistik på din WordPress-webbplats måste du installera tillägget Jetpack. @@ -2666,7 +2734,7 @@ Language: sv_SE Dokument Bilder Allt - %1$s nekades åtkomst till dina bilder. Korrigera genom att redigera dina rättigheter och aktivera %2$s. + %1$s nekades åtkomst till dina mediefiler. För att åtgärda detta, redigera dina behörigheter och aktivera %2$s. Visa kommentarer Videokvalitet. Högre värde innebär video med högre kvalitet. Ändrar storleken på videoklipp i inlägg till denna storlek diff --git a/WordPress/src/main/res/values-tr/strings.xml b/WordPress/src/main/res/values-tr/strings.xml index 95c511bb8e45..be49b38e58ee 100644 --- a/WordPress/src/main/res/values-tr/strings.xml +++ b/WordPress/src/main/res/values-tr/strings.xml @@ -1,11 +1,70 @@ + Blokları kaldır + Gizlilik ve Derecelendirme + Oynatma Ayarları + Oynatma Çubuğu Rengi + Manuel + Dinamik + Görselin amacını açıklayın. Dekoratif amaçlıysa boş bırakın. + Sipariş üzerine oluşturulan mobil uyumlu düzenlerle başlayın + Başka Bir Sayfa Oluştur + Sitenize Sayfalar ekleyin + Bunu gizle + Bulması, paylaşması ve takip etmesi kolay bir site adresiyle internetin her yerini sahiplenin. + Özel alan adıyla kişisel online kimliğiniz olsun + Blog hatırlatıcılarını kullanmak için anlık bildirimleri açmanız gerekir. + Anlık bildirimleri aç + Alt alan adı ile devam et + Alan adı satın al + Fotoğraflar ve videolar - Müzik ve ses + Müzik ve ses + Fotoğraflar ve videolar + %s seslerinize erişebilmek için izin gerektiriyor + %s videolarınıza erişebilmek için izin gerektiriyor + %s fotoğraflarınıza erişebilmek için izin gerektiriyor + %s fotoğraflarınıza ve videolarınıza erişebilmek için izin gerektiriyor + %s müziğinize, sesinize, fotoğraflarınıza ve videolarınıza erişebilmek için izin gerektiriyor + Bildirimleri aç + Hemen bildirim almak için Ayarlar &rarr; Bildirimler &rarr; Uygulama Ayarları\'na gidin ve %1$s özelliğini açın. + Bildirimleri görmek için uygulamayı açmanız gerekecek. + Anlık bildirimler kapalı + Anlık bildirimler kapalı. + Bildirim izni uyarısını kapat. + Düzelt + <b>%1$s</b>, %2$s özel Jetpack eklentisini kullanıyor + <b>%1$s</b>, <b>%2$s</b> eklentisini kullanıyor + Özel Jetpack eklentilerini kullanan siteler WordPress uygulaması tarafından desteklenmemektedir. + <b>%1$s</b>, WordPress uygulaması tarafından desteklenmeyen özel Jetpack eklentilerini kullanıyor. + <b>%1$s</b>, WordPress uygulaması tarafından desteklenmeyen <b>%2$s</b> eklentisini kullanıyor. + Web sitelerinizin bazılarına ulaşılamıyor + Web sitelerinizin birine ulaşılamıyor + Bu web sitesini uygulamayla kullanabilmek için lütfen tam Jetpack eklentisini bağlamada size yardımcı olacağımız Jetpack uygulamasına geçiş yapın. + Jetpack uygulamasına geçiş yapın + %1$s, henüz uygulamanın tüm özelliklerini desteklemeyen %2$s kullanıyor.\n\nUygulamayı bu siteyle kullanmak için lütfen %3$s öğesini yükleyin. + Bu site + %1$s, henüz uygulamanın tüm özelliklerini desteklemeyen %2$s kullanıyor. Lütfen %3$s öğesini yükleyin. + %1$s, henüz uygulamanın tüm özelliklerini desteklemeyen %2$s kullanıyor. Lütfen %3$s öğesini yükleyin. + Birkaç gün içinde Jetpack uygulamasına geçiş. + Geçiş ücretsizdir ve yalnızca bir dakika sürer. + İstatistikler, Okuyucu, Bildirimler ve Jetpack destekli diğer özellikler WordPress uygulamasından kaldırıldı ve artık yalnızca Jetpack uygulamasında bulunuyor. + Jetpack.com\'da daha fazla bilgi edinin + Jetpack uygulamasına geçiş yapın + %s, Jetpack uygulamasına geçti. + %s, Jetpack uygulamasına geçti. + Pano + Yönet + Trafik + İçerik + Ayarla + Bitti + Jetpack yüklendiğine göre artık sadece ayarlarınızı tamamlamanız yeterli. Bu işlem sadece bir dakika sürer. Şimdi bir yazı yaz Bu sayfayı Blazeleyin Bu yazıyı Blazeleyin @@ -18,7 +77,6 @@ Language: tr İndirim Önerilen En İyi Alternatif - %s/y Yardım Sık sorulan soruların yanıtları için SSS bölümümüze bakın. Jetpack uygulamasına geçtiğiniz için teşekkür ederiz! @@ -38,24 +96,22 @@ Language: tr tam Jetpack eklentisi bireysel Jetpack eklentileri %1$s eklentisi - %1$s, uygulamanın tüm özelliklerini henüz desteklemeyen %2$s kullanıyor.\n\nUygulamayı bu siteyle birlikte kullanmak için lütfen %3$s yükleyin. + %1$s, henüz uygulamanın tüm özelliklerini desteklemeyen %2$s kullanıyor.\n\nUygulamayı bu siteyle kullanmak için lütfen %3$s öğesini yükleyin. Lütfen Jetpack eklentisinin tamamını yükleyin Yalnızca bir site kullanılabilir, bu nedenle birincil sitenizi değiştiremezsiniz. - Bu site, uygulamanın tüm özelliklerini henüz desteklemeyen ayrı bir eklenti kullanmaktadır. Lütfen Jetpack eklentisinin tamamını kurun. - Destek ile iletişim kur - Yeniden dene - Jetpack şu anda kurulamıyor. - Bir sorun vardı - Hata simgesi - Bitti - Bu siteyi uygulama ile kullanmaya hazır. - Jetpack kuruldu - Sitenize Jetpack kuruluyor. Bu işlemin tamamlanması birkaç dakika sürebilir. - Jetpack kuruluyor - Devam et - Web sitesi kimlik bilgileriniz depolanmayacak ve yalnızca Jetpack\'i kurma amacıyla kullanılacak - Jetpack\'i kur - Jetpack simgesi + Desteğe Başvurun + Tekrar dene + Jetpack şu anda yüklenemiyor. + Bir hata oluştu + Hata simgesi + Bu site uygulamayla kullanılmaya hazır + Jetpack yüklendi + Sitenize Jetpack yükleniyor. Bu işlemin tamamlanması birkaç dakika sürebilir. + Jetpack yükleniyor + Devam + Web sitenizin kimlik bilgileri depolanmayacak ve yalnızca güvenli şekilde Jetpack yüklemek amacıyla kullanılacaktır. + Jetpack\'i yükleyin + Jetpack simgesi Blaze ile Tanıt Sitenizin potansiyelinin tamamını ortaya çıkarın. Jetpack ile istatistikler, bildirimler ve daha fazlasını elde edin. Sitenizde Jetpack eklentisi var @@ -152,9 +208,6 @@ Language: tr Bağlantıları Jetpack\'te aç Yardıma mı ihtiyacınız var? Anladım - Veri çakışmalarını önlemek için lütfen <b>WordPress uygulamasını silin</b>. - Görünüşe göre WordPress uygulaması hâlâ yüklü. Veri çakışmalarını önlemek için WordPress uygulamasını silmenizi öneririz. - Artık WordPress uygulamasına ihtiyacınız yok Ağ bağlantısı olmadığı için verilerinizi ve ayarlarınızı aktaramıyoruz. Lütfen ağ bağlantınızın çalışıp çalışmadığını kontrol edip yeniden deneyin. İnternete bağlanılamıyor. @@ -164,13 +217,11 @@ Language: tr Tekrar deneyin Bitir WordPress Uygulaması simgesini kaldır - Veri çakışmalarını önlemek için lütfen <b>WordPress uygulamasını silin</b>. Tüm verilerinizi ve ayarlarınızı aktardık. Her şey bıraktığınız yerde. Jetpack\'e geçtiğiniz için teşekkürler! WordPress uygulamasından gelen bildirimleri kapatacağız. Aynı bildirimleri alacaksınız, ancak bunlar artık Jetpack uygulamasından gelecek. Artık bildirimler Jetpack\'ten geliyor - Lütfen WordPress uygulamasını silin WordPress yardım merkezi Destek Uygulamanın WordPress bildirimlerini devre dışı bırakmasına izin verir. @@ -749,6 +800,7 @@ Language: tr Tekrar dene GIF Bir + Başlık ekle Önizleme yok Metin rengi Dolgu @@ -757,6 +809,7 @@ Language: tr Özel adres Gömülü içerik oluştur Sütun %d + Daha fazla Ekran okuyucu kullanıcısına yardımcı olmak için bağlantıyı kısaca açıklayın Blok ekle Hiçbir Jetpack sitesi bulunamadı @@ -1173,7 +1226,6 @@ Language: tr Öykü Yayınlarına Giriş Boş sayfa oluşturuldu Sayfa oluşturuldu - %1$s fotoğraflarınıza erişimi reddedildi. Bunu düzeltmek için izinlerinizi düzenleyip %2$s ve %3$s özelliklerini açın. Medya eklenemedi. Medya eklenemedi: %s WordPress Ortam Kütüphanesi\'nden seçin @@ -1653,6 +1705,7 @@ Language: tr GÖRSEL VEYA VİDEO EKLE GÖRSEL EKLE BURAYA BLOK EKLE + Açıklama ekle Bir gönderiyi listenize kaydetmek için Gönderileri Kaydetmek için Ekle düğmesine dokunun. \"Listeye %1$d öğe yüklendi.\" Bildirimler @@ -1858,7 +1911,6 @@ Language: tr Daha fazla fikir edinmek için anahtar sözcük girin Hiçir öneri bulunamadı Alan Adını Kaydet - Jetpack yüklendiğine göre artık sadece ayarlarınızı tamamlamanız yeterli. Bu işlem sadece bir dakika sürer. Tek bakıştan çıkar Aşağı taşı Yukarı taşı @@ -2124,15 +2176,6 @@ Language: tr Takip edilen konu yok En sevdiğiniz konularla ilgili yazıları bulmak için buraya konu ekleyin Jetpack\'i bağlamak için kullandığınız WordPress.com hesabına giriş yapın. - Tekrar dene - Devam et - Jetpack şu anda yüklenemedi. - Bir sorun vardı - Jetpack kuruldu - Jetpack sitenize yükleniyor. Bu işlemin tamamlanması birkaç dakika sürebilir. - Jetpack kuruluyor - Web sitesi kimlik bilgileriniz depolanmayacak ve yalnızca Jetpack\'i kurma amacıyla kullanılacak. - Jetpack\'i kur Jetpack Jetpack SSS WordPress sitenizdeki istatistikleri kullanmak için Jetpack eklentisini yüklemeniz gerekir. @@ -2686,7 +2729,7 @@ Language: tr Belgeler Görseller Tümü - %1$s’un fotoğraflarınıza erişimi reddedildi. Bunu düzeltmek için izinlerinizi düzenleyip %2$s özelliğini açın. + %1$s için ortam dosyalarınıza erişim reddedildi. Bunu düzeltmek için izinlerinizi düzenleyip %2$s özelliğini açın. Yorumları görüntüle Videoların kalitesi. Daha yüksek değerler daha kaliteli videolar demektir. Yazılardaki videoların boyutunu bu boyuta değiştirir diff --git a/WordPress/src/main/res/values-vi/strings.xml b/WordPress/src/main/res/values-vi/strings.xml index 250a74299f2e..fcd5c5b7eb56 100644 --- a/WordPress/src/main/res/values-vi/strings.xml +++ b/WordPress/src/main/res/values-vi/strings.xml @@ -932,7 +932,6 @@ Language: vi_VN Giới thiệu Bài Story Đã tạo trang trống Đã tạo trang - %1$s không thể truy cập album ảnh của bạn. Để sửa vấn đề này, mở quyền truy cập %2$s và %3$s. Chèn media thất bại. Chèn media thất bại: %s Chọn từ Thư viện Media WordPress @@ -1614,7 +1613,6 @@ Language: vi_VN Nhập một từ khóa để có thêm ý tưởng Không có gợi ý Đăng ký tên miền - Đã cài đặt Jetpack, đang thiết lập mọi thứ. Điều này sẽ mất vài phút. Xóa khỏi phân tích Chuyển xuống Chuyển lên @@ -1877,15 +1875,6 @@ Language: vi_VN Chưa theo dõi thẻ nào Thêm thẻ ở đây để tìm bài viết liên quan những đề tài mà bạn yêu thích Đăng nhập tài khoản WordPress.com mà bạn có kết nối Jetpack. - Thử lại - Cài ngay - Không thể cài đặt Jetpack vào lúc này. - Đã xảy ra lỗi - Đã cài đặt Jetpack - Đang cài đặt Jetpack trên trang web của bạn. Phải mất vài phút để xong. - Đang cài đặt Jetpack - Thông số trang web của bạn sẽ không được lưu trữ và chỉ dùng với mục đích cài đặt Jetpack. - Cài đặt Jetpack Jetpack Jetpack FAQ Để xem Thống Kê trên trang WordPress, bạn phải cài đặt plugin Jetpack. @@ -2438,7 +2427,6 @@ Language: vi_VN Tài liệu Ảnh Tất cả - %1$s bị từ chối truy cập vào ảnh của bạn. Hãy cấp quyền và bật %2$s. Đọc bình luận Chất lượng video. Thông số càng cao thì chất lượng video càng tốt. Thay đổi video trong bài đăng thành kích thước này diff --git a/WordPress/src/main/res/values-zh-rCN/strings.xml b/WordPress/src/main/res/values-zh-rCN/strings.xml index f424cb0d415c..400884df78b2 100644 --- a/WordPress/src/main/res/values-zh-rCN/strings.xml +++ b/WordPress/src/main/res/values-zh-rCN/strings.xml @@ -1,11 +1,89 @@ + 移除区块 + 隐私和评级 + 播放设置 + 播放栏颜色 + 手动 + 动态 + 描述图片的用途。 如果图片仅作装饰用,请留空。 + 从适用于移动设备的定制布局开始 + 创建其他页面 + 向您的站点添加页面 + 隐藏此内容 + 通过易于查找、共享和关注的站点地址,在网络上创建自己的一席之地。 + 使用自定义域名,拥有您的在线身份 + 要使用博客发布提醒,您需要开启推送通知。 + 开启推送通知 + 继续使用子域名 + 购买域名 + 照片与视频和音乐与音频 + 音乐与音频 + 照片与视频 + %s 需要权限才能访问您的音频 + %s 需要权限才能访问您的视频 + %s 需要权限才能访问您的照片 + %s 需要权限才能访问您的照片和视频 + %s 需要权限才能访问您的音乐、音频、照片和视频 + 开启通知 + 转至“设置 &rarr; 通知 &rarr; 应用设置”,然后启用“%1$s”以立即接收通知。 + 您需要打开应用才能看到通知。 + 推送通知已关闭 + 推送通知已关闭。 + 忽略通知权限警告。 + 修复 + <b>%1$s</b> 正在使用 %2$s 个独立的 Jetpack 插件 + <b>%1$s</b> 正在使用 <b>%2$s</b> 插件 + WordPress 应用不支持使用独立的 Jetpack 插件的站点。 + <b>%1$s</b> 正在使用独立的 Jetpack 插件,而 WordPress 应用不支持该插件。 + <b>%1$s</b> 正在使用 <b>%2$s</b> 插件,而 WordPress 应用不支持该插件。 + 无法访问您的一些站点 + 无法访问您的其中一个站点 + 请切换到 Jetpack 应用,我们将在其中指导您连接完整的 Jetpack 插件,以便结合使用此站点和该应用。 + 切换到 Jetpack 应用 + %1$s 使用 %2$s,尚不支持该应用程序的全部功能。\n\n请安装 %3$s,通过此站点使用该应用程序。 + 此站点 + %1$s 使用 %2$s,尚不支持该应用程序的全部功能。 请安装 %3$s。 + %1$s 使用 %2$s,尚不支持该应用程序的全部功能。 请安装 %3$s。 + 几天后将移动至 Jetpack 应用程序。 + 切换免费,只需一会儿。 + 统计信息、阅读器、通知和其他由 Jetpack 提供支持的功能已从 WordPress 应用程序上删除,现在只能在 Jetpack 应用程序中使用。 + 请访问 Jetpack.com 了解详情 + 切换到 Jetpack 应用程序 + %s 已移至 Jetpack 应用程序。 + %s 已移至 Jetpack 应用程序。 + WP 管理 + 管理 + 流量 + 内容 + 设置 + 完成 + 安装 Jetpack 后,我们只需进行设置即可。 这只需要一分钟时间。 + 立即通过 Blaze 宣传文章 + 立即通过 Blaze 宣传此页面 + 立即通过 Blaze 宣传此文章 + 随时跟踪表现、开始和停止您的 Blaze。 + 数百万个 WordPress 和 Tumblr 站点上都会显示您的内容。 + 每天只需几美元即可在几分钟内推广任意文章或页面。 + 通过 Blaze 为您的站点吸引更多流量 + Blaze + 此域名已注册 + 销售 + 推荐 + 更好的替代方案 + 帮助 + 查看我们的常见问题解答,为您可能遇到的常见问题寻找答案。 + 感谢您切换至 Jetpack 应用程序! + 日志 + 票据 + 免费 + 帮助 区块菜单 隐藏此内容 在数以百万计的站点上展示您的作品。 @@ -18,26 +96,24 @@ Language: zh_CN 完整的 Jetpack 插件 独立的 Jetpack 插件 %1$s 插件 - %1$s 使用 %2$s 插件,尚不支持该应用程序的全部功能。\n\n请安装 %3$s,通过此站点使用该应用程序。 + %1$s 使用 %2$s,尚不支持该应用程序的全部功能。\n\n请安装 %3$s,通过此站点使用该应用程序。 请安装完整的 Jetpack 插件 只有一个站点可用,因此您不能更改您的主站点。 - 此站点使用独立插件,尚不支持该应用程序的全部功能。 请安装完整的 Jetpack 插件。 - 联系支持人员 - 重试 - 目前无法安装 Jetpack - 出现问题 - 错误图标 - 完成 - 已准备好通过应用程序使用此站点。 - Jetpack 已安装 - 正在您的站点上安装 Jetpack。 这将花费几分钟的时间才能完成。 - 正在安装 Jetpack - 继续 - 系统将不会存储您的网站凭据,并且仅将其用于安装 Jetpack。 - 安装 Jetpack - Jetpack 图标 + 联系支持人员 + 重试 + 目前无法安装 Jetpack。 + 出现问题 + 错误图标 + 已准备好通过应用程序使用此站点。 + Jetpack 已安装 + 正在您的站点上安装 Jetpack。 这将花费几分钟的时间才能完成。 + 正在安装 Jetpack + 继续 + 系统将不会存储您的网站凭据,并且仅将其用于安装 Jetpack。 + 安装 Jetpack + Jetpack 图标 大力宣传并推广 - 释放站点的全部潜力。 使用 Jetpack,获得统计信息、通知等功能。 + 释放站点的全部潜力。 通过 Jetpack,获取统计信息、通知和更多功能。 您的站点已安装 Jetpack 插件 Jetpack 移动应用程序旨在与 Jetpack 插件配合使用。 立即切换,访问统计信息、通知、阅读器以及更多功能。 接收有关新评论、点赞、查看等内容的通知。 @@ -80,6 +156,7 @@ Language: zh_CN 从<b>第一天</b>开始 隐藏此内容 稍后提醒我 + 统计信息、阅读器、通知和其他功能即将移至 Jetpack 移动应用。 切换到 Jetpack 应用 请访问 jetpack.com 以了解详情 切换免费,只需一会儿。 @@ -102,7 +179,9 @@ Language: zh_CN 提示 关闭 或者,您可以轻点“转换为常规区块”分离和编辑此区块。 + 是否永久删除“%s”分类? 已成功删除分类 + 删除分类失败 此用户的文章不会再显示 屏蔽用户 举报此用户 @@ -126,9 +205,6 @@ Language: zh_CN 在 Jetpack 中打开链接 需要帮助? 知道了 - 请<b>删除 WordPress 应用</b>以避免发生数据冲突。 - 似乎您仍然安装了 WordPress 应用。 我们建议您删除 WordPress 应用以避免发生数据冲突。 - 您不再需要 WordPress 应用 如果没有网络连接,我们将无法传输您的数据和设置。 请检查以确保您的网络连接正常,然后重试。 无法连接到互联网。 @@ -138,13 +214,11 @@ Language: zh_CN 重试 完成 删除 WordPress 应用图标 - 请<b>删除 WordPress 应用</b>以避免发生数据冲突。 我们已传输您的所有数据和设置。 一切都完好如初。 感谢您切换至 Jetpack! 我们将关闭 WordPress 应用中的通知功能。 您将收到所有相同的通知,但现在这些通知来自 Jetpack 应用。 现在通知来自 Jetpack 应用 - 请删除 WordPress 应用 WordPress 帮助中心 支持 允许此应用禁用 WordPress 通知功能。 @@ -723,6 +797,7 @@ Language: zh_CN 重试 GIF + 添加标题 无可用预览 文字颜色 内边距 @@ -731,6 +806,7 @@ Language: zh_CN 自定义 URL 创建嵌入 列 %d + 更多 简短描述此链接以帮助使用读屏软件的用户。 添加区块 未找到Jetpack站点 @@ -1147,7 +1223,6 @@ Language: zh_CN 故事文章简介 创建了空白页面 页面已创建 - %1$s 已被拒绝访问您的照片。 要解决此问题,请编辑您的权限,然后启用“%2$s”和“%3$s”。 媒体插入失败。 媒体插入失败: %s 从 WordPress 媒体库中选择 @@ -1627,6 +1702,7 @@ Language: zh_CN 添加图片或视频 添加图片 在此处添加区块 + 添加描述 轻按“添加以保存文章”按钮,将文章保存到您的列表中。 “列表已加载 %1$d 项。” 通知 @@ -1832,7 +1908,6 @@ Language: zh_CN 请输入关键字以获取更多想法 未找到任何建议 注册域 - 安装 Jetpack 后,我们只需进行设置即可。这只需要一分钟时间。 从见解中删除 下移 上移 @@ -2098,15 +2173,6 @@ Language: zh_CN 无已关注的主题 在此处添加主题,以查找有关您喜爱的主题的博文 登录用于关联 Jetpack 的 WordPress.com 账户。 - 重试 - 设置 - 目前无法安装 Jetpack。 - 出现问题 - 已安装 Jetpack - 正在您的站点上安装 Jetpack。这将花费几分钟的时间才能完成。 - 正在安装 Jetpack - 系统将不会存储您的网站凭据,并且仅将其用于安装 Jetpack。 - 安装 Jetpack Jetpack Jetpack 常见问题解答 要在 WordPress 站点中使用统计功能,您需要安装 Jetpack 插件。 @@ -2660,7 +2726,7 @@ Language: zh_CN 文档 图片 全部 - %1$s 访问您的照片时被拒绝。要解决此问题,请编辑您的权限并开启 %2$s。 + 已拒绝 %1$s 访问您的媒体文件。 要解决此问题,请编辑您的权限,然后启用“%2$s”。 查看评论 视频画质。值越高,视频画质越好。 将文章中的视频调整为此大小 diff --git a/WordPress/src/main/res/values-zh-rHK/strings.xml b/WordPress/src/main/res/values-zh-rHK/strings.xml index 6b8051e75caa..c16c170b7d01 100644 --- a/WordPress/src/main/res/values-zh-rHK/strings.xml +++ b/WordPress/src/main/res/values-zh-rHK/strings.xml @@ -1,12 +1,96 @@ + 建議<b>解除安裝裝置上的 WordPress 應用程式</b>以避免資料衝突。 + 你似乎仍有安裝 WordPress 應用程式。 + 你的裝置不再需要 WordPress 應用程式了 + 建議<b>解除安裝裝置上的 WordPress 應用程式</b>以避免資料衝突。 + 歡迎使用 Jetpack 應用程式。你可以解除安裝 WordPress 應用程式。 + 移除區塊 + 隱私權和評分 + 播放設定 + 播放列顏色 + 手動 + 動態 + 請說明圖片用途。 如果是為了裝飾,請保留空白。 + 從適合行動裝置瀏覽的可自訂版面開始 + 建立其他頁面 + 在網站上新增頁面 + 隱藏此訊息 + 建立易於搜尋、分享和追蹤的網站位址,打造屬於自己的網路園地。 + 自訂網域讓你擁有專屬的網路身分 + 你需要開啟推播通知才能使用網誌提醒。 + 開啟推播通知 + 繼續使用子網域 + 購買網域 + 相片和影片以及音樂和音訊 + 音樂和音訊 + 相片和影片 + %s 需要權限才能存取你的音訊 + %s 需要權限才能存取你的影片 + %s 需要權限才能存取你的相片 + %s 需要權限才能存取你的相片和影片 + %s 需要權限才能存取你的音樂、音訊、相片和影片 + 開啟通知 + 前往「設定」&rarr;「通知」&rarr;「應用程式設定」,開啟「%1$s」將能立即收到通知。 + 你需要開啟應用程式才能查看通知。 + 推播通知已關閉 + 推播通知已關閉。 + 關閉通知權限警告。 + 修正 + <b>%1$s</b> 正在使用 %2$s 個 Jetpack 個別外掛程式 + <b>%1$s</b> 正在使用 <b>%2$s</b> 外掛程式 + WordPress 應用程式不支援安裝 Jetpack 個別外掛程式的網站。 + <b>%1$s</b> 正在使用 WordPress 應用程式不支援的 Jetpack 個別外掛程式。 + <b>%1$s</b> 正在使用 WordPress 應用程式不支援的 <b>%2$s</b> 外掛程式。 + 無法存取你的某些網站 + 無法存取你其中一個網站 + 請切換至 Jetpack 應用程式,我們會引導你連結完整 Jetpack 外掛程式,即可透過應用程式使用此網站。 + 切換至 Jetpack 應用程式 + %1$s 正在使用 %2$s,目前尚未支援應用程式的所有功能。\n\n若要在此網站使用應用程式,請安裝 %3$s。 + 本網站 + %1$s 正在使用 %2$s,目前尚未支援應用程式的所有功能。 Please install the %3$s. + %1$s 正在使用 %2$s,目前尚未支援應用程式的所有功能。 Please install the %3$s. + Moving to the Jetpack app in a few days. + 切換完全免費,只要 1 分鐘就能完成。 + WordPress 應用程式已移除「統計資料」、「閱讀器」、「通知」和其他 Jetpack 提供的功能。 + 前往 jetpack.com 深入瞭解 + 切換至 Jetpack 應用程式 + %s have moved to the Jetpack app. + %s has moved to the Jetpack app. + WP 管理員 + 管理 + 流量 + 內容 + 設定 + 完成 + 現在 Jetpack 已安裝完成,我們只需要協助你完成設定即可。 這只需要一分鐘左右。 + 立即使用 Blaze 宣傳文章 + 使用 Blaze 宣傳此頁面 + 使用 Blaze 宣傳此文章 + 隨時追蹤成效、開始以及停止使用你的 Blaze。 + 你的內容會出現在數百萬個 WordPress 和 Tumblr 網站上。 + 只要幾分鐘就能宣傳任何文章或頁面;每天只需支付幾美元。 + 使用 Blaze 為你的網站吸引更多流量 + Blaze + 此網域已被註冊 + 特價 + 推薦 + 最佳替代選項 + 說明 + 請查看我們的常見問題集,瞭解你可能想知道的一般問題的解答。 + 感謝你改用 Jetpack 應用程式! + 記錄 + 支援票證 + 免費 + 說明 區塊選單 + 隱藏此訊息 在數百萬個網站中展示你的作品。 使用 Blaze 宣傳你的內容 關閉 @@ -14,89 +98,87 @@ Language: zh_TW 安裝完整外掛程式 條款與條件 設定 Jetpack 即代表你同意我們的 - 隱藏此訊息 完整 Jetpack 外掛程式 個別 Jetpack 外掛程式 %1$s 外掛程式 - %1$s 正在使用 %2$s,目前尚未支援應用程式的所有功能。\n\n若要在此網站使用應用程式,請安裝 %3$s。 + %1$s 正在使用 %2$s,目前尚未支援應用程式的所有功能。\n\n若要在此網站使用應用程式,請安裝 %3$s。 請安裝完整 Jetpack 外掛程式 只有一個網站可用,因此你無法變更主要網站。 - 聯絡支援團隊 - 重試 - 目前無法安裝 Jetpack。 - 發生問題 - 錯誤圖示 - 此網站正在使用個別外掛程式,該外掛程式目前尚未支援應用程式的所有功能。 請安裝完整 Jetpack 外掛程式。 - 完成 - 已安裝 Jetpack - 正在你的網站上安裝 Jetpack。 這可能需要幾分鐘才能完成。 - 正在安裝 Jetpack - 繼續 - 我們不會儲存你的網站憑證,而且只會將其用於安裝 Jetpack。 - 安裝 Jetpack - Jetpack 圖示 + 聯絡支援團隊 + 重試 + 目前無法安裝 Jetpack。 + 發生問題 + 錯誤圖示 + 準備好透過應用程式使用此網站了。 + 已安裝 Jetpack + 正在你的網站上安裝 Jetpack。 這可能需要幾分鐘才能完成。 + 正在安裝 Jetpack + 繼續 + 我們不會儲存你的網站憑證,而且只會將其用於安裝 Jetpack。 + 安裝 Jetpack + Jetpack 圖示 使用 Blaze 進行宣傳 - 準備好透過應用程式使用此網站了。 - 充分發揮網站的潛能。 透過 Jetpack 取得統計資料、通知等功能。 + 充分發揮網站的潛能。 透過 Jetpack 取得統計資料、通知等。 你的網站已安裝 Jetpack 外掛程式 + Jetpack 行動應用程式設計為可搭配 Jetpack 外掛程式運作。 立即切換以取得統計資料、通知、閱讀器等功能。 接收新留言、按讚、瀏覽次數等內容的通知。 尋找並追蹤你喜愛的網站和社群,並分享你的內容。 取得實用洞察報告和全面的統計資料,帶你見證流量成長。 - Jetpack 行動應用程式設計為可搭配 Jetpack 外掛程式運作。 立即切換以取得統計資料、通知、閱讀器等功能。 統計資料和洞察報告 + Jetpack 讓你更充分發揮 WordPress 網站潛能。 切換完全免費,只要 1 分鐘就能完成。 透過 Jetpack 提升 WordPress 成效 你可以隨時前往「我的網站」>「設定」>「撰寫網誌」控制網誌提示和提醒 通知會包含可啟發靈感的字詞或短語 前往「<b>網站設定</b>」以重新啟用 已隱藏網誌提示 - Jetpack 讓你更充分發揮 WordPress 網站潛能。 切換完全免費,只要 1 分鐘就能完成。 關閉提示 從我們的志工團隊取得協助。 社群論壇 網誌提醒 顯示提示 撰寫網誌 + 請安裝 Google Play 商店以取得 Jetpack 應用程式 稍後進行 切換至 Jetpack WordPress 應用程式已移除「統計資料」、「閱讀器」、「通知」和其他 Jetpack 提供的功能。 Jetpack 功能已經搬遷。 - %1$s 即將搬遷 - %1$s 即將搬遷 - 請安裝 Google Play 商店以取得 Jetpack 應用程式 %1$s 即將於 %2$s後搬遷 %1$s 即將於 %2$s後搬遷 + %1$s 即將搬遷 + %1$s 即將搬遷 取得 Jetpack 應用程式 檢視所有回應 較先前 7 天低 %1$s 較先前 7 天高 %1$s - 更早之前 7 天 - %d 週 - 1 週 你在過去 7 天內的訪客數比更早之前 7 天的訪客數低 %1$s。 你在過去 7 天內的訪客數比更早之前 7 天的訪客數高 %1$s。 你在過去 7 天內的檢視數比更早之前 7 天的檢視數低 %1$s。 你在過去 7 天內的檢視數比更早之前 7 天的檢視數高 %1$s。 + 更早之前 7 天 過去 7 天 + %d 週 + 1 週 從 <b>DayOne</b> 隱藏此項目 稍後再提醒我 + 「統計資料」、「閱讀器」、「通知」及其他功能即將搬遷至 Jetpack 行動應用程式。 切換至 Jetpack 應用程式 + 前往 jetpack.com 深入瞭解 切換完全免費,只要 1 分鐘就能完成。 WordPress 應用程式即將移除「統計資料」、「閱讀器」、「通知」和 Jetpack 提供的其他功能。 - 前往 jetpack.com 深入瞭解 WordPress 應用程式將於 %s 移除「統計資料」、「閱讀器」、「通知」和 Jetpack 提供的其他功能。 Jetpack 功能即將搬遷了。 「通知」將轉移至 Jetpack 應用程式 「閱讀器」將轉移至 Jetpack 應用程式 + 你的統計資料將轉移至 Jetpack 應用程式 切換至全新 Jetpack 應用程式 請檢查你的網路連線並再試一次。 + 目前無法載入此內容 載入提示時發生錯誤。 + 糟糕 尚無提示 %d 個答案 1 個答案 - 你的統計資料將轉移至 Jetpack 應用程式 - 目前無法載入此內容 - 糟糕 0 個答案 ✓ 已回答 提示 @@ -105,9 +187,9 @@ Language: zh_TW 是否要永久刪除「%s」分類? 分類已成功刪除 刪除分類失敗 - 更新分類 正在刪除分類 正在更新分類 + 更新分類 將不再顯示此使用者的文章 封鎖使用者 檢舉此使用者 @@ -131,24 +213,20 @@ Language: zh_TW 在 Jetpack 中開啟連結 需要協助嗎? 知道了 - 請<b>刪除 WordPress 應用程式</b>以避免資料衝突。 - 你似乎仍有安裝 WordPress 應用程式。 建議刪除 WordPress 應用程式以避免資料衝突。 - 你不再需要 WordPress 應用程式了 + 沒有網路連線的情況下,我們無法轉移你的資料和設定。 + 請檢查並確認你的網路連線正常,然後再試一次。 無法連線到網際網路。 請聯絡支援團隊,或稍後再試一次。 很抱歉,發生未預期的情況。 你的資料安全無虞,但目前無法傳輸資料。 - 沒有網路連線的情況下,我們無法轉移你的資料和設定。 - 請檢查並確認你的網路連線正常,然後再試一次。 糟糕,發生錯誤… 再試一次 完成 移除 WordPress 應用程式圖示 - 請<b>刪除 WordPress 應用程式</b>以避免資料衝突。 我們已轉移你的所有資料和設定。 一切全部順暢銜接。 感謝改用 Jetpack! 我們會關閉 WordPress 應用程式的通知。 你將收到所有相同的通知,但來源為 Jetpack 應用程式。 - 請刪除 WordPress 應用程式 + 現在改由 Jetpack 傳送通知 WordPress 說明中心 支援 允許應用程式停用 WordPress 通知。 @@ -170,11 +248,11 @@ Language: zh_TW 從任何地方都能撰寫、編輯和發表內容。 無法擷取作者 作者 + 喜歡 %s 嗎? 將文章分享到 %s Jetpack Social 連結 若要新增小工具,請登入 Jetpack 應用程式。 Jetpack Social 連結 - 喜歡 %s 嗎? 我們剛將神奇連結傳送至: 在此裝置查看你的電子郵件! 使用密碼登入 @@ -241,9 +319,9 @@ Language: zh_TW 總計 其他 搜尋 + WordPress 檢視 排程 - WordPress 排程你的文章 設定提醒 設定你的網誌提醒 @@ -260,8 +338,8 @@ Language: zh_TW 掃描登入碼 ⭐️ 你的最新文章 %1$s 已獲得 %2$s 次按讚。 沒有足夠的活動。 有更多訪客造訪網站的時候,請再回來查看! - %1$s (%2$s%%) %1$s 個追蹤者,共 %2$s%% 個 + %1$s (%2$s%%) 複製連結 恭喜! 你熟知<br/> 開始瞭解應用程式 @@ -294,8 +372,8 @@ Language: zh_TW 關閉 答案 每日提示 - 點選 <b>%1$s</b> 即可檢視你的網站 完全了解 + 點選 <b>%1$s</b> 即可檢視你的網站 選取「%1$s 閱讀器 %2$s」來尋找其他網站。 深入瞭解提示 已取消選取影片 @@ -358,10 +436,10 @@ Language: zh_TW 注意: 儀表板上每天都會顯示最新提示,讓你的創意源源不絕! 想成為更好的撰稿人,最佳方法是養成寫作習慣並與他人分享 - 這就是「提示」的功用! - 設定提醒 - 定期張貼文章能夠吸引新讀者。 告訴我們你想寫作的時間,我們將傳送提醒給你! 簡介\n網誌提示 + 設定提醒 包含網誌提示 + 定期張貼文章能夠吸引新讀者。 告訴我們你想寫作的時間,我們將傳送提醒給你! 養成寫作習慣,成為更好的撰稿人 寫作和詩歌 旅遊 @@ -396,8 +474,8 @@ Language: zh_TW 例如: 時尚、詩歌、政治 網站主題 點選「<b>%1$s</b>」以繼續。 - 檢視更多提示 今天跳過 + 檢視更多提示 %d 個答案 分享網誌提示 ✓ 已回答 @@ -407,8 +485,8 @@ Language: zh_TW 此色彩組合可能讓人難以閱讀。 請嘗試更亮的背景顏色和/或較深的文字顏色。 此色彩組合可能讓人難以閱讀。 請嘗試更深的背景顏色和/或較亮的文字顏色。 無法插入媒體。\n點選以瞭解更多資訊。 - 你的網站主題為何? 請從下方清單選擇一個主題,或輸入你自己的主題。 + 你的網站主題為何? 每週綜合整理 首頁 新增類別中 @@ -447,6 +525,7 @@ Language: zh_TW 隱私權政策 服務條款 隨時隨地都能處理工作 + 加入我們 Automattic 系列 法律與其他 Twitter @@ -454,7 +533,6 @@ Language: zh_TW 為我們評分 與朋友分享 你可以使用網頁版本的編輯器來編輯此區塊。 - 加入我們 開啟 Jetpack Security 設定 注意:您必須允取登入 WordPress.com 方可在行動編輯器中編輯此區塊。 注意:版面配置可能會因佈景主題和螢幕尺寸而有所不同 @@ -480,27 +558,27 @@ Language: zh_TW 按兩下以選擇字型大小 按兩下以選擇預設字型大小 聯絡支援團隊 + %1$s (%2$s) 關注討論 成為第一位留言的人 查看所有留言 取得文章的資料時發生錯誤 - %1$s (%2$s) 取得留言時發生錯誤 關注討論設定 從剪貼簿 精選圖片 從剪貼簿複製 URL,%s + 關於 WordPress 前往已排程的文章 前往草稿 - 關於 WordPress 建立文章 定期發文有助於建立讀者群! 建立下一篇文章 已切換至「視覺化」模式 已切換至「HTML」模式 + 連結已複製到剪貼簿 作者 複製連結 - 連結已複製到剪貼簿 新增自訂網域可讓訪客輕鬆找到你的網站 新增你的網域 文章會在你的網誌頁面上按時間倒序顯示。 是時候和全世界分享你的想法了! @@ -529,8 +607,8 @@ Language: zh_TW 已取消訂閱此討論 正在追蹤此討論\n是否啟用應用程式內通知? 搜尋網域 - 你的方案已包含一年免費網域註冊 此網站購買的網域會將訪客重新導向至 <b>%s</b> + 你的方案已包含一年免費網域註冊 索取你的免費網域 管理網域 新增網域 @@ -540,8 +618,8 @@ Language: zh_TW 網站主要網址 變更網址 你的 WordPress.com 免費位址是 - %s<span style=\"color:#50575e;\">/年</span> 第一年 <span style=\"color:#B26200;\">%1$s,以後每年 </span><span style=\"color:#50575e;\"><s>%2$s</s></span> + %s<span style=\"color:#50575e;\">/年</span> 你要捨棄這些變更嗎? 有未儲存的變更 留言不可空白 @@ -565,14 +643,14 @@ Language: zh_TW <a href=\"\">你</a>說這個讚。 行高 取得你的網域 - 擷取建議的應用程式範本時發生未知錯誤 %s + 擷取建議的應用程式範本時發生未知錯誤 收到無效回應 未收到回應 + Automattic 應用程式 - 任何螢幕都適用的應用程式 向朋友推薦 WordPress 快速連結 網域 - Automattic 應用程式 - 任何螢幕都適用的應用程式 每週綜合整理:%s 通知時間 你會<b>每天</b>在 <b>%s</b> 收到寫網誌通知。 @@ -602,8 +680,8 @@ Language: zh_TW 區塊是讓你可以不必瞭解如何編寫程式碼,就能插入、重新排列及調整樣式的塊狀內容。 區塊可讓你用簡單且現代化的方式建立美麗的版面配置。 區塊可讓你專心撰寫內容,瞭解所有必要的格式工具一應俱全,協助你傳達自己的訊息。 在欄位中排列你的內容、新增「行動呼籲」按鈕,並在圖片中加上文字。 - 已完成 %1$s 個,共 %2$s 個 點選左下角工具列的「+」號圖示,即可隨時新增區塊。 + 已完成 %1$s 個,共 %2$s 個 透過快速逐步解說瞭解基本知識。 無法審核一則或更多留言 建立網站 @@ -636,38 +714,38 @@ Language: zh_TW 內嵌字幕。 清空 請造訪我們的文件頁面 Jetpack Backup for Multisite 安裝提供可下載的備份,無需一鍵還原。 詳情請見 %1$s。 + 定期發表文章有助於持續與讀者互動,並吸引更多訪客造訪網站。 秘訣 你可以隨時更新 - 你可以隨時前往「我的網站」>「設定」>「網誌提醒」更新此設定。 - 定期發表文章有助於持續與讀者互動,並吸引更多訪客造訪網站。 選取你希望發表網誌的日期 + 你可以隨時前往「我的網站」>「設定」>「網誌提醒」更新此設定。 你目前並未設定任何提醒。 + 你會每週收到 %1$s 次網誌通知,時間分別為 %2$s 的 %3$s。 提醒已移除! 一切就緒! - 你會每週收到 %1$s 次網誌通知,時間分別為 %2$s 的 %3$s。 更新 未設定任何目標 一週%s 設定提醒 - 正在發表你的文章…在此同時,你可以在想張貼文章的日期設定網誌提醒。 在你想發表文章的日期設定網誌提醒。 + 正在發表你的文章…在此同時,你可以在想張貼文章的日期設定網誌提醒。 設定你的網誌提醒 + 在此提醒你於今天創作一些內容 該在 %s 新增網誌內容了 iOS 版 WordPress 尚不支援編輯可重複使用區塊 - 在此提醒你於今天創作一些內容 Android 版 WordPress 尚不支援編輯可重複使用區塊 你可以改為點選「轉換為一般區塊」,然後分別移除並編輯這些區塊。 完成 通知我 <a href=\"%1$s\">輸入你的伺服器憑證</a>,以啟用從備份一鍵還原網站的功能。 - 建立分類 - Android 版 WordPress 支援 設為特色圖片 移除特色圖片 + 建立分類 + Android 版 WordPress 支援 管理你的網站分類 分類 - 最新文章頁面的內容由系統自動產生,無法編輯。 提醒 + 最新文章頁面的內容由系統自動產生,無法編輯。 框線設定 不要再顯示 檢視儲存空間 @@ -690,8 +768,8 @@ Language: zh_TW 按兩下開啟操作工作表以新增圖片或視訊 目前的單位為 %s 多重發佈 - 欄位設定 「%s」已轉換為一般區塊 + 欄位設定 新增連結至 %s 新增連結文字 新增圖片或視訊 @@ -708,13 +786,13 @@ Language: zh_TW 若你已擁有網站,則需要安裝免費 Jetpack 外掛程式,並連結你的 WordPress.com 帳號。 你的個人檔案照片 若要將此應用程式用於 %1$s,你需要安裝 Jetpack 外掛程式並連結 WordPress.com 帳號。 - 寬度設定 - 向後移動圖片 向前移動圖片 - 欄位設定 + 向後移動圖片 + 寬度設定 鏈結 Rel - 網站 + 欄位設定 (未命名) + 網站 使用者個人資料底頁資訊 按讚清單%s @@ -722,19 +800,21 @@ Language: zh_TW %s 社交圖示 標記 新功能 - 閱覽頁面 預覽文章 + 閱覽頁面 重試 GIF + 新增標題 未提供預覽 文字顏色 邊框間距 - 精選內容 - 建立嵌入內容 + 精選內容 自訂 URL + 建立嵌入內容 欄 %d + 更多 簡述連結以協助螢幕閱讀器使用者 新增區塊 找不到任何 Jetpack 網站 @@ -743,10 +823,10 @@ Language: zh_TW 轉換區塊… 無法插入媒體。 無法插入音訊檔案。 + 請說明圖片用途。 如果圖片只是為了裝飾,請保留空白。 %1$s 已轉換為 %2$s 載入讚資料時發生錯誤。%s。 %d 按讚數 - 請說明圖片用途。 如果圖片只是為了裝飾,請保留空白。 1 個讚 建議: 使用圖示按鈕 @@ -754,13 +834,13 @@ Language: zh_TW 搜尋按鈕。 目前的按鈕文字是 搜尋區塊 搜尋區塊標籤。 目前的文字是 + 外部 未設定自訂預留位置 內部 隱藏搜尋標題 按兩下以編輯預留位置文字 按兩下以編輯標籤文字 按兩下以編輯按鈕文字 - 外部 點兩下即可變更單位 目前的預留位置文字是 清除搜尋 @@ -779,8 +859,8 @@ Language: zh_TW 新增按鈕文字 追蹤主題 在網站創作並發布引人注目內容的全新方式。 - 下載 關閉 + 下載 已成功修正威脅。 請確認你要修正所有 %s 個作用中威脅。 掃描作業發現 %2$s 有 %1$s 個潛在威脅。 請檢閱以下威脅並採取行動,或點選「修復所有威脅」按鈕。 如果你有需要,我們 %3$s。 @@ -802,26 +882,26 @@ Language: zh_TW 取得角色時發生錯誤 擷取邀請連結資料時發生不明錯誤 用這個連結讓成員直接加入,不需要一一邀請。 即使是從其他人那裡收到連結,只要造訪此 URL 的人都可以註冊你的組織,因此請確保僅與值得信任的人分享。 + 有效期限 %1$s 停用邀請連結 分享邀請連結 產生新連結 重新整理連結狀態 邀請連結 - 有效期限 %1$s 發現威脅 發現數個威脅 + <b>掃描完成</b> <br> %s 個潛在威脅已發現 <b>掃描完成</b> <br> 發現一個潛在威脅 <b>掃描完成</b> <br> 未發現威脅 - <b>掃描完成</b> <br> %s 個潛在威脅已發現 正在修正威脅 停用 - 檢查你的頁面並進行變更,或新增/移除頁面。 取消釘選此項目 + 檢查你的頁面並進行變更,或新增/移除頁面。 + 查看你的網站 探索並追蹤帶給你啟發的網站。 + 社群分享 自動將新文章分享到社群媒體。 為網站取一個能反映出自我風格和主題的名稱。 - 社群分享 - 查看你的網站 檢視網站統計資料 我們仍會嘗試為你建立可下載的備份檔案。 我們擷取不到狀態,無法判斷需要花多久時間建立可下載的備份。 @@ -853,6 +933,7 @@ Language: zh_TW 按兩下以聆聽音訊檔案 選擇音樂 音樂播放器 + 音訊檔案 音訊字幕。 %s 音訊字幕。 清空 新增音訊 @@ -860,7 +941,6 @@ Language: zh_TW 使用這個音訊 從裝置中選擇音訊 選填:輸入自訂訊息,連同你的邀請一併傳送。 - 音訊檔案 瞭解更多有關角色的資訊 固定 找到 @@ -871,8 +951,8 @@ Language: zh_TW 歡迎使用 Jetpack Scan 服務。我們會掃瞄你的網站,並且很快讓你得知結果。 我們正努力在背景解決此威脅。 這段時間你可以照常使用網站,也可以隨時回來查看掃瞄進度。 如果發現威脅,你會收到通知。 這段時間你可以照常使用網站,也可以隨時回來查看掃瞄進度。 - Jetpack Scan 無法完成網站掃瞄作業。 請檢查你的網站是否已經關閉。若否,請再試一次。 如果網站已關閉,或 Jetpack Scan 仍無法順利掃瞄,請與我們的支援團隊聯絡。 正在修正威脅 + Jetpack Scan 無法完成網站掃瞄作業。 請檢查你的網站是否已經關閉。若否,請再試一次。 如果網站已關閉,或 Jetpack Scan 仍無法順利掃瞄,請與我們的支援團隊聯絡。 發生錯誤 正在備份網站 正在從 %1$s %2$s 備份網站 @@ -881,9 +961,9 @@ Language: zh_TW 你的網站已成功備份\n已從 %1$s %2$s 備份 正在備份你的網站\n正在從 %1$s %2$s 備份 設定音樂 + 有其他還原作業正在執行。 錯誤圖示 「完成」按鈕 - 有其他還原作業正在執行。 還原失敗 「造訪網站」按鈕 「完成」按鈕 @@ -912,11 +992,11 @@ Language: zh_TW 平板電腦 行動裝置 不要再顯示 + 釘選此項目 選取%1$s「頁面」%2$s以檢視頁面清單。 變更、新增或移除網站的頁面。 檢閱網站頁面 選取%1$s「首頁」%2$s以編輯首頁。 - 釘選此項目 標示為未讀。 標示為已讀。 上傳媒體失敗。\n%1$s @@ -925,8 +1005,8 @@ Language: zh_TW 已將文章標示為未讀 已將文章標示為已讀 取得修正狀態時發生錯誤。 請聯絡我們的支援團隊。 - 修正威脅時發生錯誤。 請聯絡我們的支援團隊。 已成功修正威脅。 + 修正威脅時發生錯誤。 請聯絡我們的支援團隊。 請確認你要修正一個作用中威脅。 修復所有威脅 忽略威脅時發生錯誤。 請聯絡支援團隊。 @@ -934,6 +1014,7 @@ Language: zh_TW 除非徹底確認安全無害,否則請不要忽視安全性問題。 若你選擇忽略此威脅,該威脅會繼續存在於你的網站 <b>%s</b>。 修正威脅時發生錯誤。 請聯絡我們的支援團隊。 威脅已忽略 + 威脅已於 %s 修正 正在修正威脅 已忽略 找不到任何項目 @@ -946,7 +1027,6 @@ Language: zh_TW 請嘗試調整你的日期範圍 找不到相符的備份 你的第一次備份將於 24 小時內顯示於此;備份完成後,你會收到通知 - 威脅已於 %s 修正 第一次備份即將準備就緒 處理要求時發生問題。 請稍後再試一次。 移至底部 @@ -962,10 +1042,10 @@ Language: zh_TW 我們已成功建立 %1$s %2$s 的網站備份。 你的備份現在已可下載 你的備份 + 無需在附近等待。 我們會在備份準備就緒時通知你。 建立可下載備份圖示 我們正在為你的網站建立 %1$s %2$s 的可下載備份。 目前正在為你的網站建立可下載的備份 - 無需在附近等待。 我們會在備份準備就緒時通知你。 下載備份 正在執行其他下載作業。 處理要求時發生問題。 請稍後再試一次。 @@ -976,6 +1056,10 @@ Language: zh_TW %1$s · 多重發佈 使用者 + 沒有相符的 %s。 + 載入建議時發生問題。 + 無可用的 %s 建議。 + 請輸入字詞以篩選建議清單。 免費估價 忽略威脅 威脅處理 @@ -983,10 +1067,6 @@ Language: zh_TW Jetpack Scan 會刪除受影響的檔案或目錄。 Jetpack Scan 將升級至更新版本 (%s)。 Jetpack Scan 會刪除受影響的檔案或目錄。 - 沒有相符的 %s。 - 載入建議時發生問題。 - 無可用的 %s 建議。 - 請輸入字詞以篩選建議清單。 Jetpack Scan 會替換受影響的檔案或目錄。 Jetpack Scan 無法自動修正此威脅。\n 建議你手動處理,方法如下:確認 WordPress、你使用的佈景主題與所有外掛程式都是最新的版本,並且將可能有資安疑慮的程式碼、佈景主題或外掛程式自網站上移除。 \n \n\n 如需更多協助來解決此威脅,建議你使用 <b>Codeable</b>;這個自由工作者平台上的 WordPress 專家均通過嚴格審查,專業能力值得信賴。\n 他們已經選出一群安全專家協助你處理這些專案。 價格為一小時 $70 到 $120 之間,平台提供免費報價,不一定要聘用。 正在解決威脅 @@ -1001,17 +1081,17 @@ Language: zh_TW 發現威脅 %s 已在 WordPress 中發現安全漏洞 其他安全漏洞 - %s:惡意程式碼模式 - 資料庫有 %s 個威脅 易受攻擊的佈景主題:%1$s (版本:%2$s) 易受攻擊的外掛程式:%1$s (版本:%2$s) + %s:惡意程式碼模式 + 資料庫有 %s 個威脅 %s:受感染的核心檔案 發現威脅 - 此網站 - 幾秒鐘前 全部修復 + 幾秒鐘前 %s 分鐘前 %s 小時前 + 此網站 最近一次 Jetpack 掃瞄已執行 %1$s,且未找到任何風險。 %2$s 你的網站可能存在風險 完全不必擔心 @@ -1020,22 +1100,22 @@ Language: zh_TW 掃瞄狀態圖示 備份 活動類型篩選器 (已選取 %s 個類型) + %1$s (顯示 %2$s 個項目) 活動類型篩選器 選取的日期範圍內沒有任何活動記錄。 無活動可查看 請檢查你的網際網路連線,然後再試一次。 - %1$s (顯示 %2$s 個項目) 沒有連線 活動類型 (%s) 日期範圍篩選器 請嘗試調整日期範圍或活動類型篩選條件 找不到符合的活動 + 網站資料庫 + (包括 wp-config.php 和任何非 WordPress 檔案) 媒體上傳 WordPress 外掛程式 WordPress 佈景主題 建立可下載的備份圖示 - 網站資料庫 - (包括 wp-config.php 和任何非 WordPress 檔案) 建立可下載檔案 建立可下載的備份 下載備份 @@ -1061,13 +1141,13 @@ Language: zh_TW 複製檔案 URL 選擇檔案 選擇網域 + Jetpack 設定 Jetpack + 透過電子郵件追蹤對話 + 透過電子郵件追蹤對話 無法取消訂閱這篇文章的留言 無法訂閱這篇文章的留言 擷取文章訂閱狀態時發生錯誤 - 透過電子郵件追蹤對話 - 透過電子郵件追蹤對話 - Jetpack 設定 收到無效回應 未收到回應 清除 @@ -1089,10 +1169,10 @@ Language: zh_TW 完成 下一步 刪除 + 選取佈景主題時發生錯誤。 請檢查你的網際網路連線,然後再試一次。 請在重新上線時點選重試。 版面配置在離線時無法使用 - 選取佈景主題時發生錯誤。 使用商店憑證繼續 尋找你連結的電子郵件 追蹤主題 @@ -1129,11 +1209,11 @@ Language: zh_TW Pamela Nguyen 攝影師 Cameron Karsten 的作品讓我深受啟發。 我下次會嘗試這些技巧 激發靈感 + 追蹤你喜歡的網站並發掘新網誌。 透過深入分析資料拓展讀者群。 + 即時查看留言和通知。 這個功能強大的編輯器讓你隨時隨地都能發佈文章。 歡迎使用全球最受歡迎的網站建置工具。 - 即時查看留言和通知。 - 追蹤你喜歡的網站並發掘新網誌。 媒體載入失敗 值得關注的網站 我們正在努力開發,讓每次版本更新都可以新增更多區塊。 @@ -1151,14 +1231,13 @@ Language: zh_TW 限時動態文章介紹 已建立空白頁面 已建立頁面 - %1$s 存取你的相片時遭拒絕。 若要解決此問題,請編輯存取權限並開啟 %2$s 和 %3$s。 媒體插入失敗。 媒體插入失敗:%s 從 WordPress 媒體庫選擇 返回 開始使用 - 發表者: 追蹤主題以探索新網誌 + 發表者: 此推薦連結不能被標記為垃圾郵件 取消標記為垃圾郵件 標記為垃圾 @@ -1178,8 +1257,8 @@ Language: zh_TW 傳統編輯器 休閒 你需要授予應用程式錄音權限,才能錄製影片 - 已選取 %s %s + 已選取 %s 透過電子郵件取得登入連結 很抱歉,我們找不到與此電子郵件地址連結的 WordPress.com 帳號。 麥克風 @@ -1190,13 +1269,13 @@ Language: zh_TW 發生內部伺服器錯誤 你無法執行此動作 其他 %1$s 個項目 - 請注意:內容欄版面配置可能會因佈景主題和螢幕尺寸而有所不同 選取版面配置 - 你可能會喜歡 - 隱藏 + 請注意:內容欄版面配置可能會因佈景主題和螢幕尺寸而有所不同 建立文章或故事 建立頁面 建立文章 + 你可能會喜歡 + 隱藏 視訊標題。 清空 更新標題。 將區塊貼在以下物件後方: @@ -1260,11 +1339,11 @@ Language: zh_TW 建立頁面 建立空白頁面 首先請從各種預先製作的頁面版面配置挑選。 或從空白頁面開始也可以。 - 新增故事標題 - 點選 %1$s「建立」。 %2$s 然後選取<b>「網誌文章」</b> 選擇版面配置 + 新增故事標題 建立文章或限時動態 建立文章、頁面或限時動態 + 點選 %1$s「建立」。 %2$s 然後選取<b>「網誌文章」</b> 從裝置中選擇 故事文章 你需要 Jetpack 外掛程式才能在自助託管的 WordPress 網站上編輯網站圖示。 @@ -1274,14 +1353,14 @@ Language: zh_TW 加入檔案 替換視訊 替換圖片或視訊 + 轉換成連結 選擇視訊 選擇圖片或視訊 選擇圖片 已移除區塊 + 輸入你現有的網址 註冊確認 如果你繼續使用 Google 操作且尚未擁有 WordPress.com 帳號,即表示你正在建立帳號,並同意我們的%1$s服務條款%2$s。 - 輸入你現有的網址 - 轉換成連結 繼續操作即代表你同意我們的%1$s服務條款%2$s。 我們會使用這個電子郵件地址,為你建立全新的 WordPress.com 帳號。 我們已透過電子郵件傳送註冊連結給你,使用此連結即可建立新的 WordPress.com 帳號。 請使用此裝置查看電子郵件,並點選 WordPress.com 所傳送電子郵件中的連結。 @@ -1301,10 +1380,11 @@ Language: zh_TW 以電子郵件傳送連結 重設密碼 處理要求時發生問題。 請稍後再試一次。 - 點選 <b>%1$s</b> 以設定新標題 確認網站標題 + 點選 <b>%1$s</b> 以設定新標題 將這篇文章移至垃圾桶也會捨棄本機變更,確定要繼續此操作? %s 區塊選項 + 移除區塊 重複區塊 複製區塊 複製的區塊 @@ -1313,7 +1393,6 @@ Language: zh_TW 已剪下區塊 已複製區塊 只有具有管理員角色的使用者才能變更網站標題。 - 移除區塊 網站標題會顯示在網頁瀏覽器的標題列中,且會顯示在大部分佈景主題的頁首中。 主旨 無法更新網站標題。 請檢查你的網路連線並再試一次。 @@ -1334,6 +1413,7 @@ Language: zh_TW 關閉 未設定 標籤可協助讀者瞭解文章內容。 + 發表日期 新增標籤 返回 立即儲存 @@ -1345,18 +1425,17 @@ Language: zh_TW 取消 移至草稿 你無法編輯移至垃圾桶的文章。 要將此文章狀態變更為「草稿」以便修正嗎? - 發表日期 是否將文章移至草稿? + 選擇你的主題 + 選擇你的主題 + 完成 選取幾項以繼續操作 + 已發佈 已移至垃圾桶 已排程 發表日期 閱讀《加州消費者隱私保護法》(CCPA) 隱私權聲明 根據《加州消費者隱私保護法》(以下稱「CCPA」) 規定,我們必須提供加州使用者一些額外資訊,說明我們收集和分享的個人資訊種類、從哪裡取得這些資訊,以及這些資訊的使用方式及用途。 - 完成 - 選擇你的主題 - 選擇你的主題 - 已發佈 加州使用者的隱私權聲明 狀態及可見度 馬上更新 @@ -1368,12 +1447,12 @@ Language: zh_TW 目前我們無法開啟頁面。請稍後再試一次 設為文章頁面 設為首頁 + %1$s 不是有效的 %2$s 選取頁面 文章列表頁面 靜態首頁 傳統網誌 選取的首頁和文章頁面不能相同。 - %1$s 不是有效的 %2$s 首頁設定更新失敗,請檢查你的網際網路連線 載入頁面之前無法儲存首頁設定 無法儲存首頁設定 @@ -1392,7 +1471,9 @@ Language: zh_TW 點選兩次即可前往顏色設定 關注網站後,網站內容就會顯示於此 瞭解更多 + %s 新功能 插入 %d 個 + 裁剪 無法載入檔案,請再試一次。 預覽圖片縮圖 使用此媒體 @@ -1400,8 +1481,6 @@ Language: zh_TW 選擇媒體 選擇視訊 無法選擇網站,請再試一次。 - 裁剪 - %s 新功能 繼續 轉發失敗 管理網站 @@ -1422,13 +1501,13 @@ Language: zh_TW 向左移動區塊 按兩下將區塊向右移 按兩下將區塊向左移 + 區塊設定 建立儀表板 設定佈景主題 新增網站功能 擷取網站網址 你的網站即將準備就緒 太棒了!\n快完成了 - 區塊設定 取消上傳 處理要求時發生問題 由 Tenor 支援 @@ -1443,12 +1522,12 @@ Language: zh_TW 無法存取私人網站的內容。部分媒體可能無法使用 正在存取私人網站的內容 無法裁剪和儲存圖片,請再試一次。 + 無法載入圖片。\n請點選以重試。 預覽圖片 不明的頁面格式 我們無法完成此動作,因此未將此頁面提交送審。 我們無法完成此動作,因此未排程此頁面。 我們無法完成此動作,因此未發表這篇私密文章。 - 無法載入圖片。\n請點選以重試。 我們無法完成此動作,因此未發表此頁面。 我們無法將此頁面提交送審,但稍後會再試一次。 我們無法排程此頁面,但稍後會再試一次。 @@ -1485,14 +1564,14 @@ Language: zh_TW 編輯封面媒體 自訂 按鈕連結網址 - 新增段落區塊 框線圓角半徑 + 新增段落區塊 建立文章 已移至垃圾桶 已排程 已發佈 - 未連線 Facebook 連結找不到任何頁面。 Jetpack Social 無法連結至 Facebook 個人檔案,只能連結至已發表的頁面。 + 未連線 按讚數 關注者 留言 @@ -1508,19 +1587,19 @@ Language: zh_TW 選擇一個標籤或網站、快顯視窗 選擇網站或標籤以篩選文章 移除目前的篩選條件 - 登入 WordPress.com 管理主題和網站 - 登入 WordPress.com,查看關注網站的最新文章 + 登入 WordPress.com 登入 WordPress.com,查看你關注主題的最新文章 + 登入 WordPress.com,查看關注網站的最新文章 取代目前區塊 新增至「結束」 新增至「開始」 在之前新增區塊 在之後新增區塊 - 關注網站 - 查看你關注網站的最新聞張 新增主題 + 關注網站 你可以新增主題以關注特定主題的文章 + 查看你關注網站的最新聞張 關注中 篩選 視訊標題。%s @@ -1565,22 +1644,23 @@ Language: zh_TW 無法存取你網站上的 <b>XMLRPC 檔案</b>。你必須聯絡主機商以解決此問題。 快完成了!我們只需驗證你與 Jetpack 綁定的電子郵件地址就行了<b>%1$s</b> 請使用 %1$s 網站憑證登入 - 關注中 網頁 - 目前無法開啟文章。請稍後再試一次 - %sB - %sM - %sQa - %sT - %sQi - 網站 - 已儲存 - 探索 + 關注中 按讚數 - 目前無法載入你的網站資料。請稍後再試一次 + 探索 + 已儲存 主題 + 網站 + %sQi + %sQa + %sT + %sB + %sM %sK + 目前無法開啟文章。請稍後再試一次 + 目前無法載入你的網站資料。請稍後再試一次 WordPress 媒體庫 + 不支援 取消群組 點選即可隱藏鍵盤 點選此處顯示說明 @@ -1588,7 +1668,6 @@ Language: zh_TW 拍攝相片或視訊 拍攝照片 開始撰寫內容… - 不支援 %s 區塊含有無效內容 %s 區塊空白 剪下區塊 @@ -1605,12 +1684,12 @@ Language: zh_TW 向上移動區塊 將區塊從第 %1$s 列向下移至第 %2$s 列 向下移動區塊 + 連結文字 連結已插入 圖片說明。%s 隱藏鍵盤 說明圖示 按兩次即可復原上次變更 - 連結文字 按兩下以切換設定 按兩下以選擇圖片 按兩下以選擇視訊 @@ -1627,16 +1706,17 @@ Language: zh_TW 替代文字 新增視訊 新增網址 + 新增替代文字 新增圖片或視訊 新增圖片 在此新增區塊 - 新增替代文字 + 新增說明 點選「新增至儲存文章」按鈕即可將文章儲存至你的清單。 「此清單已載入 %1$d 個項目。」 通知 - 若關閉此網站的「通知」功能,將會停用網站通知分頁的通知顯示。 你可以開啟網站的「通知」功能,進一步調整想查看的通知類型。 關閉 開啟 + 若關閉此網站的「通知」功能,將會停用網站通知分頁的通知顯示。 你可以開啟網站的「通知」功能,進一步調整想查看的通知類型。 若要在此網站的通知分頁查看通知,請開啟此網站的「通知」功能。 啟用此網站通知分頁的通知顯示 停用此網站通知分頁的通知顯示 @@ -1664,15 +1744,15 @@ Language: zh_TW 無法預覽 嘗試在預覽前儲存文章時發生錯誤 正在產生預覽… - 這篇文章有未儲存的變更 正在儲存… + 這篇文章有未儲存的變更 此應用程式的版本 其他裝置的版本 + 從此應用程式\n已儲存於 %1$s\n\n從其他裝置\n已儲存於 %2$s\n 你最近變更了這篇文章,但沒有儲存這些變更。選擇要載入的版本:\n\n 你想要編輯哪一個版本? 永久刪除 我們不會將最近的變更儲存至草稿。 - 從此應用程式\n已儲存於 %1$s\n\n從其他裝置\n已儲存於 %2$s\n 我們不會排程這些變更。 我們不會將這些變更提交送審。 我們不會發表這些變更。 @@ -1718,9 +1798,9 @@ Language: zh_TW 分享 返回 下一個 + 「%1$s」已排程透過你的 %3$s 應用程式發表到「%2$s」\n %4$s WordPress 已排程的文章:「%s」 「%s」將會在 10 分鐘後發表 - 「%1$s」已排程透過你的 %3$s 應用程式發表到「%2$s」\n %4$s 「%s」將會在 1 小時後發表 「%s」已發表 已排程的文章:10 分鐘提醒 @@ -1816,27 +1896,26 @@ Language: zh_TW 隱私權保護 請輸入有效的 %s 新增 + 關閉 立即試試 選擇想查看的統計資料,並專注於你最在乎的資料。點選「洞察報告」底端的 %1$s,自訂你的統計資料。 管理你的統計資料 - 關閉 正在擷取修訂版本… 無法插入媒體。\n請點選以顯示選項。 無法插入媒體。\n請點選以重試。 你的草稿正在上傳中… 正在上傳草稿 - 還原文章時發生錯誤 草稿 + 還原文章時發生錯誤 回溯至:%s 只查看最相關的統計資料。在下方新增及管理你的洞察報告。 社交 年度網站統計 - 註冊網域 - 現在 Jetpack 已安裝完成,我們只需要協助你完成設定即可。這只需要一分鐘左右。 追蹤者總數 - 輸入關鍵字尋找更多想法 無法載入網域建議 + 輸入關鍵字尋找更多想法 找不到任何建議 + 註冊網域 從洞察報告中移除 向下移動 向上移動 @@ -1845,8 +1924,8 @@ Language: zh_TW 正在還原文章 文章已還原 正在將文章移至垃圾桶 - 本機變更 將這篇文章移至垃圾桶也會捨棄未儲存的變更,確定要繼續此操作? + 本機變更 移至草稿 切換至清單檢視 切換至卡片檢視 @@ -1871,6 +1950,7 @@ Language: zh_TW 註冊網域 若要安裝外掛程式,你的自訂網域必須與你的網站建立關聯。 安裝外掛程式 + 你可以稍後自訂網站的外觀和風格 發表於:%s 排程於:%s 已發表於:%s @@ -1881,7 +1961,6 @@ Language: zh_TW 期間 月份與年份 載入更多 - 你可以稍後自訂網站的外觀和風格 今天 最佳時段 最好的一天 @@ -1902,20 +1981,20 @@ Language: zh_TW 目前無法載入「方案」,請稍後再試一次。 無法載入「方案」 沒有網路連線 + 切換至區塊編輯器 載入資料時發生問題,請重新整理網頁並重試。 未載入資料 使用區塊編輯器編輯新文章和網頁 使用區塊編輯器 - 切換至區塊編輯器 結束 - 後續步驟 - 你的訪客會在他們的瀏覽器上看到你的圖示。新增自訂圖示會讓網站看起來更專業幹練。 讀者持續成長 自訂你的網站 + 後續步驟 選擇專屬網站圖示 + 你的訪客會在他們的瀏覽器上看到你的圖示。新增自訂圖示會讓網站看起來更專業幹練。 + 選取「%1$s 統計資料 %2$s」以查看網站成效。 點選%1$s你的網站圖示%2$s以上傳新圖示 撰寫草稿並發表文章。 - 選取「%1$s 統計資料 %2$s」以查看網站成效。 啟用文章分享功能 自動將新文章分享到社交媒體帳號。 檢視網站統計資料 @@ -1927,8 +2006,8 @@ Language: zh_TW 提醒 選取下個期間 選取上個期間 - 最熱門的時間 %1$s 的瀏覽次數 + 最熱門的時間 %1$s (%2$s) +%1$s (%2$s) 顯示預覽網站 @@ -1958,12 +2037,14 @@ Language: zh_TW 正在更新文章 捨棄網頁版 捨棄本機版 + 本機\n已儲存於 %1$s\n\n網頁\n已儲存於 %2$s\n 此文章的兩個版本有所衝突。選取你要捨棄的版本。\n\n 解決同步衝突 - 本機\n已儲存於 %1$s\n\n網頁\n已儲存於 %2$s\n 此期間沒有資料 從媒體移除位置資訊 目前我們無法開啟統計資料。請稍後再試一次 + 沒有符合搜尋條件的媒體 + 搜尋 GIF 以新增至你的媒體庫! 瀏覽數 作者 作者 @@ -1993,10 +2074,8 @@ Language: zh_TW 分享文章 建立文章 自從 %2$s 發表後已經過了 %1$s。文章截至目前為止獲得的迴響如下︰ - 沒有符合搜尋條件的媒體 - 搜尋 GIF 以新增至你的媒體庫! - 標籤和類別 自從 %2$s 發表後已經過了 %1$s。分享你的文章,讓這些文章廣為人知,並增加文章瀏覽次數︰ + 標籤和類別 有史以來 %1$s - %2$s 關注者 @@ -2047,8 +2126,8 @@ Language: zh_TW 縮圖 記錄 - 待審中 無法使用選取的網頁 + 待審中 你沒有任何移至垃圾桶的頁面 你沒有任何已排程頁面 你沒有任何草稿頁面 @@ -2082,37 +2161,28 @@ Language: zh_TW 設定上層項目 點選這裡 建立網站 - 清單中又少了一件事情,感覺不錯吧! 讓你的網站上線運作。 + 清單中又少了一件事情,感覺不錯吧! 檢視你的網站 + 預覽你的網站即可查看訪客將會看到的內容。 分享你的網站 - 連結到你的社交媒體帳號 - 你的網站會自動分享新文章。 點選%1$s「分享」%2$s以繼續 點選%1$s「連結」%2$s以新增你的社交媒體帳號 - 預覽你的網站即可查看訪客將會看到的內容。 + 連結到你的社交媒體帳號 - 你的網站會自動分享新文章。 發表文章 點選%1$s建立文章%2$s以建立新文章 - 連結其他網站 不用了,謝謝 + 連結其他網站 執行 取消 現在不要 - 你沒有任何網站 更多 + 你沒有任何網站 沒有關注任何主題 請在此處新增主題,以尋找你最愛的主題文章 請登入你用來連結 Jetpack 的 WordPress.com 帳號。 - 重試 - 發生問題 - 已安裝 Jetpack - 正在安裝 Jetpack - 安裝 Jetpack Jetpack Jetpack 常見問題 - 設定 - 目前無法安裝 Jetpack。 - 正在你的網站上安裝 Jetpack。這可能需要幾分鐘才能完成。 - 我們不會儲存你的網站憑證,而且只會將其用於安裝 Jetpack。 若要在你的 WordPress 網站上使用「統計」功能,必須安裝 Jetpack 外掛程式。 沒有符合你搜尋條件的佈景主題 你想找什麼呢? @@ -2155,13 +2225,13 @@ Language: zh_TW WordPress 未設定 聯絡電子郵件 - 正在還原至 %1$s %2$s 正在還原 + 正在還原至 %1$s %2$s 目前正在還原你的網站 已成功還原你的網站 - 活動記錄動作按鈕 已成功還原你的網站\n(還原至 %1$s %2$s) 正在還原你的網站\n(還原至 %1$s %2$s) + 活動記錄動作按鈕 自動管理 儲存這篇文章,即可隨時返回閱讀。儲存的文章不會同步至其他裝置,因此你只能在這部裝置上存取文章。 儲存文章以供稍後使用 @@ -2177,13 +2247,13 @@ Language: zh_TW 網站位址登入 電子郵件地址登入 點選「%s」即可將文章儲存至你的清單。 + 尚未儲存任何文章! 文章已儲存 檢視全部 從已儲存文章中移除 加入已儲存文章 已儲存文章 已移除 - 尚未儲存任何文章! 變更網站圖示 取消 移除 @@ -2238,8 +2308,8 @@ Language: zh_TW 關注的網站 正在讀取通知裝置的使用者 正在查看圖表的使用者 - 確定要永久刪除此文章? %1$s (位於 %2$s) + 確定要永久刪除此文章? 重要 一般 使用此相片 @@ -2255,6 +2325,7 @@ Language: zh_TW 建立標籤 瀏覽 通知 + 開啟外部連結 顯示更多 圖片 刪除 @@ -2271,14 +2342,13 @@ Language: zh_TW 預覽 音訊 播放影片 + 移至回收桶 重試 媒體預覽,檔案名稱 %s 移除 %s %s 的個人檔案照片 檢查標記 透過 Google 註冊… - 開啟外部連結 - 移至回收桶 無法連結至 Jetpack:%s 你已連結至 Jetpack 視覺化模式 @@ -2314,8 +2384,8 @@ Language: zh_TW 通知 讀取器 - 通知設定 我的網站 + 通知設定 你的大頭貼已上傳,且稍後即可使用。 你似乎已關閉此功能所需的權限。<br/><br/>若要變更此設定,請編輯權限並確認已啟用 <strong>%s</strong>。 權限 @@ -2374,15 +2444,15 @@ Language: zh_TW 正在傳送電子郵件 重試 關閉 + 傳送電子郵件時發生錯誤。你可以現在重試,或先關閉並稍後再試。 使用者名稱 + 你可以像剛剛一樣隨時使用連結來登入,也可以依需要設定密碼。 密碼 (非必要) 顯示名稱 重試 還原 更新你的帳號時發生錯誤。你可以重試或還原變更並繼續。 上傳你的大頭貼時發生錯誤。 - 傳送電子郵件時發生錯誤。你可以現在重試,或先關閉並稍後再試。 - 你可以像剛剛一樣隨時使用連結來登入,也可以依需要設定密碼。 需要更新 搜尋外掛程式 新增 @@ -2425,11 +2495,11 @@ Language: zh_TW 由 %s 提供 變更照片 無法載入外掛程式 + 頁面 管理你的網站標籤 儲存中 正在刪除 是否要永久刪除「%s」標籤? - 頁面 已存在使用此名稱的標籤 新增標籤 說明 @@ -2451,8 +2521,8 @@ Language: zh_TW 刪除 外部連結 外掛程式圖示 - WordPress.org 外掛程式頁面 外掛首頁 + WordPress.org 外掛程式頁面 確定要將 %1$s 從 %2$s 移除?\n\n這將會停用外掛程式,並刪除所有相關的檔案及資料。 移除外掛程式 正在移除 %s… @@ -2548,8 +2618,8 @@ Language: zh_TW 檔案名稱 URL 替代文字 - 閃爍燈 連結網站 + 閃爍燈 震動裝置 選擇音效 提示與音效 @@ -2664,7 +2734,7 @@ Language: zh_TW 文件 圖片 全部 - %1$s 無法存取你的相片。若要解決此問題,請編輯存取權限並開啟 %2$s。 + %1$s 要求存取你的媒體檔案時遭拒絕。 若要解決此問題,請編輯存取權限並開啟「%2$s」。 檢視留言 影片的品質。數值愈高,表示影片品質愈好。 將文章中的影片調整到符合以下大小 @@ -2762,11 +2832,11 @@ Language: zh_TW 已上傳 + 上傳失敗 已刪除 正在刪除 正在上傳 已排進佇列 - 上傳失敗 圖片品質 由於未知的錯誤,所有的媒體上傳均已取消。請重試上傳 未知的文章格式 @@ -2826,47 +2896,47 @@ Language: zh_TW 留言已核准! 立即 - 關注者 瀏覽者 + 關注者 無法連線,故無法儲存你的個人檔案 - - + + 已選取 %1$d 無法擷取網站使用者 - 關注者 電子郵件關注者 + 關注者 正在擷取使用者… - 電子郵件關注者 讀者 + 電子郵件關注者 關注者 團隊 可邀請最多 10 個電子郵件地址及/或 WordPress.com 使用者名稱。尚未命名的使用者,將收到一份如何建立使用者名稱的指示。 如果移除這位瀏覽者,對方將無法造訪此網站。\n\n仍要移除這位瀏覽者嗎? 移除後,這位關注者如果沒有重新關注,就會停止收到此網站的通知。\n\n仍要移除這位關注者嗎? 自 %1$s 開始 - 無法移除關注者 無法移除瀏覽者 + 無法移除關注者 無法擷取網站電子郵件關注者 無法擷取網站關注者 部分媒體上傳失敗。在此狀態下,你無法\n切換至 HTML 模式。要移除所有上傳失敗的項目並繼續嗎? - 視覺化編輯器 縮圖 - 變更已儲存 - 說明 - 替代文字 - 連結至 + 視覺化編輯器 寬度 + 連結至 + 替代文字 + 說明 + 變更已儲存 要捨棄未儲存的變更嗎? 停止上傳? 插入媒體時發生錯誤 你目前正在上傳媒體。請等待此項作業完成。 無法直接在 HTML 模式中插入媒體。請切換回視覺化模式。 正在上傳圖庫… - 已送出邀請但發生錯誤! - 已成功送出邀請 點選以重試! + 已成功送出邀請 %1$s:%2$s + 已送出邀請但發生錯誤! 嘗試傳送邀請時發生錯誤! 無法傳送:使用者名稱或電子郵件無效 無法傳送:使用者名稱或電子郵件無效 @@ -2874,8 +2944,8 @@ Language: zh_TW 自訂訊息 邀請 使用者名稱或電子郵件 - 外部 邀請他人 + 外部 清除搜尋記錄 清除搜尋記錄? 在你的語言中找不到與 %s 相關的結果 @@ -2883,33 +2953,33 @@ Language: zh_TW 相關文章 預覽畫面上的連結已停用 傳送 - 如果移除 %1$s,該使用者將無法再存取此網站,但 %1$s 建立的所有內容仍會保留在網站上。\n\n仍要移除這位使用者嗎? 已成功移除 %1$s + 如果移除 %1$s,該使用者將無法再存取此網站,但 %1$s 建立的所有內容仍會保留在網站上。\n\n仍要移除這位使用者嗎? 移除 %1$s - 此清單中的網站最近未張貼任何文章 - 使用者 角色 + 使用者 + 此清單中的網站最近未張貼任何文章 無法移除使用者 - 無法擷取網站使用者 無法更新使用者角色 + 無法擷取網站使用者 更新你的 Gravatar 時發生錯誤 - 尋找裁切的圖片時發生錯誤 重新載入你的 Gravatar 時發生錯誤 + 尋找裁切的圖片時發生錯誤 裁切圖片時發生錯誤 正在檢查電子郵件 目前無法使用。請輸入你的密碼 正在登入 當你留言時公開顯示。 拍攝或選取照片 - 我們會將你的文章、頁面和設定寄到你的電子郵件地址:%s. - 方案 方案 + 方案 + 我們會將你的文章、頁面和設定寄到你的電子郵件地址:%s. 匯出你的內容 - 匯出內容… 匯出電子郵件已傳送! - 你的網站已啟用進階版升級服務。刪除網站前,請先取消升級。 - 顯示購買項目 + 匯出內容… 檢查購買項目 + 顯示購買項目 + 你的網站已啟用進階版升級服務。刪除網站前,請先取消升級。 進階版升級 發生了點錯誤。無法要求購買項目。 正在刪除網站… @@ -2918,50 +2988,50 @@ Language: zh_TW 主要網域 刪除網站時發生錯誤。請聯絡支援團隊,尋求進一步協助。 刪除網站時發生錯誤 - 請在下方欄位中輸入 %1$s 以確認。系統將永久刪除你的網站。 匯出內容 - 聯絡支援團隊 + 請在下方欄位中輸入 %1$s 以確認。系統將永久刪除你的網站。 確認刪除網站 + 聯絡支援團隊 如果你想保留網站,但不想保留現有的文章和頁面,我們的支援團隊可以為你刪除網站上的文章、頁面、媒體及留言。\n\n這能讓你保留網站和 URL,但重新開始建立內容。若要清除目前的內容,請與我們聯絡。 - 重新開始設計你的網站 我們很樂意協助 - 應用程式設定 + 重新開始設計你的網站 重新開始 + 應用程式設定 移除失敗的上傳項目 - 沒有已移至垃圾桶的留言 進階 + 沒有已移至垃圾桶的留言 無待審核的迴響 沒有已核准的留言 略過 無法連結。伺服器缺少必要的 XML-RPC 方式。 - 狀態 - 影片 置中 - 聊天 - 圖庫 - 圖片 - 連結 - 引文 + 影片 + 狀態 標準版 - WordPress.com 課程與活動相關資訊 (線上和專人洽詢)。 - 旁白 + 引文 + 連結 + 圖片 + 圖庫 + 聊天 音訊 + 旁白 + WordPress.com 課程與活動相關資訊 (線上和專人洽詢)。 參與 WordPress.com 研究與調查的機會。 讓 WordPress.com 發揮最大功效的秘訣。 社群 - 留言的回覆 - 建議 研究 - 網站成就 + 建議 + 留言的回覆 使用者名稱標記 - 文章按讚數 + 網站成就 網站關注數 + 文章按讚數 留言按讚數 網站留言 %d 個項目 1 個項目 - 已知使用者的留言 所有使用者 + 已知使用者的留言 無留言 每頁 %d 則留言 每頁 1 則留言 @@ -2971,11 +3041,11 @@ Language: zh_TW 自動核准所有人的留言。 如果使用者有已審核的留言,則自動核准 需要人工審核所有人的留言。 - 1 天 %d 天 - 按一下電子郵件 (收件者:%1$s) 中的驗證連結以確認您的新地址 - 主要網站 + 1 天 網址 + 主要網站 + 按一下電子郵件 (收件者:%1$s) 中的驗證連結以確認您的新地址 你目前正在上傳媒體。請等待此作業完成。 目前無法重新整理留言 – 顯示較舊的留言 設定精選圖片 @@ -2984,13 +3054,13 @@ Language: zh_TW 永久刪除這些留言? 永久刪除此回應? 刪除 - 留言已刪除 還原 + 留言已刪除 沒有垃圾留言 - 無法載入頁面 全部 - 介面語言 + 無法載入頁面 關閉 + 介面語言 關於此應用程式 無法儲存你的帳號設定 無法擷取你的帳號設定 @@ -2998,20 +3068,20 @@ Language: zh_TW 無法辨識語言代碼 允許留言以階層式嵌入留言串中。 留言串最多 - 移除 - 搜尋 已停用 + 搜尋 + 移除 原始尺寸 只有你和經核准的使用者可看見你的網站 所有人都能看見你的網站,但網站會要求搜尋引擎不要加入索引 所有人都能看見你的網站,且搜尋引擎可能會將網站加入索引 簡單介紹一下自己… - 如果未設定,顯示名稱會預設為你的使用者名稱 關於我 + 如果未設定,顯示名稱會預設為你的使用者名稱 公開顯示名稱 - 我的個人檔案 - 名字 姓氏 + 名字 + 我的個人檔案 相關文章預覽圖片 無法儲存網站資訊 無法擷取網站資訊 @@ -3066,13 +3136,13 @@ Language: zh_TW %d 層 私密 隱藏 - 刪除網站 公開 + 刪除網站 等待審核 留言中的連結 自動核准 - 階層顯示 分頁 + 階層顯示 排序依據 使用者必須登入 必須包含名稱和電子郵件 @@ -3082,22 +3152,22 @@ Language: zh_TW 預設格式 預設類別 位址 - 網站標題 標語 + 網站標題 新文章預設設定 - 帳號 撰寫 - 最新的在前 + 帳號 一般 - 討論 - 隱私 - 相關文章 - 留言 - 以下時間後關閉 + 最新的在前 最舊的在前 + 以下時間後關閉 + 留言 + 相關文章 + 隱私 + 討論 你沒有權限,無法上傳媒體檔案至網站 - 從未 不明 + 從未 此文章已不存在 你未獲授權無法檢視此文章 無法擷取此文章 @@ -3109,22 +3179,22 @@ Language: zh_TW 發生了點錯誤。無法啟用佈景主題 作者:%1$s 感謝你選擇 %1$s - 試用與自訂 - 檢視 - 詳細資料 - 支援 - 完成 管理網站 + 完成 + 支援 + 詳細資料 + 檢視 + 試用與自訂 啟用 - 目前佈景主題 - 自訂 - 詳細資料 - 支援 已啟用 - 文章已發表 - 頁面已發表 - 文章已更新 + 支援 + 詳細資料 + 自訂 + 目前佈景主題 頁面已更新 + 文章已更新 + 頁面已發表 + 文章已發表 很抱歉,找不到佈景主題。 載入更多文章 沒有網站符合「%s」 @@ -3157,203 +3227,203 @@ Language: zh_TW 無法載入通知設定 按讚的留言 應用程式通知 - 通知索引標籤 電子郵件 + 通知索引標籤 我們會隨時傳送與帳號資訊相關的重要電子郵件,除此之外,你也能獲得實用的額外資訊。 最新文章摘要 沒有連線 文章已移至垃圾桶 + 移至回收桶 統計 預覽 檢視 - 編輯 - 移至回收桶 上架 + 編輯 你沒有存取此網站的權限 找不到這個網站 復原 要求已到期。登入 WordPress.com 再試一次。 - 最佳的瀏覽次數 忽略 + 最佳的瀏覽次數 本日統計 有史以來的文章數、瀏覽次數與訪客數 Insights 中斷與 WordPress.com 的連線 連線 WordPress.com 登入/登出 - 由於「%s」是目前的網站,因此並未隱藏 帳號設定 + 由於「%s」是目前的網站,因此並未隱藏 建立 WordPress.com 網站 新增自助託管的網站 - 顯示/隱藏網站 新增網站 - 檢視管理員 - 檢視網站 + 顯示/隱藏網站 選擇網站 + 檢視網站 + 檢視管理員 切換網站 - 外觀和風格 網站設定 文章 發佈 + 外觀和風格 組態 點選以顯示 取消全選 - 顯示 - 隱藏 全部選取 - 語言 - 驗證碼 - 驗證碼無效 + 隱藏 + 顯示 再次登入以繼續操作。 + 驗證碼無效 + 驗證碼 + 語言 無法擷取文章 無法開啟通知 不明搜尋字詞 - 作者 搜尋字詞 + 作者 正在擷取頁面… 正在擷取文章… 正在擷取媒體… 應用程式記錄已複製到剪貼簿 + 這個網站沒有內容 新文章 將文字複製到剪貼簿時發生錯誤 - 這個網站沒有內容 正在上傳文章 - 正在擷取佈景主題… - %1$d 個月 - 一年 %1$d 年 + 一年 + %1$d 個月 一個月 - %1$d 分鐘 - 一小時前 - %1$d 個小時 - 一天 %1$d 天 + 一天 + %1$d 個小時 + 一小時前 + %1$d 分鐘 一分鐘前 幾秒鐘前 - 文章與頁面 - 影片 關注者 + 影片 + 文章與頁面 國家 按讚數 - - 點閱數 訪客 + 點閱數 + + 正在擷取佈景主題… 詳細資料 已選取:%d 瀏覽常見問題集 尚未有留言 - 查看原始文章 + 沒有任何與此主題相關的文章 按讚 + 查看原始文章 留言已關閉 第 %1$d 個,共 %2$d 個 無法發表空白的文章 你沒有查看或編輯文章的權限 你沒有查看或編輯頁面的權限 - 超過一個月 更多 - 超過 2 天 + 超過一個月 超過一週 + 超過 2 天 + 說明與客戶服務 已按讚 留言 留言已移至垃圾桶 - 尚無文章。建立文章? 回覆 %s + 尚無文章。建立文章? 正在登出… - 沒有任何與此主題相關的文章 - 說明與客戶服務 無法執行此動作 無法封鎖此網站 將不再顯示此網站的文章 封鎖此網站 - 更新 排程 - 關注的網站 - 已關注的網站 - 無法顯示此網站 - 你已經關注此網站 - 無法關注此網站 - 無法取消關注此網站 + 更新 沒有推薦的網站 - 讀者網站 - 已關注的主題 + 無法取消關注此網站 + 無法關注此網站 + 你已經關注此網站 + 無法顯示此網站 + 已關注的網站 輸入要關注的 URL 或主題 - 說明 - 無效的 SSL 憑證 + 關注的網站 + 已關注的主題 + 讀者網站 如果你通常能夠順利連線至此網站,則此錯誤可能代表有人正嘗試冒充該網站,因此請勿繼續操作。是否仍然要信任憑證? - 沒有可用的網路 - 無法擷取媒體項目 - 存取此網誌時發生錯誤 - 無法擷取佈景主題 - 非垃圾 - 新增類別失敗 - 類別已成功新增 - 類別名稱欄位為必填 - 需要掛接 SD 卡以上傳媒體 - 沒有通知 - 目前無法重新整理文章 - 目前無法重新整理頁面 - 目前無法重新整理回應 - 發生錯誤 - 審核時發生錯誤 - 編輯回應時發生錯誤 - 無法載入回應 - 下載圖片時發生錯誤 - 你的電子郵件地址無效 - 輸入有效的電子郵件地址 + 無效的 SSL 憑證 + 說明 你輸入的使用者名稱或密碼不正確 + 輸入有效的電子郵件地址 + 你的電子郵件地址無效 + 下載圖片時發生錯誤 + 無法載入回應 + 編輯回應時發生錯誤 + 審核時發生錯誤 + 發生錯誤 + 目前無法重新整理回應 + 目前無法重新整理頁面 + 目前無法重新整理文章 刪除文章時發生錯誤 - 選取類別 - 連線錯誤 - 取消編輯 - 載入文章時發生錯誤。重新整理你的文章,然後再試一次。 - 瞭解更多 - 縮圖格線 - 你沒有檢視媒體庫的權限 - 目前無法刪除部分媒體。請稍後再試一次。 - 連結文字 (選填) - 建立連結 - 頁面設定 - 本機草稿 - 水平對齊 - 文章設定 - 已核准 - 審核中 - 垃圾 - 已移至垃圾桶 - 編輯回應 - 核准 - 駁回 - 垃圾 - 移至垃圾桶? - 正在儲存變更 - 取消編輯此回應? - 必須發表回應 - 回應尚未變更 - 在瀏覽器中檢視 - 新增類別 - 類別名稱 - 無法建立暫存檔進行媒體上傳。確認裝置上有足夠的可用空間。 - 需要授權 - 新文章 - 新媒體 - 本機變更 - 圖片設定 - WordPress 網誌 - 此網誌已隱藏,無法載入。請在設定中啟用網誌,然後再試一次。 - 建立應用程式資料庫時發生錯誤。請嘗試重新安裝應用程式。 + 沒有通知 + 需要掛接 SD 卡以上傳媒體 + 類別名稱欄位為必填 + 類別已成功新增 + 新增類別失敗 + 非垃圾 + 無法擷取佈景主題 + 存取此網誌時發生錯誤 + 無法擷取媒體項目 + 沒有可用的網路 + 無法移除此主題 + 無法新增此主題 應用程式記錄檔 - 移除網站 + 建立應用程式資料庫時發生錯誤。請嘗試重新安裝應用程式。 + 此網誌已隱藏,無法載入。請在設定中啟用網誌,然後再試一次。 + 目前無法重新整理媒體 + WordPress 網誌 + 圖片設定 + 本機變更 + 新媒體 + 新文章 尚未有任何通知。 + 需要授權 檢查輸入的網站 URL 是否有效 - 目前無法重新整理媒體 - 存取此外掛程式時發生錯誤 - 找不到可上傳的檔案。該檔案是否已刪除或移走? - 是否要刪除文章? - 是否要刪除頁面? - 無法新增此主題 - 無法移除此主題 + 無法建立暫存檔進行媒體上傳。確認裝置上有足夠的可用空間。 + 類別名稱 + 新增類別 + 在瀏覽器中檢視 + 移除網站 + 回應尚未變更 + 必須發表回應 + 取消編輯此回應? + 正在儲存變更 移至回收桶 + 移至垃圾桶? 移至垃圾桶 + 垃圾 + 駁回 + 核准 + 編輯回應 + 已移至垃圾桶 + 垃圾 + 審核中 + 已核准 + 是否要刪除頁面? + 是否要刪除文章? + 文章設定 + 找不到可上傳的檔案。該檔案是否已刪除或移走? + 水平對齊 + 本機草稿 + 頁面設定 + 建立連結 + 連結文字 (選填) + 目前無法刪除部分媒體。請稍後再試一次。 + 你沒有檢視媒體庫的權限 + 縮圖格線 + 瞭解更多 + 載入文章時發生錯誤。重新整理你的文章,然後再試一次。 + 存取此外掛程式時發生錯誤 + 取消編輯 + 連線錯誤 + 選取類別 分享連結 正在擷取文章… 你和其他 %,d 個人都說這個讚 @@ -3361,75 +3431,75 @@ Language: zh_TW 你無法在沒有可見網誌的情形下分享至 WordPress 已將留言標記為垃圾 留言未核准 + 無法擷取此文章 你和其他 1 人都說這個讚 - 選取照片 選取影片 - 無法擷取此文章 - 清單是空的 - (未命名) - 追蹤 - 新增 %s - 移除 %s - 分享 - 追蹤 + 選取照片 + 註冊 無法開啟 %s - 1 人說這個讚 - 你說這個讚 - 無法張貼你的回應 - 無法分享 無法檢視圖片 + 無法分享 + 這不是有效的主題 + 你已經關注了此主題 + 無法張貼你的回應 + 你說這個讚 + 1 人說這個讚 + 移除 %s + 新增 %s 回應評論 - 尚未有留言 + 追蹤 + 追蹤 + 分享 轉發 - 註冊 - 你已經關注了此主題 - 這不是有效的主題 - 佈景主題 - 幻燈片 + (未命名) + 尚未有留言 + 清單是空的 + + + + 昨天 + 今天 + 來源網址 + 標籤與分類 + 點擊率 + 統計 + 分享 + 啟用 + 更新失敗 說明 - 標題 副標題 - 方塊 - 磁磚 + 標題 + 幻燈片 圓形 - 更新失敗 - 啟用 - 分享 - 統計 - 點擊率 - 來源網址 - 今天 - 昨天 - - - - 標籤與分類 - 管理 + 磁磚 + 方塊 + 佈景主題 放棄 - 登入 - 回覆已發佈 - %d 個新通知 + 管理 還有 %d 個。 + %d 個新通知 關注者 + 回覆已發佈 + 登入 正在載入… HTTP 密碼 HTTP 使用者帳號 上傳媒體時發生錯誤 錯誤的使用者帳號或密碼。 - 密碼 - 使用者帳號 登入 + 使用者帳號 + 密碼 閱讀器 - 頁面 - 說明(可選) + 文章內容包含圖片 + 選為精選圖片 寬度 + 說明(可選) + 頁面 文章 匿名 - 選為精選圖片 - 文章內容包含圖片 沒有網路連線 - OK 完成 + OK URL 上傳中… 對齊 @@ -3442,27 +3512,27 @@ Language: zh_TW 捷徑名稱不能為空 私人 標題 - 分類 以逗號分隔標籤 + 分類 需要 SD 卡 媒體 分類已成功更新 - 刪除 通過 - + 刪除 更新分類失敗 - 錯誤 - 取消 - 儲存 - 新增 - 分類更新錯誤 - + + 立即發佈 回覆 - - + 預覽 + 分類更新錯誤 + 錯誤 + + 通知設定 - 立即發佈 + 新增 + 儲存 + 取消 一次 兩次 diff --git a/WordPress/src/main/res/values-zh-rTW/strings.xml b/WordPress/src/main/res/values-zh-rTW/strings.xml index a52dd2ba47e7..c16c170b7d01 100644 --- a/WordPress/src/main/res/values-zh-rTW/strings.xml +++ b/WordPress/src/main/res/values-zh-rTW/strings.xml @@ -1,11 +1,94 @@ + 建議<b>解除安裝裝置上的 WordPress 應用程式</b>以避免資料衝突。 + 你似乎仍有安裝 WordPress 應用程式。 + 你的裝置不再需要 WordPress 應用程式了 + 建議<b>解除安裝裝置上的 WordPress 應用程式</b>以避免資料衝突。 + 歡迎使用 Jetpack 應用程式。你可以解除安裝 WordPress 應用程式。 + 移除區塊 + 隱私權和評分 + 播放設定 + 播放列顏色 + 手動 + 動態 + 請說明圖片用途。 如果是為了裝飾,請保留空白。 + 從適合行動裝置瀏覽的可自訂版面開始 + 建立其他頁面 + 在網站上新增頁面 + 隱藏此訊息 + 建立易於搜尋、分享和追蹤的網站位址,打造屬於自己的網路園地。 + 自訂網域讓你擁有專屬的網路身分 + 你需要開啟推播通知才能使用網誌提醒。 + 開啟推播通知 + 繼續使用子網域 + 購買網域 + 相片和影片以及音樂和音訊 + 音樂和音訊 + 相片和影片 + %s 需要權限才能存取你的音訊 + %s 需要權限才能存取你的影片 + %s 需要權限才能存取你的相片 + %s 需要權限才能存取你的相片和影片 + %s 需要權限才能存取你的音樂、音訊、相片和影片 + 開啟通知 + 前往「設定」&rarr;「通知」&rarr;「應用程式設定」,開啟「%1$s」將能立即收到通知。 + 你需要開啟應用程式才能查看通知。 + 推播通知已關閉 + 推播通知已關閉。 + 關閉通知權限警告。 + 修正 + <b>%1$s</b> 正在使用 %2$s 個 Jetpack 個別外掛程式 + <b>%1$s</b> 正在使用 <b>%2$s</b> 外掛程式 + WordPress 應用程式不支援安裝 Jetpack 個別外掛程式的網站。 + <b>%1$s</b> 正在使用 WordPress 應用程式不支援的 Jetpack 個別外掛程式。 + <b>%1$s</b> 正在使用 WordPress 應用程式不支援的 <b>%2$s</b> 外掛程式。 + 無法存取你的某些網站 + 無法存取你其中一個網站 + 請切換至 Jetpack 應用程式,我們會引導你連結完整 Jetpack 外掛程式,即可透過應用程式使用此網站。 + 切換至 Jetpack 應用程式 + %1$s 正在使用 %2$s,目前尚未支援應用程式的所有功能。\n\n若要在此網站使用應用程式,請安裝 %3$s。 + 本網站 + %1$s 正在使用 %2$s,目前尚未支援應用程式的所有功能。 Please install the %3$s. + %1$s 正在使用 %2$s,目前尚未支援應用程式的所有功能。 Please install the %3$s. + Moving to the Jetpack app in a few days. + 切換完全免費,只要 1 分鐘就能完成。 + WordPress 應用程式已移除「統計資料」、「閱讀器」、「通知」和其他 Jetpack 提供的功能。 + 前往 jetpack.com 深入瞭解 + 切換至 Jetpack 應用程式 + %s have moved to the Jetpack app. + %s has moved to the Jetpack app. + WP 管理員 + 管理 + 流量 + 內容 + 設定 + 完成 + 現在 Jetpack 已安裝完成,我們只需要協助你完成設定即可。 這只需要一分鐘左右。 + 立即使用 Blaze 宣傳文章 + 使用 Blaze 宣傳此頁面 + 使用 Blaze 宣傳此文章 + 隨時追蹤成效、開始以及停止使用你的 Blaze。 + 你的內容會出現在數百萬個 WordPress 和 Tumblr 網站上。 + 只要幾分鐘就能宣傳任何文章或頁面;每天只需支付幾美元。 + 使用 Blaze 為你的網站吸引更多流量 + Blaze + 此網域已被註冊 + 特價 + 推薦 + 最佳替代選項 + 說明 + 請查看我們的常見問題集,瞭解你可能想知道的一般問題的解答。 + 感謝你改用 Jetpack 應用程式! + 記錄 + 支援票證 + 免費 + 說明 區塊選單 隱藏此訊息 在數百萬個網站中展示你的作品。 @@ -18,26 +101,24 @@ Language: zh_TW 完整 Jetpack 外掛程式 個別 Jetpack 外掛程式 %1$s 外掛程式 - %1$s 正在使用 %2$s,目前尚未支援應用程式的所有功能。\n\n若要在此網站使用應用程式,請安裝 %3$s。 + %1$s 正在使用 %2$s,目前尚未支援應用程式的所有功能。\n\n若要在此網站使用應用程式,請安裝 %3$s。 請安裝完整 Jetpack 外掛程式 只有一個網站可用,因此你無法變更主要網站。 - 此網站正在使用個別外掛程式,該外掛程式目前尚未支援應用程式的所有功能。 請安裝完整 Jetpack 外掛程式。 - 聯絡支援團隊 - 重試 - 目前無法安裝 Jetpack。 - 發生問題 - 錯誤圖示 - 完成 - 準備好透過應用程式使用此網站了。 - 已安裝 Jetpack - 正在你的網站上安裝 Jetpack。 這可能需要幾分鐘才能完成。 - 正在安裝 Jetpack - 繼續 - 我們不會儲存你的網站憑證,而且只會將其用於安裝 Jetpack。 - 安裝 Jetpack - Jetpack 圖示 + 聯絡支援團隊 + 重試 + 目前無法安裝 Jetpack。 + 發生問題 + 錯誤圖示 + 準備好透過應用程式使用此網站了。 + 已安裝 Jetpack + 正在你的網站上安裝 Jetpack。 這可能需要幾分鐘才能完成。 + 正在安裝 Jetpack + 繼續 + 我們不會儲存你的網站憑證,而且只會將其用於安裝 Jetpack。 + 安裝 Jetpack + Jetpack 圖示 使用 Blaze 進行宣傳 - 充分發揮網站的潛能。 透過 Jetpack 取得統計資料、通知等功能。 + 充分發揮網站的潛能。 透過 Jetpack 取得統計資料、通知等。 你的網站已安裝 Jetpack 外掛程式 Jetpack 行動應用程式設計為可搭配 Jetpack 外掛程式運作。 立即切換以取得統計資料、通知、閱讀器等功能。 接收新留言、按讚、瀏覽次數等內容的通知。 @@ -80,6 +161,7 @@ Language: zh_TW 從 <b>DayOne</b> 隱藏此項目 稍後再提醒我 + 「統計資料」、「閱讀器」、「通知」及其他功能即將搬遷至 Jetpack 行動應用程式。 切換至 Jetpack 應用程式 前往 jetpack.com 深入瞭解 切換完全免費,只要 1 分鐘就能完成。 @@ -131,9 +213,6 @@ Language: zh_TW 在 Jetpack 中開啟連結 需要協助嗎? 知道了 - 請<b>刪除 WordPress 應用程式</b>以避免資料衝突。 - 你似乎仍有安裝 WordPress 應用程式。 建議刪除 WordPress 應用程式以避免資料衝突。 - 你不再需要 WordPress 應用程式了 沒有網路連線的情況下,我們無法轉移你的資料和設定。 請檢查並確認你的網路連線正常,然後再試一次。 無法連線到網際網路。 @@ -143,12 +222,11 @@ Language: zh_TW 再試一次 完成 移除 WordPress 應用程式圖示 - 請<b>刪除 WordPress 應用程式</b>以避免資料衝突。 我們已轉移你的所有資料和設定。 一切全部順暢銜接。 感謝改用 Jetpack! 我們會關閉 WordPress 應用程式的通知。 你將收到所有相同的通知,但來源為 Jetpack 應用程式。 - 請刪除 WordPress 應用程式 + 現在改由 Jetpack 傳送通知 WordPress 說明中心 支援 允許應用程式停用 WordPress 通知。 @@ -727,6 +805,7 @@ Language: zh_TW 重試 GIF + 新增標題 未提供預覽 文字顏色 邊框間距 @@ -735,6 +814,7 @@ Language: zh_TW 自訂 URL 建立嵌入內容 欄 %d + 更多 簡述連結以協助螢幕閱讀器使用者 新增區塊 找不到任何 Jetpack 網站 @@ -1151,7 +1231,6 @@ Language: zh_TW 限時動態文章介紹 已建立空白頁面 已建立頁面 - %1$s 存取你的相片時遭拒絕。 若要解決此問題,請編輯存取權限並開啟 %2$s 和 %3$s。 媒體插入失敗。 媒體插入失敗:%s 從 WordPress 媒體庫選擇 @@ -1631,6 +1710,7 @@ Language: zh_TW 新增圖片或視訊 新增圖片 在此新增區塊 + 新增說明 點選「新增至儲存文章」按鈕即可將文章儲存至你的清單。 「此清單已載入 %1$d 個項目。」 通知 @@ -1836,7 +1916,6 @@ Language: zh_TW 輸入關鍵字尋找更多想法 找不到任何建議 註冊網域 - 現在 Jetpack 已安裝完成,我們只需要協助你完成設定即可。這只需要一分鐘左右。 從洞察報告中移除 向下移動 向上移動 @@ -2102,15 +2181,6 @@ Language: zh_TW 沒有關注任何主題 請在此處新增主題,以尋找你最愛的主題文章 請登入你用來連結 Jetpack 的 WordPress.com 帳號。 - 重試 - 設定 - 目前無法安裝 Jetpack。 - 發生問題 - 已安裝 Jetpack - 正在你的網站上安裝 Jetpack。這可能需要幾分鐘才能完成。 - 正在安裝 Jetpack - 我們不會儲存你的網站憑證,而且只會將其用於安裝 Jetpack。 - 安裝 Jetpack Jetpack Jetpack 常見問題 若要在你的 WordPress 網站上使用「統計」功能,必須安裝 Jetpack 外掛程式。 @@ -2664,7 +2734,7 @@ Language: zh_TW 文件 圖片 全部 - %1$s 無法存取你的相片。若要解決此問題,請編輯存取權限並開啟 %2$s。 + %1$s 要求存取你的媒體檔案時遭拒絕。 若要解決此問題,請編輯存取權限並開啟「%2$s」。 檢視留言 影片的品質。數值愈高,表示影片品質愈好。 將文章中的影片調整到符合以下大小 diff --git a/WordPress/src/main/res/values/colors.xml b/WordPress/src/main/res/values/colors.xml index e96e0f8e3c74..8e70abbab9f5 100644 --- a/WordPress/src/main/res/values/colors.xml +++ b/WordPress/src/main/res/values/colors.xml @@ -130,6 +130,7 @@ @color/blue_50 @color/grey_lighten_30 + @color/black_translucent_20 @color/white_translucent_80 diff --git a/WordPress/src/main/res/values/dashboard_card_styles.xml b/WordPress/src/main/res/values/dashboard_card_styles.xml new file mode 100644 index 000000000000..ff4e799ea7d4 --- /dev/null +++ b/WordPress/src/main/res/values/dashboard_card_styles.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml index a649472a1120..8b147569d957 100644 --- a/WordPress/src/main/res/values/dimens.xml +++ b/WordPress/src/main/res/values/dimens.xml @@ -115,6 +115,7 @@ 48dp 64dp 68dp + 114dp 16dp 160dp -8dp @@ -173,6 +174,7 @@ 10sp 12sp 14sp + 15sp 16sp 18sp 20sp @@ -488,8 +490,6 @@ 250dp 290dp - 30dp - 88dp 64dp @@ -530,7 +530,6 @@ 24dp 48dp - 40dp 68dp 6dp @@ -746,4 +745,9 @@ 15sp 24dp + + 40dp + 16dp + 12dp + diff --git a/WordPress/src/main/res/values/stats_styles.xml b/WordPress/src/main/res/values/stats_styles.xml index ae4774f2c704..b7943b50bb21 100644 --- a/WordPress/src/main/res/values/stats_styles.xml +++ b/WordPress/src/main/res/values/stats_styles.xml @@ -12,15 +12,6 @@ @dimen/margin_extra_medium_large - - - - - - - - - - - - - + + + + + + + + + + - + - + + - + + + + diff --git a/WordPress/src/main/res/values/styles_toolbar.xml b/WordPress/src/main/res/values/styles_toolbar.xml index f9687e3ebc3a..050c6ef44bc0 100644 --- a/WordPress/src/main/res/values/styles_toolbar.xml +++ b/WordPress/src/main/res/values/styles_toolbar.xml @@ -39,16 +39,19 @@ diff --git a/WordPress/src/test/java/org/wordpress/android/TestUtils.kt b/WordPress/src/test/java/org/wordpress/android/TestUtils.kt index d2c4f10ea229..ae5c9c89d2fc 100644 --- a/WordPress/src/test/java/org/wordpress/android/TestUtils.kt +++ b/WordPress/src/test/java/org/wordpress/android/TestUtils.kt @@ -1,6 +1,11 @@ package org.wordpress.android import androidx.lifecycle.LiveData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.mockito.Mockito import org.mockito.kotlin.internal.createInstance import org.wordpress.android.viewmodel.Event @@ -27,3 +32,18 @@ fun LiveData>.eventToList(): MutableList { } return list } + +/** + * Test helper for capturing emitted Flow values during the execution of a certain [testBody] + * + * @param scope should be the test [CoroutineScope] + * @param testBody is the code that will be executed which result in flow emissions + * @return the list of values emitted by the Flow during the executing of [testBody] + */ +suspend fun Flow.testCollect(scope: CoroutineScope, testBody: () -> Unit): List { + val result = mutableListOf() + val job = onEach { result.add(it) }.launchIn(scope) + testBody() + job.cancelAndJoin() + return result +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/accounts/LoginEpilogueViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/accounts/LoginEpilogueViewModelTest.kt index 173bcffa50b5..95acc2d4f38c 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/accounts/LoginEpilogueViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/accounts/LoginEpilogueViewModelTest.kt @@ -8,6 +8,7 @@ import org.mockito.Mock import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginHelper import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.util.BuildConfigWrapper @@ -24,9 +25,17 @@ class LoginEpilogueViewModelTest : BaseUnitTest() { @Mock lateinit var siteStore: SiteStore + @Mock + private lateinit var wpJetpackIndividualPluginHelper: WPJetpackIndividualPluginHelper + @Before fun setUp() { - viewModel = LoginEpilogueViewModel(appPrefsWrapper, buildConfigWrapper, siteStore) + viewModel = LoginEpilogueViewModel( + appPrefsWrapper, + buildConfigWrapper, + siteStore, + wpJetpackIndividualPluginHelper + ) } @Test @@ -262,6 +271,30 @@ class LoginEpilogueViewModelTest : BaseUnitTest() { assertThat(navigationEvents.last()).isInstanceOf(LoginNavigationEvents.ShowNoJetpackSites::class.java) } + @Test + fun `when onSiteListLoaded is invoked then show jetpack individual plugin overlay`() = + test { + val navigationEvents = initObservers().navigationEvents + whenever(wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay()).thenReturn(true) + + viewModel.onSiteListLoaded() + advanceUntilIdle() + + assertThat(navigationEvents.last()).isEqualTo(LoginNavigationEvents.ShowJetpackIndividualPluginOverlay) + } + + @Test + fun `when onSiteListLoaded is invoked then don't show jetpack individual plugin overlay`() = + test { + val navigationEvents = initObservers().navigationEvents + whenever(wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay()).thenReturn(false) + + viewModel.onSiteListLoaded() + advanceUntilIdle() + + assertThat(navigationEvents.lastOrNull()).isNull() + } + private data class Observers(val navigationEvents: List) private fun initObservers(): Observers { diff --git a/WordPress/src/test/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesViewModelTest.kt index 43560b33d49c..7a267b1a8799 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/accounts/login/jetpack/LoginNoSitesViewModelTest.kt @@ -21,6 +21,7 @@ import org.wordpress.android.ui.accounts.UnifiedLoginTracker import org.wordpress.android.ui.accounts.login.jetpack.LoginNoSitesViewModel.State.NoUser import org.wordpress.android.ui.accounts.login.jetpack.LoginNoSitesViewModel.State.ShowUser import org.wordpress.android.ui.accounts.login.jetpack.LoginNoSitesViewModel.UiModel +import org.wordpress.android.util.extensions.getSerializableCompat private const val USERNAME = "username" private const val DISPLAY_NAME = "display_name" @@ -152,11 +153,11 @@ class LoginNoSitesViewModelTest : BaseUnitTest() { } private fun setupInstanceStateForNoUser() { - whenever(savedInstanceState.getSerializable(KEY_STATE)).thenReturn(NoUser) + whenever(savedInstanceState.getSerializableCompat(KEY_STATE)).thenReturn(NoUser) } private fun setupInstanceStateForShowUser() { - whenever(savedInstanceState.getSerializable(KEY_STATE)).thenReturn( + whenever(savedInstanceState.getSerializableCompat(KEY_STATE)).thenReturn( ShowUser( userName = USERNAME, displayName = DISPLAY_NAME, diff --git a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProviderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProviderTest.kt new file mode 100644 index 000000000000..c4dd0da8a655 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/BloggingPromptsPostTagProviderTest.kt @@ -0,0 +1,23 @@ +package org.wordpress.android.ui.bloggingprompts + +import org.junit.Test +import org.wordpress.android.models.ReaderTag +import org.wordpress.android.models.ReaderTagType +import org.wordpress.android.ui.reader.services.post.ReaderPostLogic +import kotlin.test.assertEquals + +class BloggingPromptsPostTagProviderTest { + @Test + fun `Should return the expected ReaderTag when promptIdSearchReaderTag is called`() { + val promptId = 1234 + val expected = ReaderTag( + BloggingPromptsPostTagProvider.promptIdTag(promptId), + BloggingPromptsPostTagProvider.promptIdTag(promptId), + BloggingPromptsPostTagProvider.promptIdTag(promptId), + ReaderPostLogic.formatFullEndpointForTag(BloggingPromptsPostTagProvider.promptIdTag(promptId)), + ReaderTagType.FOLLOWED, + ) + val actual = BloggingPromptsPostTagProvider.promptIdSearchReaderTag(promptId) + assertEquals(expected, actual) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingUiStateMapperTest.kt index 0ee0e11831e4..10b3d0f6eff7 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingUiStateMapperTest.kt @@ -16,12 +16,12 @@ import org.wordpress.android.ui.bloggingprompts.onboarding.BloggingPromptsOnboar import org.wordpress.android.ui.bloggingprompts.onboarding.BloggingPromptsOnboardingUiState.Ready import org.wordpress.android.ui.utils.UiString.UiStringPluralRes import org.wordpress.android.ui.utils.UiString.UiStringRes -import org.wordpress.android.util.config.BloggingPromptsEnhancementsFeatureConfig +import org.wordpress.android.util.config.BloggingPromptsSocialFeatureConfig @ExperimentalCoroutinesApi class BloggingPromptsOnboardingUiStateMapperTest : BaseUnitTest() { @Mock - lateinit var bloggingPromptsEnhancementsFeatureConfig: BloggingPromptsEnhancementsFeatureConfig + lateinit var bloggingPromptsSocialFeatureConfig: BloggingPromptsSocialFeatureConfig private lateinit var classToTest: BloggingPromptsOnboardingUiStateMapper @@ -87,46 +87,46 @@ class BloggingPromptsOnboardingUiStateMapperTest : BaseUnitTest() { @Before fun setUp() { - classToTest = BloggingPromptsOnboardingUiStateMapper(bloggingPromptsEnhancementsFeatureConfig) + classToTest = BloggingPromptsOnboardingUiStateMapper(bloggingPromptsSocialFeatureConfig) } @Test fun `Should return correct Ready state for ONBOARDING type dialog when enhancements are turned off`() { - val enhancementsEnabled = false - whenever(bloggingPromptsEnhancementsFeatureConfig.isEnabled()).thenReturn(enhancementsEnabled) + val socialEnabled = false + whenever(bloggingPromptsSocialFeatureConfig.isEnabled()).thenReturn(socialEnabled) val actual = classToTest.mapReady(ONBOARDING, primaryButtonListener, secondaryButtonListener) - val expected = expectedOnboardingDialogReadyState(enhancementsEnabled) + val expected = expectedOnboardingDialogReadyState(socialEnabled) assertThat(actual).isEqualTo(expected) } @Test fun `Should return correct Ready state for INFORMATION type dialog when enhancements are turned off`() { - val enhancementsEnabled = false - whenever(bloggingPromptsEnhancementsFeatureConfig.isEnabled()).thenReturn(enhancementsEnabled) + val socialEnabled = false + whenever(bloggingPromptsSocialFeatureConfig.isEnabled()).thenReturn(socialEnabled) val actual = classToTest.mapReady(INFORMATION, primaryButtonListener, secondaryButtonListener) - val expected = expectedInformationDialogReadyState(enhancementsEnabled) + val expected = expectedInformationDialogReadyState(socialEnabled) assertThat(actual).isEqualTo(expected) } @Test fun `Should return correct Ready state for ONBOARDING type dialog when enhancements are turned on`() { - val enhancementsEnabled = true - whenever(bloggingPromptsEnhancementsFeatureConfig.isEnabled()).thenReturn(enhancementsEnabled) + val socialEnabled = true + whenever(bloggingPromptsSocialFeatureConfig.isEnabled()).thenReturn(socialEnabled) val actual = classToTest.mapReady(ONBOARDING, primaryButtonListener, secondaryButtonListener) - val expected = expectedOnboardingDialogReadyState(enhancementsEnabled) + val expected = expectedOnboardingDialogReadyState(socialEnabled) assertThat(actual).isEqualTo(expected) } @Test fun `Should return correct Ready state for INFORMATION type dialog when enhancements are turned on`() { - val enhancementsEnabled = true - whenever(bloggingPromptsEnhancementsFeatureConfig.isEnabled()).thenReturn(enhancementsEnabled) + val socialEnabled = true + whenever(bloggingPromptsSocialFeatureConfig.isEnabled()).thenReturn(socialEnabled) val actual = classToTest.mapReady(INFORMATION, primaryButtonListener, secondaryButtonListener) - val expected = expectedInformationDialogReadyState(enhancementsEnabled) + val expected = expectedInformationDialogReadyState(socialEnabled) assertThat(actual).isEqualTo(expected) } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingViewModelTest.kt index 047d62156a45..faab01f437cb 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/bloggingprompts/onboarding/BloggingPromptsOnboardingViewModelTest.kt @@ -35,14 +35,14 @@ import org.wordpress.android.ui.bloggingprompts.onboarding.usecase.SaveFirstBlog import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.utils.UiString.UiStringRes -import org.wordpress.android.util.config.BloggingPromptsEnhancementsFeatureConfig +import org.wordpress.android.util.config.BloggingPromptsSocialFeatureConfig import org.wordpress.android.viewmodel.Event import java.util.Date @ExperimentalCoroutinesApi class BloggingPromptsOnboardingViewModelTest : BaseUnitTest() { - private val bloggingPromptsEnhancementsFeatureConfig: BloggingPromptsEnhancementsFeatureConfig = mock() - private val uiStateMapper = BloggingPromptsOnboardingUiStateMapper(bloggingPromptsEnhancementsFeatureConfig) + private val bloggingPromptsSocialFeatureConfig: BloggingPromptsSocialFeatureConfig = mock() + private val uiStateMapper = BloggingPromptsOnboardingUiStateMapper(bloggingPromptsSocialFeatureConfig) private val siteStore: SiteStore = mock() private val selectedSiteRepository: SelectedSiteRepository = mock() private val bloggingPromptsStore: BloggingPromptsStore = mock() diff --git a/WordPress/src/test/java/org/wordpress/android/ui/bloggingreminders/BloggingRemindersViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/bloggingreminders/BloggingRemindersViewModelTest.kt index e1ab9e5163d1..5ca57407ccf2 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/bloggingreminders/BloggingRemindersViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/bloggingreminders/BloggingRemindersViewModelTest.kt @@ -43,6 +43,7 @@ import org.wordpress.android.ui.utils.ListItemInteraction.Companion import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.ui.utils.UiString.UiStringText +import org.wordpress.android.viewmodel.ResourceProvider import org.wordpress.android.workers.reminder.ReminderConfig.WeeklyReminder import org.wordpress.android.workers.reminder.ReminderScheduler import java.time.DayOfWeek @@ -61,6 +62,9 @@ class BloggingRemindersViewModelTest : BaseUnitTest() { @Mock lateinit var epilogueBuilder: EpilogueBuilder + @Mock + lateinit var notificationsPermissionBuilder: NotificationsPermissionBuilder + @Mock lateinit var daySelectionBuilder: DaySelectionBuilder @@ -75,6 +79,9 @@ class BloggingRemindersViewModelTest : BaseUnitTest() { @Mock lateinit var siteStore: SiteStore + + @Mock + lateinit var resourceProvider: ResourceProvider private lateinit var viewModel: BloggingRemindersViewModel private val siteId = 123 private val hour = 10 @@ -91,12 +98,15 @@ class BloggingRemindersViewModelTest : BaseUnitTest() { prologueBuilder, daySelectionBuilder, epilogueBuilder, + notificationsPermissionBuilder, dayLabelUtils, analyticsTracker, reminderScheduler, BloggingRemindersModelMapper(), - siteStore + siteStore, + resourceProvider ) + viewModel.setPermissionState(hasNotificationsPermission = true, notificationsPermissionAlwaysDenied = true) events = mutableListOf() events = viewModel.isBottomSheetShowing.eventToList() uiState = viewModel.uiState.toList() diff --git a/WordPress/src/test/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandlerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandlerTest.kt index 573a92fe78c9..1c6984ff5c9d 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandlerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/deeplinks/handlers/StatsLinkHandlerTest.kt @@ -11,6 +11,7 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.deeplinks.DeepLinkNavigator.NavigateAction import org.wordpress.android.ui.deeplinks.DeepLinkUriUtils import org.wordpress.android.ui.deeplinks.buildUri +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper import org.wordpress.android.ui.stats.StatsTimeframe.DAY import org.wordpress.android.ui.stats.StatsTimeframe.INSIGHTS import org.wordpress.android.ui.stats.StatsTimeframe.MONTH @@ -26,9 +27,12 @@ class StatsLinkHandlerTest { lateinit var site: SiteModel private lateinit var statsLinkHandler: StatsLinkHandler + @Mock + private lateinit var jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper + @Before fun setUp() { - statsLinkHandler = StatsLinkHandler(deepLinkUriUtils) + statsLinkHandler = StatsLinkHandler(deepLinkUriUtils, jetpackFeatureRemovalPhaseHelper) } @Test diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadViewModelTest.kt index 48f14274e95e..aa6290a639b4 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpack/backup/download/BackupDownloadViewModelTest.kt @@ -42,6 +42,7 @@ import org.wordpress.android.ui.jetpack.common.providers.JetpackAvailableItemsPr import org.wordpress.android.ui.jetpack.usecases.GetActivityLogItemUseCase import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.text.PercentFormatter import org.wordpress.android.util.wizard.WizardManager import org.wordpress.android.util.wizard.WizardNavigationTarget @@ -426,7 +427,7 @@ class BackupDownloadViewModelTest : BaseUnitTest() { private fun startViewModelForProgress() { whenever(savedInstanceState.getInt(KEY_BACKUP_DOWNLOAD_CURRENT_STEP)) .thenReturn(BackupDownloadStep.PROGRESS.id) - whenever(savedInstanceState.getParcelable(KEY_BACKUP_DOWNLOAD_STATE)) + whenever(savedInstanceState.getParcelableCompat(KEY_BACKUP_DOWNLOAD_STATE)) .thenReturn(backupDownloadState) whenever(percentFormatter.format(30)) .thenReturn("30%") @@ -439,7 +440,7 @@ class BackupDownloadViewModelTest : BaseUnitTest() { private fun startViewModelForComplete(backupDownloadState: BackupDownloadState? = null) { whenever(savedInstanceState.getInt(KEY_BACKUP_DOWNLOAD_CURRENT_STEP)) .thenReturn(BackupDownloadStep.COMPLETE.id) - whenever(savedInstanceState.getParcelable(KEY_BACKUP_DOWNLOAD_STATE)) + whenever(savedInstanceState.getParcelableCompat(KEY_BACKUP_DOWNLOAD_STATE)) .thenReturn(backupDownloadState) startViewModel(savedInstanceState) } @@ -447,7 +448,7 @@ class BackupDownloadViewModelTest : BaseUnitTest() { private fun startViewModelForError() { whenever(savedInstanceState.getInt(KEY_BACKUP_DOWNLOAD_CURRENT_STEP)) .thenReturn(BackupDownloadStep.ERROR.id) - whenever(savedInstanceState.getParcelable(KEY_BACKUP_DOWNLOAD_STATE)) + whenever(savedInstanceState.getParcelableCompat(KEY_BACKUP_DOWNLOAD_STATE)) .thenReturn(backupDownloadState) startViewModel(savedInstanceState) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jetpack/restore/RestoreViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpack/restore/RestoreViewModelTest.kt index b676056375cc..0c50149d97b9 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/jetpack/restore/RestoreViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpack/restore/RestoreViewModelTest.kt @@ -60,6 +60,7 @@ import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.utils.HtmlMessageUtils import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.ui.utils.UiString.UiStringText +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.text.PercentFormatter import org.wordpress.android.util.wizard.WizardManager import org.wordpress.android.util.wizard.WizardNavigationTarget @@ -459,7 +460,7 @@ class RestoreViewModelTest : BaseUnitTest() { private fun startViewModelForStep(step: RestoreStep, restoreState: RestoreState? = null) { whenever(savedInstanceState.getInt(KEY_RESTORE_CURRENT_STEP)) .thenReturn(step.id) - whenever(savedInstanceState.getParcelable(KEY_RESTORE_STATE)) + whenever(savedInstanceState.getParcelableCompat(KEY_RESTORE_STATE)) .thenReturn(restoreState ?: this.restoreState) startViewModel(savedInstanceState) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtilTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtilTest.kt index 42c2eb1ca0e4..79d66732c65c 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtilTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalBrandingUtilTest.kt @@ -296,6 +296,16 @@ class JetpackFeatureRemovalBrandingUtilTest { verifyNoInteractions(dateTimeUtilsWrapper) } + @Test + fun `given static posters phase started, all banners and badges should be Jetpack powered`() { + givenPhase(JetpackFeatureRemovalPhase.PhaseStaticPosters) + + val actual = allJpScreens.map(classToTest::getBrandingTextByPhase) + + actual.assertAllMatch(R.string.wp_jetpack_feature_removal_static_posters_phase) + verifyNoInteractions(dateTimeUtilsWrapper) + } + // endregion // region Helpers @@ -305,7 +315,7 @@ class JetpackFeatureRemovalBrandingUtilTest { } private fun whenJpDeadlineIs(daysAway: Int?) { - whenever(jpDeadlineConfig.appConfig.getRemoteFieldConfigValue(any())).thenReturn(daysAway?.toString()) + whenever(jpDeadlineConfig.appConfig.getRemoteFieldConfigValue(any())).thenReturn(daysAway?.toString() ?: "") daysAway?.toLong()?.let { val today = Date(System.currentTimeMillis()) val deadline = Date.from(today.toInstant().atZone(ZoneId.systemDefault()).plusDays(it).toInstant()) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelperTest.kt index 4421ccb9d7d0..133065501cf1 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/JetpackFeatureRemovalPhaseHelperTest.kt @@ -24,6 +24,7 @@ import org.wordpress.android.util.config.JetpackFeatureRemovalPhaseOneConfig import org.wordpress.android.util.config.JetpackFeatureRemovalPhaseThreeConfig import org.wordpress.android.util.config.JetpackFeatureRemovalPhaseTwoConfig import org.wordpress.android.util.config.JetpackFeatureRemovalSelfHostedUsersConfig +import org.wordpress.android.util.config.JetpackFeatureRemovalStaticPostersConfig @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) @@ -49,6 +50,9 @@ class JetpackFeatureRemovalPhaseHelperTest : BaseUnitTest() { @Mock private lateinit var jetpackFeatureRemovalSelfHostedUsersConfig: JetpackFeatureRemovalSelfHostedUsersConfig + @Mock + private lateinit var jetpackFeatureRemovalStaticPostersConfig: JetpackFeatureRemovalStaticPostersConfig + private lateinit var jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper @Before @@ -60,7 +64,8 @@ class JetpackFeatureRemovalPhaseHelperTest : BaseUnitTest() { jetpackFeatureRemovalPhaseThreeConfig, jetpackFeatureRemovalPhaseFourConfig, jetpackFeatureRemovalNewUsersConfig, - jetpackFeatureRemovalSelfHostedUsersConfig + jetpackFeatureRemovalSelfHostedUsersConfig, + jetpackFeatureRemovalStaticPostersConfig ) } @@ -128,6 +133,15 @@ class JetpackFeatureRemovalPhaseHelperTest : BaseUnitTest() { assertEquals(currentPhase, PhaseSelfHostedUsers) } + @Test + fun `given static posters config true, when current phase is fetched, then return static posters config`() { + whenever(jetpackFeatureRemovalStaticPostersConfig.isEnabled()).thenReturn(true) + + val currentPhase = jetpackFeatureRemovalPhaseHelper.getCurrentPhase() + + assertEquals(currentPhase, JetpackFeatureRemovalPhase.PhaseStaticPosters) + } + // site creation phase tests @Test fun `given jetpack app, when current site creation phase is fetched, then return null`() { @@ -155,4 +169,13 @@ class JetpackFeatureRemovalPhaseHelperTest : BaseUnitTest() { assertEquals(currentPhase, PHASE_TWO) } + + @Test + fun `given static posters config true, when current site creation phase is fetched, then return phase two`() { + whenever(jetpackFeatureRemovalStaticPostersConfig.isEnabled()).thenReturn(true) + + val currentPhase = jetpackFeatureRemovalPhaseHelper.getSiteCreationPhase() + + assertEquals(currentPhase, PHASE_TWO) + } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginAnalyticsTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginAnalyticsTrackerTest.kt new file mode 100644 index 000000000000..93b2a71a847c --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginAnalyticsTrackerTest.kt @@ -0,0 +1,36 @@ +package org.wordpress.android.ui.jetpackoverlay.individualplugin + +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper + +class WPJetpackIndividualPluginAnalyticsTrackerTest { + private val analyticsTrackerWrapper: AnalyticsTrackerWrapper = mock() + private val tracker = WPJetpackIndividualPluginAnalyticsTracker(analyticsTrackerWrapper) + + @Test + fun `Should track screen shown when trackScreenShown is called`() { + tracker.trackScreenShown() + verify(analyticsTrackerWrapper).track( + AnalyticsTracker.Stat.WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_SHOWN, emptyMap() + ) + } + + @Test + fun `Should track screen dismissed when trackScreenDismissed is called`() { + tracker.trackScreenDismissed() + verify(analyticsTrackerWrapper).track( + AnalyticsTracker.Stat.WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_DISMISSED, emptyMap() + ) + } + + @Test + fun `Should track install button click when trackInstallButtonClick is called`() { + tracker.trackPrimaryButtonClick() + verify(analyticsTrackerWrapper).track( + AnalyticsTracker.Stat.WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_PRIMARY_TAPPED, emptyMap() + ) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginHelperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginHelperTest.kt new file mode 100644 index 000000000000..bc213323df37 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginHelperTest.kt @@ -0,0 +1,223 @@ +package org.wordpress.android.ui.jetpackoverlay.individualplugin + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.any +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.persistence.JetpackCPConnectedSiteModel +import org.wordpress.android.fluxc.store.SiteStore +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.util.config.AppConfig +import org.wordpress.android.util.config.WPIndividualPluginOverlayFeatureConfig +import org.wordpress.android.util.config.WPIndividualPluginOverlayMaxShownConfig +import java.util.Calendar + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(MockitoJUnitRunner::class) +class WPJetpackIndividualPluginHelperTest : BaseUnitTest() { + @Mock + lateinit var siteStore: SiteStore + + @Mock + lateinit var appPrefs: AppPrefsWrapper + + @Mock + lateinit var wpIndividualPluginOverlayFeatureConfig: WPIndividualPluginOverlayFeatureConfig + + @Mock + lateinit var appConfig: AppConfig + + private lateinit var wpIndividualPluginOverlayMaxShownConfig: WPIndividualPluginOverlayMaxShownConfig + + private lateinit var helper: WPJetpackIndividualPluginHelper + + @Before + fun setUp() { + wpIndividualPluginOverlayMaxShownConfig = WPIndividualPluginOverlayMaxShownConfig(appConfig) + helper = WPJetpackIndividualPluginHelper( + siteStore, + appPrefs, + wpIndividualPluginOverlayFeatureConfig, + wpIndividualPluginOverlayMaxShownConfig, + ) + + whenever(appConfig.getRemoteFieldConfigValue(any())).thenReturn("3") + } + + @Test + fun `GIVEN config is off WHEN shouldShowJetpackIndividualPluginOverlay THEN return false`() = + test { + whenever(wpIndividualPluginOverlayFeatureConfig.isEnabled()).thenReturn(false) + + assertThat(helper.shouldShowJetpackIndividualPluginOverlay()).isFalse + } + + @Test + fun `GIVEN config is on and no problem sites WHEN shouldShowJetpackIndividualPluginOverlay THEN return false`() = + test { + whenever(wpIndividualPluginOverlayFeatureConfig.isEnabled()).thenReturn(true) + whenever(siteStore.getJetpackCPConnectedSites()).thenReturn(emptyList()) + + assertThat(helper.shouldShowJetpackIndividualPluginOverlay()).isFalse + } + + @Test + fun `GIVEN first time WHEN shouldShowJetpackIndividualPluginOverlay THEN returns true`() = + test { + mockBaseConditionsForShowingOverlay() + + // 1st time: should return true + whenever(appPrefs.wpJetpackIndividualPluginOverlayShownCount).thenReturn(0) + whenever(appPrefs.wpJetpackIndividualPluginOverlayLastShownTimestamp).thenReturn(0) + + assertThat(helper.shouldShowJetpackIndividualPluginOverlay()).isTrue + } + + @Test + fun `GIVEN second time, after less than a day WHEN shouldShowJetpackIndividualPluginOverlay THEN returns false`() = + test { + mockBaseConditionsForShowingOverlay() + + // 2nd time, after less than a day: should return false + whenever(appPrefs.wpJetpackIndividualPluginOverlayShownCount).thenReturn(1) + whenever(appPrefs.wpJetpackIndividualPluginOverlayLastShownTimestamp).thenReturn(timeFor(hoursAgo = 20)) + + assertThat(helper.shouldShowJetpackIndividualPluginOverlay()).isFalse + } + + @Test + fun `GIVEN second time, after more than a day WHEN shouldShowJetpackIndividualPluginOverlay THEN returns true`() = + test { + mockBaseConditionsForShowingOverlay() + + // 2nd time, after more than a day: should return true + whenever(appPrefs.wpJetpackIndividualPluginOverlayShownCount).thenReturn(1) + whenever(appPrefs.wpJetpackIndividualPluginOverlayLastShownTimestamp).thenReturn(timeFor(daysAgo = 1)) + + assertThat(helper.shouldShowJetpackIndividualPluginOverlay()).isTrue + } + + @Test + fun `GIVEN third time, after less than 3 days WHEN shouldShowJetpackIndividualPluginOverlay THEN returns false`() = + test { + mockBaseConditionsForShowingOverlay() + + // 3rd time, after less than 3 days: should return false + whenever(appPrefs.wpJetpackIndividualPluginOverlayShownCount).thenReturn(2) + whenever(appPrefs.wpJetpackIndividualPluginOverlayLastShownTimestamp).thenReturn(timeFor(daysAgo = 2)) + + assertThat(helper.shouldShowJetpackIndividualPluginOverlay()).isFalse + } + + @Test + fun `GIVEN third time, after more than 3 days WHEN shouldShowJetpackIndividualPluginOverlay THEN returns true`() = + test { + mockBaseConditionsForShowingOverlay() + + // 3rd time, after more than 3 days: should return true + whenever(appPrefs.wpJetpackIndividualPluginOverlayShownCount).thenReturn(2) + whenever(appPrefs.wpJetpackIndividualPluginOverlayLastShownTimestamp).thenReturn(timeFor(daysAgo = 4)) + + assertThat(helper.shouldShowJetpackIndividualPluginOverlay()).isTrue + } + + @Test + fun `GIVEN fourth time WHEN shouldShowJetpackIndividualPluginOverlay THEN returns false`() = + test { + mockBaseConditionsForShowingOverlay() + + // 4th time, after many days: should return false + whenever(appPrefs.wpJetpackIndividualPluginOverlayShownCount).thenReturn(3) + + assertThat(helper.shouldShowJetpackIndividualPluginOverlay()).isFalse + } + + @Test + fun `GIVEN no problem sites WHEN getJetpackConnectedSitesWithIndividualPlugins THEN return empty list`() = test { + whenever(siteStore.getJetpackCPConnectedSites()).thenReturn(emptyList()) + + assertThat(helper.getJetpackConnectedSitesWithIndividualPlugins()).isEmpty() + } + + @Test + fun `GIVEN has problem sites WHEN getJetpackConnectedSitesWithIndividualPlugins THEN return list of sites`() = + test { + val connectedSites = listOf( + jetpackCPConnectedSiteModel( + name = "site1", + url = "https://site1.com", + activeJpPlugins = "jetpack-social" + ), + jetpackCPConnectedSiteModel( + name = "site2", + url = "https://site2.com", + activeJpPlugins = "other-plugin" + ) + ) + whenever(siteStore.getJetpackCPConnectedSites()).thenReturn(connectedSites) + + val sites = helper.getJetpackConnectedSitesWithIndividualPlugins() + assertThat(sites).hasSize(1) + assertThat(sites[0].name).isEqualTo("site1") + assertThat(sites[0].url).isEqualTo("site1.com") + assertThat(sites[0].individualPluginNames).hasSize(1) + assertThat(sites[0].individualPluginNames[0]).isEqualTo("Jetpack Social") + } + + @Test + fun `WHEN onJetpackIndividualPluginOverlayShown THEN app prefs is called to increment count`() { + helper.onJetpackIndividualPluginOverlayShown() + + verify(appPrefs).incrementWPJetpackIndividualPluginOverlayShownCount() + } + + @Test + fun `WHEN onJetpackIndividualPluginOverlayShown THEN app prefs is called to update timestamp`() { + helper.onJetpackIndividualPluginOverlayShown() + + verify(appPrefs).wpJetpackIndividualPluginOverlayLastShownTimestamp = any() + } + + private suspend fun mockBaseConditionsForShowingOverlay() { + whenever(wpIndividualPluginOverlayFeatureConfig.isEnabled()).thenReturn(true) + val connectedSites = listOf( + jetpackCPConnectedSiteModel( + name = "site1", + url = "https://site1.com", + activeJpPlugins = "jetpack-social" + ), + jetpackCPConnectedSiteModel( + name = "site2", + url = "https://site2.com", + activeJpPlugins = "other-plugin" + ) + ) + whenever(siteStore.getJetpackCPConnectedSites()).thenReturn(connectedSites) + } + + private fun jetpackCPConnectedSiteModel(name: String, url: String, activeJpPlugins: String?) = + JetpackCPConnectedSiteModel( + remoteSiteId = null, + localSiteId = 0, + url = url, + name = name, + description = "description", + activeJetpackConnectionPlugins = activeJpPlugins?.split(",")?.toList() ?: listOf() + ) + + private fun timeFor(daysAgo: Int = 0, hoursAgo: Int = 0): Long { + val calendar = Calendar.getInstance() + // subtract a minute to make sure the time is a bit longer ago than the current time + calendar.add(Calendar.MINUTE, -1) + calendar.add(Calendar.DAY_OF_YEAR, -daysAgo) + calendar.add(Calendar.HOUR_OF_DAY, -hoursAgo) + return calendar.timeInMillis + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginViewModelTest.kt new file mode 100644 index 000000000000..d20b0f979af7 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackoverlay/individualplugin/WPJetpackIndividualPluginViewModelTest.kt @@ -0,0 +1,110 @@ +package org.wordpress.android.ui.jetpackoverlay.individualplugin + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.testCollect +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginViewModel.ActionEvent +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginViewModel.UiState + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(MockitoJUnitRunner::class) +class WPJetpackIndividualPluginViewModelTest : BaseUnitTest() { + @Mock + lateinit var helper: WPJetpackIndividualPluginHelper + + @Mock + lateinit var tracker: WPJetpackIndividualPluginAnalyticsTracker + + private lateinit var viewModel: WPJetpackIndividualPluginViewModel + + @Before + fun setUp() { + viewModel = WPJetpackIndividualPluginViewModel(helper, tracker, testDispatcher()) + } + + @Test + fun `WHEN onScreenShown THEN UI state is updated only once`() = test { + whenever(helper.getJetpackConnectedSitesWithIndividualPlugins()).thenReturn(connectedSites) + val uiStates = viewModel.uiState.testCollect(this) { + viewModel.onScreenShown() + viewModel.onScreenShown() + viewModel.onScreenShown() + } + + assertThat(uiStates).hasSize(2) + assertThat(uiStates[0]).isEqualTo(UiState.None) + assertThat(uiStates[1]).isEqualTo(UiState.Loaded(connectedSites)) + } + + @Test + fun `WHEN onScreenShown THEN overlay shown helper method is called only once`() = test { + whenever(helper.getJetpackConnectedSitesWithIndividualPlugins()).thenReturn(connectedSites) + viewModel.onScreenShown() + viewModel.onScreenShown() + viewModel.onScreenShown() + + verify(helper).onJetpackIndividualPluginOverlayShown() + } + + @Test + fun `WHEN onScreenShown THEN analytics event is tracked only once`() = test { + whenever(helper.getJetpackConnectedSitesWithIndividualPlugins()).thenReturn(connectedSites) + viewModel.onScreenShown() + viewModel.onScreenShown() + viewModel.onScreenShown() + + verify(tracker).trackScreenShown() + } + + @Test + fun `WHEN onDismissScreenClick THEN emit appropriate event`() = test { + val result = viewModel.actionEvents.testCollect(this) { + viewModel.onDismissScreenClick() + } + + assertThat(result).hasSize(1) + assertThat(result.first()).isEqualTo(ActionEvent.Dismiss) + } + + @Test + fun `WHEN onDismissScreenClick THEN analytics event is tracked`() = test { + viewModel.onDismissScreenClick() + + verify(tracker).trackScreenDismissed() + } + + @Test + fun `WHEN onPrimaryButtonClick THEN emit appropriate event`() = test { + val result = viewModel.actionEvents.testCollect(this) { + viewModel.onPrimaryButtonClick() + } + + assertThat(result).hasSize(1) + assertThat(result.first()).isEqualTo(ActionEvent.PrimaryButtonClick) + } + + @Test + fun `WHEN onPrimaryButtonClick THEN analytics event is tracked`() = test { + viewModel.onPrimaryButtonClick() + + verify(tracker).trackPrimaryButtonClick() + } + + companion object { + private val connectedSites = listOf( + SiteWithIndividualJetpackPlugins( + name = "Site 1", + url = "site1.wordpress.com", + individualPluginNames = listOf("Jetpack Social") + ), + ) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/GetShowJetpackFullPluginInstallOnboardingUseCaseTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/GetShowJetpackFullPluginInstallOnboardingUseCaseTest.kt similarity index 97% rename from WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/GetShowJetpackFullPluginInstallOnboardingUseCaseTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/GetShowJetpackFullPluginInstallOnboardingUseCaseTest.kt index e6198b5eda22..1ea37bbf86b8 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/GetShowJetpackFullPluginInstallOnboardingUseCaseTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/GetShowJetpackFullPluginInstallOnboardingUseCaseTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall +package org.wordpress.android.ui.jetpackplugininstall.fullplugin import org.assertj.core.api.Assertions.assertThat import org.junit.Test diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallAnalyticsTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallAnalyticsTrackerTest.kt similarity index 78% rename from WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallAnalyticsTrackerTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallAnalyticsTrackerTest.kt index 7d464e6f4257..a7dff6861b5b 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallAnalyticsTrackerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallAnalyticsTrackerTest.kt @@ -1,10 +1,10 @@ -package org.wordpress.android.ui.jpfullplugininstall.install +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.install import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.wordpress.android.analytics.AnalyticsTracker.Stat -import org.wordpress.android.ui.jpfullplugininstall.install.JetpackFullPluginInstallAnalyticsTracker.Status +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.install.JetpackFullPluginInstallAnalyticsTracker.Status import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper class JetpackFullPluginInstallAnalyticsTrackerTest { @@ -14,12 +14,22 @@ class JetpackFullPluginInstallAnalyticsTrackerTest { ) @Test - fun `Should track screen status when trackScreenShown is called`() { + fun `Should track screen status when trackScreenShown is called without description`() { classToTest.trackScreenShown(Status.Initial) verify(analyticsTrackerWrapper) .track(Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_VIEWED, mapOf("status" to "initial")) } + @Test + fun `Should track screen status when trackScreenShown is called with description`() { + classToTest.trackScreenShown(Status.Error, "description") + verify(analyticsTrackerWrapper) + .track( + Stat.JETPACK_INSTALL_FULL_PLUGIN_FLOW_VIEWED, + mapOf("status" to "error", "description" to "description") + ) + } + @Test fun `Should track cancel button clicked when trackCancelButtonClicked is called`() { classToTest.trackCancelButtonClicked(Status.Loading) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallUiStateMapperTest.kt similarity index 57% rename from WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallUiStateMapperTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallUiStateMapperTest.kt index 49dbc61fa32b..d8a9d69e43ee 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallUiStateMapperTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.install +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.install import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -11,14 +11,14 @@ class JetpackFullPluginInstallUiStateMapperTest { fun `Should map Initial state correctly`() { val actual = classToTest.mapInitial() with(actual) { - assertThat(buttonText).isEqualTo(R.string.jetpack_full_plugin_install_initial_button) + assertThat(buttonText).isEqualTo(R.string.jetpack_plugin_install_initial_button) assertThat(toolbarTitle).isEqualTo(R.string.jetpack) assertThat(image).isEqualTo(R.drawable.ic_jetpack_logo_green_24dp) assertThat(imageContentDescription).isEqualTo( - R.string.jetpack_full_plugin_install_jp_logo_content_description + R.string.jetpack_plugin_install_jp_logo_content_description ) - assertThat(title).isEqualTo(R.string.jetpack_full_plugin_install_initial_title) - assertThat(description).isEqualTo(R.string.jetpack_full_plugin_install_initial_description) + assertThat(title).isEqualTo(R.string.jetpack_plugin_install_initial_title) + assertThat(description).isEqualTo(R.string.jetpack_plugin_install_initial_description) assertThat(showCloseButton).isTrue } } @@ -30,10 +30,10 @@ class JetpackFullPluginInstallUiStateMapperTest { assertThat(toolbarTitle).isEqualTo(R.string.jetpack) assertThat(image).isEqualTo(R.drawable.ic_jetpack_logo_green_24dp) assertThat(imageContentDescription).isEqualTo( - R.string.jetpack_full_plugin_install_jp_logo_content_description + R.string.jetpack_plugin_install_jp_logo_content_description ) - assertThat(title).isEqualTo(R.string.jetpack_full_plugin_install_initial_title) - assertThat(description).isEqualTo(R.string.jetpack_full_plugin_install_initial_description) + assertThat(title).isEqualTo(R.string.jetpack_plugin_install_installing_title) + assertThat(description).isEqualTo(R.string.jetpack_plugin_install_installing_description) assertThat(showCloseButton).isFalse } } @@ -42,14 +42,14 @@ class JetpackFullPluginInstallUiStateMapperTest { fun `Should map Done state correctly`() { val actual = classToTest.mapDone() with(actual) { - assertThat(buttonText).isEqualTo(R.string.jetpack_full_plugin_install_done_button) + assertThat(buttonText).isEqualTo(R.string.jetpack_plugin_install_full_plugin_done_button) assertThat(toolbarTitle).isEqualTo(R.string.jetpack) assertThat(image).isEqualTo(R.drawable.ic_jetpack_logo_green_24dp) assertThat(imageContentDescription).isEqualTo( - R.string.jetpack_full_plugin_install_jp_logo_content_description + R.string.jetpack_plugin_install_jp_logo_content_description ) - assertThat(title).isEqualTo(R.string.jetpack_full_plugin_install_done_title) - assertThat(description).isEqualTo(R.string.jetpack_full_plugin_install_done_description) + assertThat(title).isEqualTo(R.string.jetpack_plugin_install_done_title) + assertThat(description).isEqualTo(R.string.jetpack_plugin_install_full_plugin_done_description) assertThat(showCloseButton).isFalse } } @@ -58,17 +58,17 @@ class JetpackFullPluginInstallUiStateMapperTest { fun `Should map Error state correctly`() { val actual = classToTest.mapError() with(actual) { - assertThat(retryButtonText).isEqualTo(R.string.jetpack_full_plugin_install_error_button_retry) + assertThat(retryButtonText).isEqualTo(R.string.jetpack_plugin_install_error_button_retry) assertThat(contactSupportButtonText).isEqualTo( - R.string.jetpack_full_plugin_install_error_button_contact_support + R.string.jetpack_plugin_install_error_button_contact_support ) assertThat(toolbarTitle).isEqualTo(R.string.jetpack) assertThat(image).isEqualTo(R.drawable.ic_warning) assertThat(imageContentDescription).isEqualTo( - R.string.jetpack_full_plugin_install_error_image_content_description + R.string.jetpack_plugin_install_error_image_content_description ) - assertThat(title).isEqualTo(R.string.jetpack_full_plugin_install_error_title) - assertThat(description).isEqualTo(R.string.jetpack_full_plugin_install_error_description) + assertThat(title).isEqualTo(R.string.jetpack_plugin_install_error_title) + assertThat(description).isEqualTo(R.string.jetpack_plugin_install_error_description) assertThat(showCloseButton).isTrue } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallViewModelTest.kt similarity index 89% rename from WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallViewModelTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallViewModelTest.kt index ef8ecb8f8372..6941cc6d5828 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/install/JetpackFullPluginInstallViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/install/JetpackFullPluginInstallViewModelTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.install +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.install import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collectLatest @@ -19,7 +19,8 @@ import org.wordpress.android.fluxc.annotations.action.Action import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.PluginStore import org.wordpress.android.ui.accounts.HelpActivity -import org.wordpress.android.ui.jpfullplugininstall.install.JetpackFullPluginInstallAnalyticsTracker.* +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.install.JetpackFullPluginInstallAnalyticsTracker.Status +import org.wordpress.android.ui.jetpackplugininstall.install.UiState import org.wordpress.android.ui.mysite.SelectedSiteRepository @ExperimentalCoroutinesApi @@ -141,9 +142,20 @@ class JetpackFullPluginInstallViewModelTest : BaseUnitTest() { } @Test - fun `Should track error screen shown when onErrorShown is called`() { + fun `Should track error screen shown when onErrorShown is called without description`() { classToTest.onErrorShown() - verify(analyticsTracker).trackScreenShown(Status.Error) + verify(analyticsTracker).trackScreenShown(Status.Error, null) + } + + @Test + fun `Should track error screen shown when onErrorShown is called with description`() { + val event = PluginStore.OnSitePluginInstalled(null, null).apply { + error = PluginStore.InstallSitePluginError("GENERIC_ERROR", "description") + } + + classToTest.onSitePluginInstalled(event) + classToTest.onErrorShown() + verify(analyticsTracker).trackScreenShown(Status.Error, "GENERIC_ERROR: description") } @Test diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTrackerTest.kt similarity index 94% rename from WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTrackerTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTrackerTest.kt index 20f13f7c0710..024b3a01e8c0 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTrackerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingAnalyticsTrackerTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.onboarding +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding import org.junit.Test import org.mockito.kotlin.mock diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/JetpackFullPluginInstallOnboardingUiStateMapperTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingUiStateMapperTest.kt similarity index 76% rename from WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/JetpackFullPluginInstallOnboardingUiStateMapperTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingUiStateMapperTest.kt index efa267f5310c..8d713fe16f29 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/JetpackFullPluginInstallOnboardingUiStateMapperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingUiStateMapperTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Test @@ -6,18 +6,17 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingUiStateMapper -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.UiState +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.UiState import org.wordpress.android.ui.mysite.SelectedSiteRepository import kotlin.test.assertEquals @ExperimentalCoroutinesApi class JetpackFullPluginInstallOnboardingUiStateMapperTest : BaseUnitTest() { private val selectedSiteRepository: SelectedSiteRepository = mock() - private val selectedSiteName = "Site name" + private val selectedSiteUrl = "wordpress.com" private val selectedPluginNames = "jetpack-search,jetpack-backup" private val selectedSiteModel: SiteModel = SiteModel().apply { - name = selectedSiteName + url = selectedSiteUrl activeJetpackConnectionPlugins = selectedPluginNames } private val classToTest = JetpackFullPluginInstallOnboardingUiStateMapper( @@ -28,7 +27,7 @@ class JetpackFullPluginInstallOnboardingUiStateMapperTest : BaseUnitTest() { fun `Should return correct Loaded state when mapLoaded is called`() { mockSelectedSite() val expected = UiState.Loaded( - siteName = selectedSiteName, + siteUrl = selectedSiteUrl, pluginNames = listOf("Jetpack Search", "Jetpack VaultPress Backup") ) val actual = classToTest.mapLoaded() diff --git a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingViewModelTest.kt similarity index 90% rename from WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingViewModelTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingViewModelTest.kt index 4cdd4375dea0..46f925a14604 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/jpfullplugininstall/onboarding/JetpackFullPluginInstallOnboardingViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/fullplugin/onboarding/JetpackFullPluginInstallOnboardingViewModelTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.jpfullplugininstall.onboarding +package org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collectLatest @@ -11,8 +11,8 @@ import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.ui.accounts.HelpActivity -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent -import org.wordpress.android.ui.jpfullplugininstall.onboarding.JetpackFullPluginInstallOnboardingViewModel.UiState +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.ActionEvent +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.onboarding.JetpackFullPluginInstallOnboardingViewModel.UiState import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.ui.prefs.AppPrefsWrapper @@ -23,15 +23,15 @@ class JetpackFullPluginInstallOnboardingViewModelTest : BaseUnitTest() { private val analyticsTracker: JetpackFullPluginInstallOnboardingAnalyticsTracker = mock() private val appPrefsWrapper: AppPrefsWrapper = mock() - private val siteName = "Site Name" + private val siteUrl = "wordpress.com" private val pluginNames = listOf("jetpack-search", "jetpack-backup") private val selectedSite = SiteModel().apply { id = 1 - name = siteName + url = siteUrl activeJetpackConnectionPlugins = pluginNames.joinToString(",") } private val loadedUiState = UiState.Loaded( - siteName = siteName, + siteUrl = siteUrl, pluginNames = pluginNames, ) private val classToTest = JetpackFullPluginInstallOnboardingViewModel( @@ -52,7 +52,7 @@ class JetpackFullPluginInstallOnboardingViewModelTest : BaseUnitTest() { mockUiStateMapper() classToTest.onScreenShown() val loadedUiState = classToTest.uiState.value as UiState.Loaded - assertThat(loadedUiState.siteName).isEqualTo(siteName) + assertThat(loadedUiState.siteUrl).isEqualTo(siteUrl) assertThat(loadedUiState.pluginNames).isEqualTo(pluginNames) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/JetpackRemoteInstallViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/remoteplugin/JetpackRemoteInstallViewModelTest.kt similarity index 57% rename from WordPress/src/test/java/org/wordpress/android/ui/JetpackRemoteInstallViewModelTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/remoteplugin/JetpackRemoteInstallViewModelTest.kt index 9051e83f74b1..144e2a7bb49b 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/JetpackRemoteInstallViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/jetpackplugininstall/remoteplugin/JetpackRemoteInstallViewModelTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui +package org.wordpress.android.ui.jetpackplugininstall.remoteplugin import kotlinx.coroutines.ExperimentalCoroutinesApi import org.assertj.core.api.Assertions.assertThat @@ -6,6 +6,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.argumentCaptor @@ -14,6 +15,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.action.JetpackAction import org.wordpress.android.fluxc.annotations.action.Action @@ -24,11 +26,10 @@ import org.wordpress.android.fluxc.store.JetpackStore.JetpackInstallError import org.wordpress.android.fluxc.store.JetpackStore.JetpackInstallErrorType import org.wordpress.android.fluxc.store.JetpackStore.OnJetpackInstalled import org.wordpress.android.fluxc.store.SiteStore -import org.wordpress.android.ui.JetpackRemoteInstallViewModel.JetpackResultActionData -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Error -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Installed -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Installing -import org.wordpress.android.ui.JetpackRemoteInstallViewState.Start +import org.wordpress.android.ui.JetpackConnectionSource +import org.wordpress.android.ui.JetpackConnectionUtils +import org.wordpress.android.ui.jetpackplugininstall.install.UiState +import org.wordpress.android.ui.jetpackplugininstall.remoteplugin.JetpackRemoteInstallViewModel.JetpackResultActionData @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) @@ -51,7 +52,7 @@ class JetpackRemoteInstallViewModelTest : BaseUnitTest() { private val siteId = 1 private lateinit var viewModel: JetpackRemoteInstallViewModel - private val viewStates = mutableListOf() + private val viewStates = mutableListOf() private var jetpackResultActionData: JetpackResultActionData? = null @Before @@ -67,16 +68,16 @@ class JetpackRemoteInstallViewModelTest : BaseUnitTest() { @Test fun `on click starts jetpack install`() = test { - viewModel.start(site, null) + viewModel.initialize(site, null) - val startState = viewStates[0] - assertStartState(startState) + val initialState = viewStates[0] + initialState.assertInitialState() // Trigger install - startState.onClick() + viewModel.onInitialButtonClick() val installingState = viewStates[1] - assertInstallingState(installingState) + installingState.assertInstallingState() verify(dispatcher).dispatch(actionCaptor.capture()) assertThat(actionCaptor.lastValue.type).isEqualTo(JetpackAction.INSTALL_JETPACK) @@ -88,17 +89,17 @@ class JetpackRemoteInstallViewModelTest : BaseUnitTest() { val updatedSite = mock() whenever(siteStore.getSiteByLocalId(siteId)).thenReturn(updatedSite) whenever(accountStore.hasAccessToken()).thenReturn(true) - viewModel.start(site, null) + viewModel.initialize(site, null) - val startState = viewStates[0] - assertStartState(startState) + val initialState = viewStates[0] + initialState.assertInitialState() viewModel.onEventsUpdated(OnJetpackInstalled(true, JetpackAction.INSTALL_JETPACK)) val installedState = viewStates[1] - assertInstalledState(installedState) + installedState.assertDoneState() // Continue after Jetpack is installed - installedState.onClick() + viewModel.onDoneButtonClick() val connectionData = jetpackResultActionData!! assertThat(connectionData.loggedIn).isTrue @@ -109,17 +110,17 @@ class JetpackRemoteInstallViewModelTest : BaseUnitTest() { @Test fun `on error result shows failure`() = test { val installError = JetpackInstallError(JetpackInstallErrorType.GENERIC_ERROR, "error") - viewModel.start(site, null) + viewModel.initialize(site, null) - val startState = viewStates[0] - assertStartState(startState) + val initialState = viewStates[0] + initialState.assertInitialState() viewModel.onEventsUpdated(OnJetpackInstalled(installError, JetpackAction.INSTALL_JETPACK)) val errorState = viewStates[1] - assertErrorState(errorState) + errorState.assertErrorState() - errorState.onClick() + viewModel.onRetryButtonClick() verify(dispatcher).dispatch(actionCaptor.capture()) assertThat(actionCaptor.lastValue.type).isEqualTo(JetpackAction.INSTALL_JETPACK) @@ -133,10 +134,10 @@ class JetpackRemoteInstallViewModelTest : BaseUnitTest() { "INVALID_CREDENTIALS", message = "msg" ) - viewModel.start(site, null) + viewModel.initialize(site, null) - val startState = viewStates[0] - assertStartState(startState) + val initialState = viewStates[0] + initialState.assertInitialState() viewModel.onEventsUpdated(OnJetpackInstalled(installError, JetpackAction.INSTALL_JETPACK)) @@ -160,43 +161,68 @@ class JetpackRemoteInstallViewModelTest : BaseUnitTest() { assertThat(connectionData.site == updatedSite).isTrue } - private fun assertStartState(state: JetpackRemoteInstallViewState) { - assertThat(state).isInstanceOf(Start::class.java) - assertThat(state.type).isEqualTo(JetpackRemoteInstallViewState.Type.START) - assertThat(state.titleResource).isEqualTo(R.string.install_jetpack) - assertThat(state.messageResource).isEqualTo(R.string.install_jetpack_message) - assertThat(state.icon).isEqualTo(R.drawable.ic_plans_white_24dp) - assertThat(state.buttonResource).isEqualTo(R.string.install_jetpack_continue) - assertThat(state.progressBarVisible).isEqualTo(false) + @Test + fun `calling isBackButtonEnabled corresponds to expectations`() { + // for START type, back button should be enabled + val type = JetpackRemoteInstallViewModel.Type.START + viewModel.initialize(site, type) + assertThat(viewModel.isBackButtonEnabled()).isTrue + + // for INSTALLING type, back button should be disabled + viewModel.onInitialButtonClick() + assertThat(viewModel.isBackButtonEnabled()).isFalse + + // for ERROR type, back button should be enabled + viewModel.onEventsUpdated( + OnJetpackInstalled( + JetpackInstallError(JetpackInstallErrorType.GENERIC_ERROR, "error"), + JetpackAction.INSTALL_JETPACK + ) + ) + assertThat(viewModel.isBackButtonEnabled()).isTrue + + // for INSTALLED type, back button should be disabled + viewModel.onRetryButtonClick() + assertThat(viewModel.isBackButtonEnabled()).isFalse + } + + @Test + fun `calling onBackPressed tracks install cancellation`() { + val mockUtils = Mockito.mockStatic(JetpackConnectionUtils::class.java) + + val source = JetpackConnectionSource.STATS + viewModel.onBackPressed(source) + mockUtils.verify { JetpackConnectionUtils.trackWithSource(Stat.INSTALL_JETPACK_CANCELLED, source) } + } + + private fun UiState.assertInitialState() { + assertThat(this).isInstanceOf(UiState.Initial::class.java) + + with(this as UiState.Initial) { + assertThat(buttonText).isEqualTo(R.string.jetpack_plugin_install_initial_button) + } } - private fun assertInstallingState(state: JetpackRemoteInstallViewState) { - assertThat(state).isInstanceOf(Installing::class.java) - assertThat(state.type).isEqualTo(JetpackRemoteInstallViewState.Type.INSTALLING) - assertThat(state.titleResource).isEqualTo(R.string.installing_jetpack) - assertThat(state.messageResource).isEqualTo(R.string.installing_jetpack_message) - assertThat(state.icon).isEqualTo(R.drawable.ic_plans_white_24dp) - assertThat(state.buttonResource).isNull() - assertThat(state.progressBarVisible).isEqualTo(true) + private fun UiState.assertInstallingState() { + assertThat(this).isInstanceOf(UiState.Installing::class.java) } - private fun assertInstalledState(state: JetpackRemoteInstallViewState) { - assertThat(state).isInstanceOf(Installed::class.java) - assertThat(state.type).isEqualTo(JetpackRemoteInstallViewState.Type.INSTALLED) - assertThat(state.titleResource).isEqualTo(R.string.jetpack_installed) - assertThat(state.messageResource).isEqualTo(R.string.jetpack_installed_message) - assertThat(state.icon).isEqualTo(R.drawable.ic_plans_white_24dp) - assertThat(state.buttonResource).isEqualTo(R.string.install_jetpack_continue) - assertThat(state.progressBarVisible).isEqualTo(false) + private fun UiState.assertDoneState() { + assertThat(this).isInstanceOf(UiState.Done::class.java) + + with(this as UiState.Done) { + assertThat(description).isEqualTo(R.string.jetpack_plugin_install_remote_plugin_done_description) + assertThat(buttonText).isEqualTo(R.string.jetpack_plugin_install_remote_plugin_done_button) + } } - private fun assertErrorState(state: JetpackRemoteInstallViewState) { - assertThat(state).isInstanceOf(Error::class.java) - assertThat(state.type).isEqualTo(JetpackRemoteInstallViewState.Type.ERROR) - assertThat(state.titleResource).isEqualTo(R.string.jetpack_installation_problem) - assertThat(state.messageResource).isEqualTo(R.string.jetpack_installation_problem_message) - assertThat(state.icon).isEqualTo(R.drawable.ic_warning) - assertThat(state.buttonResource).isEqualTo(R.string.install_jetpack_retry) - assertThat(state.progressBarVisible).isEqualTo(false) + private fun UiState.assertErrorState() { + assertThat(this).isInstanceOf(UiState.Error::class.java) + + with(this as UiState.Error) { + assertThat(retryButtonText).isEqualTo(R.string.jetpack_plugin_install_error_button_retry) + assertThat(contactSupportButtonText) + .isEqualTo(R.string.jetpack_plugin_install_error_button_contact_support) + } } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModelTest.kt index fa1554b0aedf..345ac5a9038e 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/MediaPickerViewModelTest.kt @@ -116,11 +116,32 @@ class MediaPickerViewModelTest : BaseUnitTest() { private var uiStates = mutableListOf() private lateinit var actions: Channel private var navigateEvents = mutableListOf>() - private val singleSelectMediaPickerSetup = buildMediaPickerSetup(false, setOf(IMAGE)) - private val multiSelectMediaPickerSetup = buildMediaPickerSetup(true, setOf(IMAGE, VIDEO)) - private val singleSelectVideoPickerSetup = buildMediaPickerSetup(false, setOf(VIDEO)) - private val singleSelectAudioPickerSetup = buildMediaPickerSetup(false, setOf(AUDIO)) - private val multiSelectFilePickerSetup = buildMediaPickerSetup(true, setOf(IMAGE, VIDEO, AUDIO, DOCUMENT)) + private val singleSelectMediaPickerSetup = buildMediaPickerSetup( + false, + setOf(IMAGE), + requiresPhotosVideosPermission = true + ) + private val multiSelectMediaPickerSetup = buildMediaPickerSetup( + true, + setOf(IMAGE, VIDEO), + requiresPhotosVideosPermission = true + ) + private val singleSelectVideoPickerSetup = buildMediaPickerSetup( + false, + setOf(VIDEO), + requiresPhotosVideosPermission = true + ) + private val singleSelectAudioPickerSetup = buildMediaPickerSetup( + false, + setOf(AUDIO), + requiresMusicAudioPermission = true + ) + private val multiSelectFilePickerSetup = buildMediaPickerSetup( + true, + setOf(IMAGE, VIDEO, AUDIO, DOCUMENT), + requiresPhotosVideosPermission = true, + requiresMusicAudioPermission = true + ) private val site = SiteModel() private lateinit var firstItem: MediaItem private lateinit var secondItem: MediaItem @@ -320,13 +341,21 @@ class MediaPickerViewModelTest : BaseUnitTest() { } @Test - fun `shows soft ask screen when storage permissions are turned off`() = test { - setupViewModel(listOf(), singleSelectMediaPickerSetup, hasStoragePermissions = false) + fun `shows soft ask screen when photos videos permissions are turned off`() = test { + setupViewModel( + listOf(), + singleSelectMediaPickerSetup, + hasPhotosVideosPermission = false + ) whenever(resourceProvider.getString(R.string.app_name)).thenReturn("WordPress") - whenever(resourceProvider.getString(R.string.photo_picker_soft_ask_label)).thenReturn("Soft ask label") + whenever(resourceProvider.getString(R.string.photo_picker_soft_ask_photos_label)) + .thenReturn("Soft ask label") val isAlwaysDenied = false - viewModel.checkStoragePermission(isAlwaysDenied = isAlwaysDenied) + viewModel.checkMediaPermissions( + isPhotosVideosAlwaysDenied = isAlwaysDenied, + isMusicAudioAlwaysDenied = isAlwaysDenied + ) assertThat(uiStates).hasSize(3) @@ -682,7 +711,7 @@ class MediaPickerViewModelTest : BaseUnitTest() { fun `empty state is emitted when no items in picker`() = test { setupViewModel(null, singleSelectMediaPickerSetup, numberOfStates = 1) - viewModel.checkStoragePermission(isAlwaysDenied = false) + viewModel.checkMediaPermissions(isPhotosVideosAlwaysDenied = false, isMusicAudioAlwaysDenied = false) assertThat(uiStates).hasSize(2) assertPhotoListUiStateEmpty() @@ -690,11 +719,15 @@ class MediaPickerViewModelTest : BaseUnitTest() { @Test fun `hidden state is emitted when when need to ask permission in picker`() = test { - setupViewModel(listOf(firstItem), singleSelectMediaPickerSetup, hasStoragePermissions = false) + setupViewModel( + listOf(firstItem), + singleSelectMediaPickerSetup, + hasPhotosVideosPermission = false + ) whenever(resourceProvider.getString(R.string.app_name)).thenReturn("WordPress") - whenever(resourceProvider.getString(R.string.photo_picker_soft_ask_label)).thenReturn("Soft ask label") + whenever(resourceProvider.getString(R.string.photo_picker_soft_ask_photos_label)).thenReturn("Soft ask label") - viewModel.checkStoragePermission(isAlwaysDenied = false) + viewModel.checkMediaPermissions(isPhotosVideosAlwaysDenied = false, isMusicAudioAlwaysDenied = false) assertThat(uiStates).hasSize(3) assertPhotoListUiStateHidden() @@ -702,7 +735,11 @@ class MediaPickerViewModelTest : BaseUnitTest() { @Test fun `data items state is emitted when items available in picker and have permissions`() = test { - setupViewModel(listOf(firstItem), singleSelectMediaPickerSetup, hasStoragePermissions = true) + setupViewModel( + listOf(firstItem), + singleSelectMediaPickerSetup, + hasPhotosVideosPermission = true + ) viewModel.refreshData(false) @@ -711,33 +748,36 @@ class MediaPickerViewModelTest : BaseUnitTest() { } @Test - fun `does not start loading without storage permissions`() = test { + fun `does not start loading without media permission`() = test { setupViewModel( listOf(firstItem), - singleSelectMediaPickerSetup.copy(requiresStoragePermissions = true), - hasStoragePermissions = false + singleSelectMediaPickerSetup.copy(requiresPhotosVideosPermissions = true), + hasPhotosVideosPermission = false ) assertThat(actions.tryReceive().getOrNull()).isNull() } @Test - fun `starts loading with storage permissions`() = test { + fun `starts loading with media permissions`() = test { setupViewModel( listOf(firstItem), - singleSelectMediaPickerSetup.copy(requiresStoragePermissions = true), - hasStoragePermissions = true + singleSelectMediaPickerSetup.copy(requiresPhotosVideosPermissions = true), + hasPhotosVideosPermission = true ) assertThat(actions.tryReceive().getOrNull()).isEqualTo(LoadAction.Start(null)) } @Test - fun `starts loading when storage permissions not necessary`() = test { + fun `starts loading when no permission necessary`() = test { setupViewModel( listOf(firstItem), - singleSelectMediaPickerSetup.copy(requiresStoragePermissions = false), - hasStoragePermissions = false + singleSelectMediaPickerSetup.copy( + requiresPhotosVideosPermissions = false, + requiresMusicAudioPermissions = false + ), + hasPhotosVideosPermission = false ) assertThat(actions.tryReceive().getOrNull()).isEqualTo(LoadAction.Start(null)) @@ -833,12 +873,12 @@ class MediaPickerViewModelTest : BaseUnitTest() { private suspend fun setupViewModel( domainModel: List?, mediaPickerSetup: MediaPickerSetup, - hasStoragePermissions: Boolean = true, + hasPhotosVideosPermission: Boolean = true, filter: String? = null, numberOfStates: Int = 2, hasMore: Boolean = false ) { - whenever(permissionsHandler.hasStoragePermission()).thenReturn(hasStoragePermissions) + whenever(permissionsHandler.hasPhotosVideosPermission()).thenReturn(hasPhotosVideosPermission) whenever(mediaLoaderFactory.build(mediaPickerSetup, site)).thenReturn(mediaLoader) doAnswer { actions = it.getArgument(0) @@ -931,12 +971,14 @@ class MediaPickerViewModelTest : BaseUnitTest() { allowedTypes: Set, cameraSetup: CameraSetup = HIDDEN, editingEnabled: Boolean = true, - requiresStoragePermissions: Boolean = true + requiresPhotosVideosPermission: Boolean = false, + requiresMusicAudioPermission: Boolean = false ) = MediaPickerSetup( primaryDataSource = DEVICE, availableDataSources = setOf(), canMultiselect = canMultiselect, - requiresStoragePermissions = requiresStoragePermissions, + requiresPhotosVideosPermissions = requiresPhotosVideosPermission, + requiresMusicAudioPermissions = requiresMusicAudioPermission, allowedTypes = allowedTypes, cameraSetup = cameraSetup, systemPickerEnabled = true, diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderFactoryTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderFactoryTest.kt index 2be0e469f6af..4b024e5899e0 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderFactoryTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mediapicker/loader/MediaLoaderFactoryTest.kt @@ -58,7 +58,8 @@ class MediaLoaderFactoryTest { DEVICE, availableDataSources = setOf(), canMultiselect = true, - requiresStoragePermissions = true, + requiresPhotosVideosPermissions = true, + requiresMusicAudioPermissions = true, allowedTypes = setOf(), cameraSetup = HIDDEN, systemPickerEnabled = true, @@ -79,7 +80,8 @@ class MediaLoaderFactoryTest { WP_LIBRARY, availableDataSources = setOf(), canMultiselect = true, - requiresStoragePermissions = false, + requiresPhotosVideosPermissions = false, + requiresMusicAudioPermissions = false, allowedTypes = setOf(), cameraSetup = HIDDEN, systemPickerEnabled = false, @@ -101,7 +103,8 @@ class MediaLoaderFactoryTest { STOCK_LIBRARY, availableDataSources = setOf(), canMultiselect = true, - requiresStoragePermissions = false, + requiresPhotosVideosPermissions = false, + requiresMusicAudioPermissions = false, allowedTypes = setOf(), cameraSetup = HIDDEN, systemPickerEnabled = false, @@ -122,7 +125,8 @@ class MediaLoaderFactoryTest { GIF_LIBRARY, availableDataSources = setOf(), canMultiselect = true, - requiresStoragePermissions = false, + requiresPhotosVideosPermissions = false, + requiresMusicAudioPermissions = false, allowedTypes = setOf(), cameraSetup = HIDDEN, systemPickerEnabled = false, diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteSourceManagerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteSourceManagerTest.kt index 177287df14bf..3bf0607e66ca 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteSourceManagerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteSourceManagerTest.kt @@ -21,6 +21,7 @@ import org.wordpress.android.ui.mysite.MySiteUiState.PartialState.SelectedSite import org.wordpress.android.ui.mysite.cards.blaze.PromoteWithBlazeCardSource import org.wordpress.android.ui.mysite.cards.dashboard.CardsSource import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptCardSource +import org.wordpress.android.ui.mysite.cards.dashboard.domain.DashboardCardDomainSource import org.wordpress.android.ui.mysite.cards.domainregistration.DomainRegistrationSource import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartCardSource import org.wordpress.android.ui.mysite.dynamiccards.DynamicCardMenuViewModel.DynamicCardMenuInteraction @@ -67,6 +68,9 @@ class MySiteSourceManagerTest : BaseUnitTest() { @Mock lateinit var promoteWithBlazeCardSource: PromoteWithBlazeCardSource + @Mock + lateinit var dashboardCardDomainSource: DashboardCardDomainSource + @Mock lateinit var selectedSiteRepository: SelectedSiteRepository @@ -99,7 +103,8 @@ class MySiteSourceManagerTest : BaseUnitTest() { siteIconProgressSource, bloggingPromptCardSource, promoteWithBlazeCardSource, - selectedSiteRepository + selectedSiteRepository, + dashboardCardDomainSource ) allRefreshedMySiteSources = listOf( diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt index a1592ef1ab18..7d13abf1234f 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt @@ -56,8 +56,11 @@ import org.wordpress.android.ui.blaze.BlazeFeatureUtils import org.wordpress.android.ui.blaze.BlazeFlowSource import org.wordpress.android.ui.bloggingprompts.BloggingPromptsPostTagProvider import org.wordpress.android.ui.bloggingprompts.BloggingPromptsSettingsHelper +import org.wordpress.android.ui.mysite.cards.dashboard.domain.DashboardCardDomainUtils import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil -import org.wordpress.android.ui.jpfullplugininstall.GetShowJetpackFullPluginInstallOnboardingUseCase +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginHelper +import org.wordpress.android.ui.jetpackplugininstall.fullplugin.GetShowJetpackFullPluginInstallOnboardingUseCase import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard @@ -323,6 +326,15 @@ class MySiteViewModelTest : BaseUnitTest() { @Mock lateinit var blazeFeatureUtils: BlazeFeatureUtils + @Mock + lateinit var dashboardCardDomainUtils: DashboardCardDomainUtils + + @Mock + lateinit var jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper + + @Mock + lateinit var wpJetpackIndividualPluginHelper: WPJetpackIndividualPluginHelper + private lateinit var viewModel: MySiteViewModel private lateinit var uiModels: MutableList private lateinit var snackbars: MutableList @@ -489,6 +501,7 @@ class MySiteViewModelTest : BaseUnitTest() { whenever(quickStartType.getTaskFromString(QuickStartStore.QUICK_START_VIEW_SITE_LABEL)) .thenReturn(QuickStartNewSiteTask.VIEW_SITE) whenever(jetpackBrandingUtils.getBrandingTextForScreen(any())).thenReturn(mock()) + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowDashboard()).thenReturn(true) viewModel = MySiteViewModel( networkUtilsWrapper, testDispatcher(), @@ -540,7 +553,10 @@ class MySiteViewModelTest : BaseUnitTest() { bloggingPromptsCardTrackHelper, getShowJetpackFullPluginInstallOnboardingUseCase, jetpackInstallFullPluginShownTracker, - blazeFeatureUtils + blazeFeatureUtils, + dashboardCardDomainUtils, + jetpackFeatureRemovalPhaseHelper, + wpJetpackIndividualPluginHelper, ) uiModels = mutableListOf() snackbars = mutableListOf() @@ -1400,6 +1416,7 @@ class MySiteViewModelTest : BaseUnitTest() { @Test fun `given dynamic cards enabled + new site, when check & start QS triggered, then new site QS starts`() { whenever(quickStartDynamicCardsFeatureConfig.isEnabled()).thenReturn(true) + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowQuickStart()).thenReturn(true) viewModel.checkAndStartQuickStart(siteLocalId, false, isNewSite = true) @@ -1416,6 +1433,7 @@ class MySiteViewModelTest : BaseUnitTest() { fun `given dynamic cards enabled + existing site, when check & start QS triggered, then existing site QS starts`() { whenever(quickStartRepository.quickStartType).thenReturn(ExistingSiteQuickStartType) whenever(quickStartDynamicCardsFeatureConfig.isEnabled()).thenReturn(true) + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowQuickStart()).thenReturn(true) viewModel.checkAndStartQuickStart(siteLocalId, false, isNewSite = false) @@ -1432,6 +1450,7 @@ class MySiteViewModelTest : BaseUnitTest() { fun `given no selected site, when check and start QS is triggered, then QSP is not shown`() { whenever(quickStartDynamicCardsFeatureConfig.isEnabled()).thenReturn(false) whenever(selectedSiteRepository.getSelectedSite()).thenReturn(null) + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowQuickStart()).thenReturn(true) viewModel.checkAndStartQuickStart(siteLocalId, isSiteTitleTaskCompleted = false, isNewSite = false) @@ -1443,6 +1462,7 @@ class MySiteViewModelTest : BaseUnitTest() { whenever(quickStartDynamicCardsFeatureConfig.isEnabled()).thenReturn(false) whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) whenever(quickStartUtilsWrapper.isQuickStartAvailableForTheSite(site)).thenReturn(false) + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowQuickStart()).thenReturn(true) viewModel.checkAndStartQuickStart(siteLocalId, isSiteTitleTaskCompleted = false, isNewSite = true) @@ -1454,6 +1474,7 @@ class MySiteViewModelTest : BaseUnitTest() { whenever(quickStartDynamicCardsFeatureConfig.isEnabled()).thenReturn(false) whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) whenever(quickStartUtilsWrapper.isQuickStartAvailableForTheSite(site)).thenReturn(false) + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowQuickStart()).thenReturn(true) viewModel.checkAndStartQuickStart(siteLocalId, isSiteTitleTaskCompleted = false, isNewSite = false) @@ -1465,6 +1486,7 @@ class MySiteViewModelTest : BaseUnitTest() { whenever(quickStartDynamicCardsFeatureConfig.isEnabled()).thenReturn(false) whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) whenever(quickStartUtilsWrapper.isQuickStartAvailableForTheSite(site)).thenReturn(true) + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowQuickStart()).thenReturn(true) viewModel.checkAndStartQuickStart(siteLocalId, false, isNewSite = true) @@ -1483,6 +1505,7 @@ class MySiteViewModelTest : BaseUnitTest() { whenever(quickStartDynamicCardsFeatureConfig.isEnabled()).thenReturn(false) whenever(selectedSiteRepository.getSelectedSite()).thenReturn(site) whenever(quickStartUtilsWrapper.isQuickStartAvailableForTheSite(site)).thenReturn(true) + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowQuickStart()).thenReturn(true) viewModel.checkAndStartQuickStart(siteLocalId, false, isNewSite = false) @@ -2661,7 +2684,7 @@ class MySiteViewModelTest : BaseUnitTest() { whenever(appStatus.isAppInstalled(packageName)).thenReturn(true) initSelectedSite() - val expected = R.drawable.ic_wordpress_blue_32dp + val expected = R.drawable.ic_wordpress_jetpack_appicon assertThat((getSiteMenuTabLastItems()[0] as SingleActionCard).imageResource).isEqualTo(expected) assertThat((getLastItems()[0] as SingleActionCard).imageResource).isEqualTo(expected) assertThat((getDashboardTabLastItems()[0] as SingleActionCard).imageResource).isEqualTo(expected) @@ -3260,6 +3283,7 @@ class MySiteViewModelTest : BaseUnitTest() { verify(blazeFeatureUtils).trackEntryPointTapped(BlazeFlowSource.DASHBOARD_CARD) } + @Test fun `when promote with blaze card menu is accessed, then blaze card menu is accessed is tracked`() = test { initSelectedSite() @@ -3284,6 +3308,28 @@ class MySiteViewModelTest : BaseUnitTest() { ) } + @Test + fun `when onActionableEmptyViewVisible is invoked then show jetpack individual plugin overlay`() = + test { + whenever(wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay()).thenReturn(true) + + viewModel.onActionableEmptyViewVisible() + advanceUntilIdle() + + assertThat(viewModel.onShowJetpackIndividualPluginOverlay.value?.peekContent()).isEqualTo(Unit) + } + + @Test + fun `when onActionableEmptyViewVisible is invoked then don't show jetpack individual plugin overlay`() = + test { + whenever(wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay()).thenReturn(false) + + viewModel.onActionableEmptyViewVisible() + advanceUntilIdle() + + assertThat(viewModel.onShowJetpackIndividualPluginOverlay.value?.peekContent()).isNull() + } + private fun findQuickActionsCard() = getLastItems().find { it is QuickActionsCard } as QuickActionsCard? private fun findQuickStartDynamicCard() = getLastItems().find { it is DynamicCard } as DynamicCard? @@ -3374,8 +3420,8 @@ class MySiteViewModelTest : BaseUnitTest() { whenever(bloggingPromptsEnhancementsFeatureConfig.isEnabled()).thenReturn(isBloggingPromptsEnhancementsEnabled) whenever(bloggingPromptsSocialFeatureConfig.isEnabled()).thenReturn(isBloggingPromptsSocialEnabled) whenever(mySiteDashboardTabsFeatureConfig.isEnabled()).thenReturn(isMySiteDashboardTabsEnabled) - whenever(jetpackBrandingUtils.shouldShowJetpackBranding()).thenReturn(shouldShowJetpackBranding) - whenever(blazeFeatureUtils.shouldShowBlazeEntryPoint(any(), any())).thenReturn(isBlazeEnabled) + whenever(jetpackBrandingUtils.shouldShowJetpackBrandingInDashboard()).thenReturn(shouldShowJetpackBranding) + whenever(blazeFeatureUtils.shouldShowBlazeCardEntryPoint(any(), any())).thenReturn(isBlazeEnabled) if (isSiteUsingWpComRestApi) { site.setIsWPCom(true) site.setIsJetpackConnected(true) @@ -3548,7 +3594,7 @@ class MySiteViewModelTest : BaseUnitTest() { add(initPostCard(mockInvocation)) add(initTodaysStatsCard(mockInvocation)) if (bloggingPromptsFeatureConfig.isEnabled()) add(initBloggingPromptCard(mockInvocation)) - if (blazeFeatureUtils.shouldShowBlazeEntryPoint( + if (blazeFeatureUtils.shouldShowBlazeCardEntryPoint( BlazeStatusModel(1, true), 1) ) add( initPromoteWithBlazeCard(mockInvocation) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/CardsBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/CardsBuilderTest.kt index 8602a46ce5aa..820021cd3e51 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/CardsBuilderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/CardsBuilderTest.kt @@ -20,6 +20,8 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickActionsCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickLinkRibbon import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickStartCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.QuickStartCard.QuickStartTaskTypeItem +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardDomainBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BloggingPromptCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardsBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DomainRegistrationCardBuilderParams @@ -30,6 +32,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.QuickActio import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.QuickLinkRibbonBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.QuickStartCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.TodaysStatsCardBuilderParams +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.PagesCardBuilderParams import org.wordpress.android.ui.mysite.cards.jpfullplugininstall.JetpackInstallFullPluginCardBuilder import org.wordpress.android.ui.mysite.cards.quickactions.QuickActionsCardBuilder import org.wordpress.android.ui.mysite.cards.quicklinksribbon.QuickLinkRibbonBuilder @@ -175,6 +178,7 @@ class CardsBuilderTest { private fun List.findQuickLinkRibbon() = this.find { it is QuickLinkRibbon } as QuickLinkRibbon? + @Suppress("LongMethod") private fun buildCards( isQuickActionEnabled: Boolean = true, activeTask: QuickStartTask? = null, @@ -218,7 +222,10 @@ class CardsBuilderTest { mock(), mock(), ), - promoteWithBlazeCardBuilderParams = PromoteWithBlazeCardBuilderParams(true, mock(), mock(), mock()) + promoteWithBlazeCardBuilderParams = PromoteWithBlazeCardBuilderParams(true, mock(), mock(), mock()), + dashboardCardDomainBuilderParams = DashboardCardDomainBuilderParams(true, mock(), mock(), mock()), + pagesCardBuilderParams = PagesCardBuilderParams(mock(), mock(), mock()), + activityCardBuilderParams = ActivityCardBuilderParams(mock(), mock(), mock(), mock()), ), quickLinkRibbonBuilderParams = QuickLinkRibbonBuilderParams( siteModel = mock(), diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsBuilderTest.kt index 2cd5e2aee519..90e9dde0ccca 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsBuilderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsBuilderTest.kt @@ -13,25 +13,33 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ActivityCard.ActivityCardWithItems import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.BloggingPromptCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.BloggingPromptCard.BloggingPromptCardWithData +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.DashboardDomainCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ErrorCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PostCard.FooterLink import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PostCard.PostCardWithPostItems import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PostCard.PostCardWithoutPostItems +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PagesCard.PagesCardWithData import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PromoteWithBlazeCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.TodaysStatsCard.TodaysStatsCardWithData +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardDomainBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.PromoteWithBlazeCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.BloggingPromptCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.DashboardCardsBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.PostCardBuilderParams import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.TodaysStatsCardBuilderParams import org.wordpress.android.ui.mysite.cards.blaze.PromoteWithBlazeCardBuilder +import org.wordpress.android.ui.mysite.cards.dashboard.activity.ActivityCardBuilder import org.wordpress.android.ui.mysite.cards.dashboard.bloggingprompts.BloggingPromptCardBuilder +import org.wordpress.android.ui.mysite.cards.dashboard.pages.PagesCardBuilder import org.wordpress.android.ui.mysite.cards.dashboard.posts.PostCardBuilder import org.wordpress.android.ui.mysite.cards.dashboard.posts.PostCardType.CREATE_FIRST import org.wordpress.android.ui.mysite.cards.dashboard.posts.PostCardType.DRAFT import org.wordpress.android.ui.mysite.cards.dashboard.todaysstats.TodaysStatsCardBuilder +import org.wordpress.android.ui.mysite.cards.dashboard.domain.DashboardDomainCardBuilder import org.wordpress.android.ui.utils.UiString.UiStringText @ExperimentalCoroutinesApi @@ -48,6 +56,16 @@ class CardsBuilderTest : BaseUnitTest() { @Mock lateinit var promoteWithBlazeCardBuilder: PromoteWithBlazeCardBuilder + + @Mock + lateinit var dashboardDomainCardBuilder: DashboardDomainCardBuilder + + @Mock + lateinit var pagesCardBuilder: PagesCardBuilder + + @Mock + lateinit var activityCardBuilder: ActivityCardBuilder + private lateinit var cardsBuilder: CardsBuilder @Before @@ -56,7 +74,10 @@ class CardsBuilderTest : BaseUnitTest() { todaysStatsCardBuilder, postCardBuilder, bloggingPromptCardsBuilder, - promoteWithBlazeCardBuilder + promoteWithBlazeCardBuilder, + dashboardDomainCardBuilder, + pagesCardBuilder, + activityCardBuilder ) } @@ -173,6 +194,48 @@ class CardsBuilderTest : BaseUnitTest() { assertThat(cards.findPromoteWithBlazeCard()).isNotNull } + @Test + fun `given is not eligible for domain, when cards are built, then domain card is not built`() { + val cards = buildDashboardCards(isEligibleForDomainCard = false) + + assertThat(cards.findDashboardDomainCard()).isNull() + } + + @Test + fun `given is eligible for domain, when cards are built, then domain card is built`() { + val cards = buildDashboardCards(isEligibleForDomainCard = true) + + assertThat(cards.findDashboardDomainCard()).isNotNull + } + + @Test + fun `given has pages, when cards are built, then pages card is not built`() { + val cards = buildDashboardCards(hasPagesCard = false) + + assertThat(cards.findPagesCard()).isNull() + } + + @Test + fun `given has pages, when cards are built, then pages card is built`() { + val cards = buildDashboardCards(hasPagesCard = true) + + assertThat(cards.findPagesCard()).isNotNull + } + + @Test + fun `given no activities, when cards are built, then activity card is not built`() { + val cards = buildDashboardCards(hasActivityCard = false) + + assertThat(cards.findActivityCard()).isNull() + } + + @Test + fun `given has activities, when cards are built, then activity card is built`() { + val cards = buildDashboardCards(hasActivityCard = true) + + assertThat(cards.findActivityCard()).isNotNull + } + private fun DashboardCards.findTodaysStatsCard() = this.cards.find { it is TodaysStatsCardWithData } as? TodaysStatsCardWithData @@ -188,6 +251,15 @@ class CardsBuilderTest : BaseUnitTest() { private fun DashboardCards.findPromoteWithBlazeCard() = this.cards.find { it is PromoteWithBlazeCard } as? PromoteWithBlazeCard + private fun DashboardCards.findDashboardDomainCard() = + this.cards.find { it is DashboardDomainCard } as? DashboardDomainCard + + private fun DashboardCards.findPagesCard() = + this.cards.find { it is PagesCardWithData } as? PagesCardWithData + + private fun DashboardCards.findActivityCard() = + this.cards.find { it is ActivityCardWithItems } as? ActivityCardWithItems + private fun DashboardCards.findErrorCard() = this.cards.find { it is ErrorCard } as? ErrorCard private val todaysStatsCard = mock() @@ -196,6 +268,12 @@ class CardsBuilderTest : BaseUnitTest() { private val promoteWithBlazeCard = mock() + private val dashboardDomainCard = mock() + + private val pagesCard = mock() + + private val activityCard = mock() + private fun createPostCards() = listOf( PostCardWithPostItems( postCardType = DRAFT, @@ -222,6 +300,9 @@ class CardsBuilderTest : BaseUnitTest() { hasBlogginPrompt: Boolean = false, showErrorCard: Boolean = false, isEligibleForBlaze: Boolean = false, + isEligibleForDomainCard: Boolean = false, + hasPagesCard: Boolean = false, + hasActivityCard: Boolean = false ): DashboardCards { doAnswer { if (hasTodaysStats) todaysStatsCard else null }.whenever(todaysStatsCardBuilder).build(any()) doAnswer { if (hasPostsForPostCard) createPostCards() else createPostPromptCards() }.whenever(postCardBuilder) @@ -229,6 +310,10 @@ class CardsBuilderTest : BaseUnitTest() { doAnswer { if (hasBlogginPrompt) blogingPromptCard else null }.whenever(bloggingPromptCardsBuilder).build(any()) doAnswer { if (isEligibleForBlaze) promoteWithBlazeCard else null }.whenever(promoteWithBlazeCardBuilder) .build(any()) + doAnswer { if (isEligibleForDomainCard) dashboardDomainCard else null }.whenever(dashboardDomainCardBuilder) + .build(any()) + doAnswer { if (hasPagesCard) pagesCard else null }.whenever(pagesCardBuilder).build(any()) + doAnswer { if (hasActivityCard) activityCard else null }.whenever(activityCardBuilder).build(any()) return cardsBuilder.build( dashboardCardsBuilderParams = DashboardCardsBuilderParams( showErrorCard = showErrorCard, @@ -243,6 +328,20 @@ class CardsBuilderTest : BaseUnitTest() { mock(), mock(), mock() + ), + dashboardCardDomainBuilderParams = DashboardCardDomainBuilderParams( + isEligibleForDomainCard, mock(), mock(), mock() + ), + pagesCardBuilderParams = MySiteCardAndItemBuilderParams.PagesCardBuilderParams( + mock(), + mock(), + mock() + ), + activityCardBuilderParams = MySiteCardAndItemBuilderParams.ActivityCardBuilderParams( + mock(), + mock(), + mock(), + mock() ) ) ) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsShownTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsShownTrackerTest.kt index 77d38d646eae..9bfba024d96a 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsShownTrackerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsShownTrackerTest.kt @@ -9,11 +9,14 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ActivityCard +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ActivityCard.ActivityCardWithItems import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ErrorCard import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PostCard.FooterLink import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PostCard.PostCardWithPostItems import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.PostCard.PostCardWithoutPostItems +import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker.ActivityLogSubtype import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker.PostSubtype import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker.Type import org.wordpress.android.ui.mysite.cards.dashboard.posts.PostCardType @@ -77,6 +80,13 @@ class CardsShownTrackerTest { ) } + @Test + fun `when activity card is shown, then activity log shown event is tracked`() { + cardsShownTracker.track(buildActivityDashboardCards()) + + verifyCardShownTracked(Type.ACTIVITY.label, ActivityLogSubtype.ACTIVITY_LOG.label) + } + private fun verifyCardShownTracked(type: String, subtype: String) { verify(analyticsTracker).track( Stat.MY_SITE_DASHBOARD_CARD_SHOWN, @@ -125,6 +135,21 @@ class CardsShownTrackerTest { ) ) + private fun buildActivityDashboardCards() = DashboardCards( + cards = mutableListOf().apply { + addAll(buildActivityCard()) + } + ) + + private fun buildActivityCard() = + listOf( + ActivityCardWithItems( + title = UiStringText("title"), + footerLink = ActivityCard.FooterLink(UiStringText("footer"), onClick = mock()), + activityItems = emptyList() + ) + ) + private fun buildErrorCard(): DashboardCards { val cards = listOf(ErrorCard(onRetryClick = mock())) val dashboardCard = mutableListOf() diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSourceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSourceTest.kt index 6161afaa7ab1..966cd1fa3ddf 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSourceTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsSourceTest.kt @@ -11,17 +11,23 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.activity.ActivityLogModel import org.wordpress.android.fluxc.model.dashboard.CardModel import org.wordpress.android.fluxc.model.dashboard.CardModel.PostsCardModel import org.wordpress.android.fluxc.model.dashboard.CardModel.PostsCardModel.PostCardModel +import org.wordpress.android.fluxc.model.dashboard.CardModel.PagesCardModel +import org.wordpress.android.fluxc.model.dashboard.CardModel.PagesCardModel.PageCardModel import org.wordpress.android.fluxc.model.dashboard.CardModel.TodaysStatsCardModel import org.wordpress.android.fluxc.network.rest.wpcom.dashboard.CardsUtils import org.wordpress.android.fluxc.store.dashboard.CardsStore import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsError import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsErrorType import org.wordpress.android.fluxc.store.dashboard.CardsStore.CardsResult +import org.wordpress.android.fluxc.tools.FormattableContent import org.wordpress.android.ui.mysite.MySiteUiState.PartialState.CardsUpdate import org.wordpress.android.ui.mysite.SelectedSiteRepository +import org.wordpress.android.util.config.DashboardCardActivityLogConfig +import org.wordpress.android.util.config.DashboardCardPagesConfig import org.wordpress.android.util.config.MySiteDashboardTodaysStatsCardFeatureConfig /* SITE */ @@ -43,6 +49,31 @@ const val POST_CONTENT = "content" const val POST_FEATURED_IMAGE = "featuredImage" const val POST_DATE = "2021-12-27 11:33:55" +/* PAGES */ +const val PAGE_ID = 1 +const val PAGE_TITLE = "title" +const val PAGE_CONTENT = "content" +const val PAGE_MODIFIED_ON = "2023-03-02 10:26:53" +const val PAGE_STATUS = "publish" +const val PAGE_DATE = "2023-03-02 10:30:53" + +/* ACTIVITY */ +const val ACTIVITY_ID = "activity123" +const val ACTIVITY_SUMMARY = "activity" +const val ACTIVITY_NAME = "name" +const val ACTIVITY_TYPE = "create a blog" +const val ACTIVITY_IS_REWINDABLE = false +const val ACTIVITY_REWIND_ID = "10.0" +const val ACTIVITY_GRID_ICON = "gridicon.jpg" +const val ACTIVITY_STATUS = "OK" +const val ACTIVITY_ACTOR_TYPE = "author" +const val ACTIVITY_ACTOR_NAME = "John Smith" +const val ACTIVITY_ACTOR_WPCOM_USER_ID = 15L +const val ACTIVITY_ACTOR_ROLE = "admin" +const val ACTIVITY_ACTOR_ICON_URL = "dog.jpg" +const val ACTIVITY_PUBLISHED_DATE = "2021-12-27 11:33:55" +const val ACTIVITY_CONTENT = "content" + /* MODEL */ private val TODAYS_STATS_CARDS_MODEL = TodaysStatsCardModel( @@ -66,13 +97,54 @@ private val POSTS_MODEL = PostsCardModel( scheduled = listOf(POST_MODEL) ) +private val PAGE_MODEL = PageCardModel( + id = PAGE_ID, + title = PAGE_TITLE, + content = PAGE_CONTENT, + lastModifiedOrScheduledOn = CardsUtils.fromDate(PAGE_MODIFIED_ON), + status = PAGE_STATUS, + date = CardsUtils.fromDate(PAGE_DATE) +) + +private val PAGES_MODEL = PagesCardModel( + pages = listOf(PAGE_MODEL) +) + +private val ACTIVITY_LOG_MODEL = ActivityLogModel( + summary = ACTIVITY_SUMMARY, + content = FormattableContent(text = ACTIVITY_CONTENT), + name = ACTIVITY_NAME, + actor = ActivityLogModel.ActivityActor( + displayName = ACTIVITY_ACTOR_NAME, + type = ACTIVITY_ACTOR_TYPE, + wpcomUserID = ACTIVITY_ACTOR_WPCOM_USER_ID, + avatarURL = ACTIVITY_ACTOR_ICON_URL, + role = ACTIVITY_ACTOR_ROLE, + ), + type = ACTIVITY_TYPE, + published = CardsUtils.fromDate(ACTIVITY_PUBLISHED_DATE), + rewindable = ACTIVITY_IS_REWINDABLE, + rewindID = ACTIVITY_REWIND_ID, + gridicon = ACTIVITY_GRID_ICON, + status = ACTIVITY_STATUS, + activityID = ACTIVITY_ID +) + +private val ACTIVITY_CARD_MODEL = CardModel.ActivityCardModel( + activities = listOf(ACTIVITY_LOG_MODEL) +) + private val CARDS_MODEL: List = listOf( TODAYS_STATS_CARDS_MODEL, - POSTS_MODEL + POSTS_MODEL, + PAGES_MODEL, + ACTIVITY_CARD_MODEL ) private val DEFAULT_CARD_TYPE = listOf(CardModel.Type.POSTS) private val STATS_FEATURED_ENABLED_CARD_TYPES = listOf(CardModel.Type.TODAYS_STATS, CardModel.Type.POSTS) +private val PAGES_FEATURED_ENABLED_CARD_TYPE = listOf(CardModel.Type.PAGES, CardModel.Type.POSTS) +private val ACTIVITY_FEATURED_ENABLED_CARD_TYPE = listOf(CardModel.Type.ACTIVITY, CardModel.Type.POSTS) @ExperimentalCoroutinesApi class CardsSourceTest : BaseUnitTest() { @@ -87,6 +159,13 @@ class CardsSourceTest : BaseUnitTest() { @Mock private lateinit var todaysStatsCardFeatureConfig: MySiteDashboardTodaysStatsCardFeatureConfig + + @Mock + private lateinit var dashboardCardPagesConfig: DashboardCardPagesConfig + + @Mock + private lateinit var dashboardCardActivityLogConfig: DashboardCardActivityLogConfig + private lateinit var cardSource: CardsSource private val data = CardsResult( @@ -102,18 +181,34 @@ class CardsSourceTest : BaseUnitTest() { init() } - private fun init(isTodaysStatsCardFeatureConfigEnabled: Boolean = false) { - setUpMocks(isTodaysStatsCardFeatureConfigEnabled) + private fun init( + isTodaysStatsCardFeatureConfigEnabled: Boolean = false, + isDashboardCardPagesConfigEnabled: Boolean = false, + isDashboardCardActivityLogConfigEnabled: Boolean = false + ) { + setUpMocks( + isTodaysStatsCardFeatureConfigEnabled, + isDashboardCardPagesConfigEnabled, + isDashboardCardActivityLogConfigEnabled + ) cardSource = CardsSource( selectedSiteRepository, cardsStore, todaysStatsCardFeatureConfig, + dashboardCardPagesConfig, + dashboardCardActivityLogConfig, testDispatcher() ) } - private fun setUpMocks(isTodaysStatsCardFeatureConfigEnabled: Boolean) { + private fun setUpMocks( + isTodaysStatsCardFeatureConfigEnabled: Boolean, + isDashboardCardPagesConfigEnabled: Boolean = false, + isDashboardCardActivityLogConfig: Boolean = false + ) { whenever(todaysStatsCardFeatureConfig.isEnabled()).thenReturn(isTodaysStatsCardFeatureConfigEnabled) + whenever(dashboardCardPagesConfig.isEnabled()).thenReturn(isDashboardCardPagesConfigEnabled) + whenever(dashboardCardActivityLogConfig.isEnabled()).thenReturn(isDashboardCardActivityLogConfig) whenever(siteModel.id).thenReturn(SITE_LOCAL_ID) whenever(selectedSiteRepository.getSelectedSite()).thenReturn(siteModel) } @@ -372,4 +467,64 @@ class CardsSourceTest : BaseUnitTest() { assertThat(result.first()).isEqualTo(CardsUpdate(showErrorCard = true)) assertThat(result.last()).isEqualTo(CardsUpdate(showErrorCard = true)) } + + @Test + fun `given pages feature enabled, when build is invoked, then pages from store(database)`() = test { + init(isDashboardCardPagesConfigEnabled = true) + val result = mutableListOf() + whenever(cardsStore.getCards(siteModel, PAGES_FEATURED_ENABLED_CARD_TYPE)).thenReturn(flowOf(data)) + + cardSource.build(testScope(), SITE_LOCAL_ID).observeForever { + it?.let { result.add(it) } + } + + verify(cardsStore).getCards(siteModel, PAGES_FEATURED_ENABLED_CARD_TYPE) + } + + @Test + fun `given pages feature enabled, when refresh is invoked, then pages are requested from network`() = + test { + init(isDashboardCardPagesConfigEnabled = true) + val result = mutableListOf() + whenever(cardsStore.getCards(siteModel, PAGES_FEATURED_ENABLED_CARD_TYPE)).thenReturn(flowOf(data)) + whenever(cardsStore.fetchCards(siteModel, PAGES_FEATURED_ENABLED_CARD_TYPE)).thenReturn(success) + cardSource.refresh.observeForever { } + + cardSource.build(testScope(), SITE_LOCAL_ID).observeForever { + it?.let { result.add(it) } + } + advanceUntilIdle() + + verify(cardsStore).fetchCards(siteModel, PAGES_FEATURED_ENABLED_CARD_TYPE) + } + + @Test + fun `given activity feature enabled, when build is invoked, then activity from store(database)`() = test { + init(isDashboardCardActivityLogConfigEnabled = true) + val result = mutableListOf() + whenever(cardsStore.getCards(siteModel, ACTIVITY_FEATURED_ENABLED_CARD_TYPE)).thenReturn(flowOf(data)) + + cardSource.build(testScope(), SITE_LOCAL_ID).observeForever { + it?.let { result.add(it) } + } + + verify(cardsStore).getCards(siteModel, ACTIVITY_FEATURED_ENABLED_CARD_TYPE) + } + + @Test + fun `given activity feature enabled, when refresh is invoked, then activity are requested from network`() = + test { + init(isDashboardCardActivityLogConfigEnabled = true) + val result = mutableListOf() + whenever(cardsStore.getCards(siteModel, ACTIVITY_FEATURED_ENABLED_CARD_TYPE)).thenReturn(flowOf(data)) + whenever(cardsStore.fetchCards(siteModel, ACTIVITY_FEATURED_ENABLED_CARD_TYPE)).thenReturn(success) + cardSource.refresh.observeForever { } + + cardSource.build(testScope(), SITE_LOCAL_ID).observeForever { + it?.let { result.add(it) } + } + advanceUntilIdle() + + verify(cardsStore).fetchCards(siteModel, ACTIVITY_FEATURED_ENABLED_CARD_TYPE) + } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTrackerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTrackerTest.kt index 4af50dac8f8f..2c94cd1d0865 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTrackerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/CardsTrackerTest.kt @@ -8,6 +8,7 @@ import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.verify import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTaskType +import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker.ActivityLogSubtype import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker.PostSubtype import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker.QuickStartSubtype import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker.StatsSubtype @@ -119,6 +120,20 @@ class CardsTrackerTest { verifyCardItemClickedTracked(Type.POST, PostSubtype.SCHEDULED.label) } + @Test + fun `when activity log item is clicked, then activity card item event is tracked`() { + cardsTracker.trackActivityCardItemClicked() + + verifyCardItemClickedTracked(Type.ACTIVITY, ActivityLogSubtype.ACTIVITY_LOG.label) + } + + @Test + fun `when activity card footer link is clicked, then footer link clicked is tracked`() { + cardsTracker.trackActivityCardFooterClicked() + + verifyFooterLinkClickedTracked(Type.ACTIVITY, ActivityLogSubtype.ACTIVITY_LOG.label) + } + private fun verifyFooterLinkClickedTracked( typeValue: Type, subtypeValue: String diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityCardBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityCardBuilderTest.kt new file mode 100644 index 000000000000..9f0e832eb7af --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/activity/ActivityCardBuilderTest.kt @@ -0,0 +1,194 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.activity + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.activity.ActivityLogModel +import org.wordpress.android.fluxc.model.dashboard.CardModel.ActivityCardModel +import org.wordpress.android.fluxc.network.rest.wpcom.dashboard.CardsUtils +import org.wordpress.android.fluxc.store.dashboard.CardsStore.ActivityCardError +import org.wordpress.android.fluxc.store.dashboard.CardsStore.ActivityCardErrorType +import org.wordpress.android.fluxc.tools.FormattableContent +import org.wordpress.android.ui.mysite.MySiteCardAndItem.Card.DashboardCards.DashboardCard.ActivityCard +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.ActivityCardBuilderParams +import org.wordpress.android.util.DateTimeUtilsWrapper +import org.wordpress.android.util.SiteUtilsWrapper +import org.wordpress.android.util.config.DashboardCardActivityLogConfig + +@ExperimentalCoroutinesApi +class ActivityCardBuilderTest : BaseUnitTest() { + @Mock + private lateinit var dashboardCardActivityLogConfig: DashboardCardActivityLogConfig + + @Mock + private lateinit var dateTimeUtilsWrapper: DateTimeUtilsWrapper + + @Mock + private lateinit var siteUtilsWrapper: SiteUtilsWrapper + + @Mock + private lateinit var siteModel: SiteModel + + private lateinit var builder: ActivityCardBuilder + + private val maxItemsInCard = 3 + private val displayDate = "X days ago" + private val activityLogModel = ActivityLogModel( + activityID = "1", + summary = "summary", + content = FormattableContent(text = "text"), + gridicon = "gridicon", + status = "OK", + published = CardsUtils.fromDate("2021-12-27 11:33:55"), + actor = ActivityLogModel.ActivityActor( + avatarURL = "avatarURL", + displayName = "name", + type = "type", + role = "admin", + wpcomUserID = 1L + ), + name = "name", + rewindable = false, + rewindID = "1234", + type = "type", + ) + + private val activityCardModel = ActivityCardModel(activities = listOf(activityLogModel)) + + private val onActivityCardFooterLinkClick: () -> Unit = {} + private val onActivityItemClick: (ActivityCardBuilderParams.ActivityCardItemClickParams) -> Unit = {} + + @Before + fun setUp() { + builder = ActivityCardBuilder(dashboardCardActivityLogConfig, dateTimeUtilsWrapper, siteUtilsWrapper) + } + + @Test + fun `given activity error, when build is called, then null is returned`() { + val activity = ActivityCardModel(error = ActivityCardError(ActivityCardErrorType.UNAUTHORIZED)) + + val result = buildActivityCard(activity) + + assertThat(result).isNull() + } + + @Test + fun `given generic error, when build is called, then null is returned`() { + val activity = ActivityCardModel(error = ActivityCardError(ActivityCardErrorType.GENERIC_ERROR)) + + val result = buildActivityCard(activity) + + assertThat(result).isNull() + } + + @Test + fun `given feature flag is disabled, when build is called, then null is returned`() { + whenever(dashboardCardActivityLogConfig.isEnabled()).thenReturn(false) + + val result = buildActivityCard(activityCardModel) + + assertThat(result).isNull() + } + + @Test + fun `given activities list is empty, when build is called, then null is returned`() { + whenever(dashboardCardActivityLogConfig.isEnabled()).thenReturn(true) + val activity = ActivityCardModel(activities = emptyList()) + + val result = buildActivityCard(activity) + + assertThat(result).isNull() + } + + @Test + fun `given site accessed is not via wpComOrJetpack, when build is called, then null is returned`() { + whenever(dashboardCardActivityLogConfig.isEnabled()).thenReturn(true) + whenever(siteUtilsWrapper.isAccessedViaWPComRest(any())).thenReturn(false) + whenever(siteModel.isJetpackConnected).thenReturn(true) + whenever(siteModel.hasCapabilityManageOptions).thenReturn(true) + whenever(siteModel.isWpForTeamsSite).thenReturn(true) + + val result = buildActivityCard(activityCardModel) + + assertThat(result).isNull() + } + + @Test + fun `given site is not Jetpack connected, when build is called, then null is returned`() { + whenever(dashboardCardActivityLogConfig.isEnabled()).thenReturn(true) + whenever(siteModel.isJetpackConnected).thenReturn(false) + + val result = buildActivityCard(activityCardModel) + + assertThat(result).isNull() + } + + @Test + fun `given does not hasCapabilityManageOptions for site, when build is called, then null is returned`() { + whenever(dashboardCardActivityLogConfig.isEnabled()).thenReturn(true) + whenever(siteUtilsWrapper.isAccessedViaWPComRest(any())).thenReturn(true) + whenever(siteModel.hasCapabilityManageOptions).thenReturn(false) + + val result = buildActivityCard(activityCardModel) + + assertThat(result).isNull() + } + + @Test + fun `given is wp for teams site, when build is called, then null is returned`() { + whenever(dashboardCardActivityLogConfig.isEnabled()).thenReturn(true) + whenever(siteUtilsWrapper.isAccessedViaWPComRest(any())).thenReturn(true) + whenever(siteModel.hasCapabilityManageOptions).thenReturn(true) + whenever(siteModel.isWpForTeamsSite).thenReturn(true) + + val result = buildActivityCard(activityCardModel) + + assertThat(result).isNull() + } + + @Test + fun `given feature flag enabled, when build is called, then card is returned`() { + whenever(dashboardCardActivityLogConfig.isEnabled()).thenReturn(true) + whenever(siteUtilsWrapper.isAccessedViaWPComRest(any())).thenReturn(true) + whenever(siteModel.hasCapabilityManageOptions).thenReturn(true) + whenever(siteModel.isWpForTeamsSite).thenReturn(false) + whenever(dateTimeUtilsWrapper.javaDateToTimeSpan(any())).thenReturn(displayDate) + + val result = buildActivityCard(activityCardModel) + + assertThat(result).isNotNull + } + + @Test + fun `given activities list size is greater than 3, when build is called, then only 3 activities are selected`() { + setShouldBuildActivityCard() + val activityModelWithFiveItems = ActivityCardModel(activities = List(5) { activityLogModel }) + + val result = buildActivityCard(activityModelWithFiveItems) + + assertThat((result as ActivityCard.ActivityCardWithItems).activityItems.size).isEqualTo(maxItemsInCard) + } + + private fun setShouldBuildActivityCard() { + whenever(dashboardCardActivityLogConfig.isEnabled()).thenReturn(true) + whenever(siteUtilsWrapper.isAccessedViaWPComRest(any())).thenReturn(true) + whenever(siteModel.hasCapabilityManageOptions).thenReturn(true) + whenever(siteModel.isWpForTeamsSite).thenReturn(false) + whenever(dateTimeUtilsWrapper.javaDateToTimeSpan(any())).thenReturn(displayDate) + } + + private fun buildActivityCard(model: ActivityCardModel) = builder.build( + ActivityCardBuilderParams( + site = siteModel, + activityCardModel = model, + onFooterLinkClick = onActivityCardFooterLinkClick, + onActivityItemClick = onActivityItemClick + ) + ) +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardDomainCardBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardDomainCardBuilderTest.kt new file mode 100644 index 000000000000..91a83cd60027 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/domain/DashboardDomainCardBuilderTest.kt @@ -0,0 +1,61 @@ +package org.wordpress.android.ui.mysite.cards.dashboard.domain + +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.wordpress.android.R +import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams +import org.wordpress.android.ui.utils.UiString + +class DashboardDomainCardBuilderTest { + private lateinit var builder: DashboardDomainCardBuilder + + @Before + fun setUp() { + builder = DashboardDomainCardBuilder() + } + @Test + fun `when is eligible for domain, then return the DashboardDomainCard`() { + // Arrange + val params = MySiteCardAndItemBuilderParams.DashboardCardDomainBuilderParams( + isEligible = true, + onClick = { }, + onHideMenuItemClick = { }, + onMoreMenuClick = { } + ) + + // Act + val result = builder.build(params) + + // Assert + Assert.assertNotNull(result) + Assert.assertEquals( + R.string.dashboard_card_domain_title, + (result!!.title as UiString.UiStringRes).stringRes + ) + Assert.assertEquals( + R.string.dashboard_card_domain_sub_title, + (result.subtitle as UiString.UiStringRes).stringRes + ) + Assert.assertNotNull(result.onClick) + Assert.assertNotNull(result.onHideMenuItemClick) + Assert.assertNotNull(result.onMoreMenuClick) + } + + @Test + fun `when is not eligible for blaze, then return null`() { + // Arrange + val params = MySiteCardAndItemBuilderParams.DashboardCardDomainBuilderParams( + isEligible = false, + onClick = { }, + onHideMenuItemClick = { }, + onMoreMenuClick = { } + ) + + // Act + val result = builder.build(params) + + // Assert + Assert.assertNull(result) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModelTest.kt index d096eac556ae..6d3fc187f49f 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/photopicker/PhotoPickerViewModelTest.kt @@ -245,12 +245,12 @@ class PhotoPickerViewModelTest : BaseUnitTest() { } @Test - fun `shows soft ask screen when storage permissions are turned off`() = test { - setupViewModel(listOf(), singleSelectBrowserType, hasStoragePermissions = false) + fun `shows soft ask screen when photos videos permissions are turned off`() = test { + setupViewModel(listOf(), singleSelectBrowserType, hasPhotosVideosPermissions = false) whenever(resourceProvider.getString(R.string.app_name)).thenReturn("WordPress") - whenever(resourceProvider.getString(R.string.photo_picker_soft_ask_label)).thenReturn("Soft ask label") + whenever(resourceProvider.getString(R.string.photo_picker_soft_ask_photos_label)).thenReturn("Soft ask label") - viewModel.checkStoragePermission(isAlwaysDenied = false) + viewModel.checkMediaPermissions(isPhotosVideosAlwaysDenied = false, isMusicAudioAlwaysDenied = false) assertThat(uiStates).hasSize(2) @@ -380,9 +380,9 @@ class PhotoPickerViewModelTest : BaseUnitTest() { private suspend fun setupViewModel( domainModel: List, browserType: MediaBrowserType, - hasStoragePermissions: Boolean = true + hasPhotosVideosPermissions: Boolean = true ) { - whenever(permissionsHandler.hasWriteStoragePermission()).thenReturn(hasStoragePermissions) + whenever(permissionsHandler.hasPhotosVideosPermission()).thenReturn(hasPhotosVideosPermissions) viewModel.start(listOf(), browserType, null, site) whenever(deviceMediaListBuilder.buildDeviceMedia(browserType)).thenReturn(domainModel) viewModel.uiState.observeForever { diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelCopyPostTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelCopyPostTest.kt index ed73fc7439cc..ef1c16728773 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelCopyPostTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelCopyPostTest.kt @@ -75,7 +75,8 @@ class PostListMainViewModelCopyPostTest : BaseUnitTest() { savePostToDbUseCase = mock(), jetpackFeatureRemovalPhaseHelper = mock(), blazeFeatureUtils = mock(), - blazeStore = mock() + blazeStore = mock(), + siteUtilsWrapper = mock() ) viewModel.postListAction.observeForever(onPostListActionObserver) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelTest.kt index 77426f8dfe81..4c18026b1f67 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelTest.kt @@ -18,10 +18,12 @@ import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.model.LocalOrRemoteId.LocalId import org.wordpress.android.fluxc.model.PostModel import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper import org.wordpress.android.ui.posts.PostListViewLayoutType.COMPACT import org.wordpress.android.ui.posts.PostListViewLayoutType.STANDARD import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.ui.uploads.UploadStarter +import org.wordpress.android.util.SiteUtilsWrapper import org.wordpress.android.viewmodel.Event @ExperimentalCoroutinesApi @@ -41,6 +43,12 @@ class PostListMainViewModelTest : BaseUnitTest() { @Mock lateinit var savePostToDbUseCase: SavePostToDbUseCase + + @Mock + lateinit var jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper + + @Mock + lateinit var siteUtilsWrapper: SiteUtilsWrapper private lateinit var viewModel: PostListMainViewModel @Before @@ -50,6 +58,7 @@ class PostListMainViewModelTest : BaseUnitTest() { } whenever(editPostRepository.postChanged).thenReturn(MutableLiveData(Event(PostModel()))) + whenever(siteUtilsWrapper.supportsStoriesFeature(any(), any())).thenReturn(true) viewModel = PostListMainViewModel( dispatcher = dispatcher, @@ -69,7 +78,8 @@ class PostListMainViewModelTest : BaseUnitTest() { savePostToDbUseCase = savePostToDbUseCase, jetpackFeatureRemovalPhaseHelper = mock(), blazeFeatureUtils = mock(), - blazeStore = mock() + blazeStore = mock(), + siteUtilsWrapper = siteUtilsWrapper ) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostUtilsUnitTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostUtilsUnitTest.kt index 324eb458dc08..873e5dbad75f 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostUtilsUnitTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostUtilsUnitTest.kt @@ -45,6 +45,51 @@ class PostUtilsUnitTest { assertThat(PostUtils.removeWPGallery(content)).isEqualTo(expectedResult) } + @Test + fun `removeWPVideoPress removes VideoPress block tags and its internals without affecting content in between`() { + val content = """ + +

Before

+ + + +
+https://videopress.com/v/AbCDe?resizeToParent=true&cover=true&preloadContent=metadata&useAverageColor=true +
+ + + +

Between

+ + + +
+https://videopress.com/v/AbCDe?resizeToParent=true&cover=true&preloadContent=metadata&useAverageColor=true +
+ + + +

After

+ +""" + val expectedResult = """ + +

Before

+ + + + +

Between

+ + + + +

After

+ +""" + assertThat(PostUtils.removeWPVideoPress(content)).isEqualTo(expectedResult) + } + @Test fun `prepareForPublish updates dateLocallyChanged`() { val post = invokePreparePostForPublish() diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorTest.kt index b159e8cf385b..241375ec69eb 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorTest.kt @@ -37,6 +37,14 @@ class MediaUploadCompletionProcessorTest { Assertions.assertThat(blocks).isEqualTo(TestContent.newPostVideo) } + @Test + fun `processPost splices id and guid for a VideoPress block`() { + whenever(mediaFile.videoPressGuid).thenReturn(TestContent.videoPressGuid) + processor = MediaUploadCompletionProcessor(TestContent.localMediaId, mediaFile, TestContent.siteUrl) + val blocks = processor.processContent(TestContent.oldPostVideoPress) + Assertions.assertThat(blocks).isEqualTo(TestContent.newPostVideoPress) + } + @Test fun `processPost splices id and url for a media-text block`() { val blocks = processor.processContent(TestContent.oldPostMediaText) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/TestContent.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/TestContent.kt index c1b5d730cd37..43341fc1e89b 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/TestContent.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/TestContent.kt @@ -30,6 +30,7 @@ object TestContent { const val remoteMediaId = "97629" const val remoteMediaId2 = "97630" const val attachmentPageUrl = "https://wordpress.org?p=${remoteMediaId}" + const val videoPressGuid = "AbCdE" const val oldImageBlock = """
@@ -659,10 +660,20 @@ $newRefactoredGalleryBlockInnerBlocks
""" + const val oldVideoPressBlockWithDefaultAttrs = """""" + + const val newVideoPressBlockWithDefaultAttrs = """""" + + const val oldVideoPressBlockWithAttrs = """""" + + const val newVideoPressBlockWithAttrs = """""" + const val oldPostImage = paragraphBlock + oldImageBlock + newVideoBlock + newMediaTextBlock + newGalleryBlock const val newPostImage = paragraphBlock + newImageBlock + newVideoBlock + newMediaTextBlock + newGalleryBlock const val oldPostVideo = paragraphBlock + newImageBlock + oldVideoBlock + newMediaTextBlock + newGalleryBlock const val newPostVideo = paragraphBlock + newImageBlock + newVideoBlock + newMediaTextBlock + newGalleryBlock + const val oldPostVideoPress = paragraphBlock + newImageBlock + oldVideoPressBlockWithDefaultAttrs + newMediaTextBlock + newGalleryBlock + const val newPostVideoPress = paragraphBlock + newImageBlock + newVideoPressBlockWithDefaultAttrs + newMediaTextBlock + newGalleryBlock const val oldPostMediaText = paragraphBlock + newImageBlock + newVideoBlock + oldMediaTextBlock + newGalleryBlock const val newPostMediaText = paragraphBlock + newImageBlock + newVideoBlock + newMediaTextBlock + newGalleryBlock const val oldPostGallery = paragraphBlock + newImageBlock + newVideoBlock + newMediaTextBlock + oldGalleryBlock diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessorTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessorTest.kt index 0ca83416b29c..971ee5ac93d3 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessorTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessorTest.kt @@ -29,4 +29,12 @@ class VideoBlockProcessorTest { val processedBlock = processor.processBlock(TestContent.oldVideoBlockIdNotFirst) Assertions.assertThat(processedBlock).isEqualTo(TestContent.newVideoBlockIdNotFirst) } + + @Test + fun `processBlock leaves VideoPress block unchanged`() { + val nonMatchingId = "123" + val processor = VideoPressBlockProcessor(nonMatchingId, mediaFile) + val processedBlock = processor.processBlock(TestContent.oldVideoPressBlockWithDefaultAttrs, true) + Assertions.assertThat(processedBlock).isEqualTo(TestContent.oldVideoPressBlockWithDefaultAttrs) + } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoPressBlockProcessorTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoPressBlockProcessorTest.kt new file mode 100644 index 000000000000..fb9afb17f059 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoPressBlockProcessorTest.kt @@ -0,0 +1,44 @@ +package org.wordpress.android.ui.posts.mediauploadcompletionprocessors + +import org.assertj.core.api.Assertions +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.wordpress.android.util.helpers.MediaFile + +@RunWith(MockitoJUnitRunner::class) +class VideoPressBlockProcessorTest { + private val mediaFile: MediaFile = mock() + private lateinit var processor: VideoPressBlockProcessor + + @Before + fun before() { + whenever(mediaFile.mediaId).thenReturn(TestContent.remoteMediaId) + whenever(mediaFile.videoPressGuid).thenReturn(TestContent.videoPressGuid) + + processor = VideoPressBlockProcessor(TestContent.localMediaId, mediaFile) + } + + @Test + fun `processBlock replaces id in VideoPress block with default attributes`() { + val processedBlock = processor.processBlock(TestContent.oldVideoPressBlockWithDefaultAttrs, true) + Assertions.assertThat(processedBlock).isEqualTo(TestContent.newVideoPressBlockWithDefaultAttrs) + } + + @Test + fun `processBlock replaces id in VideoPress block with different attributes to the default`() { + val processedBlock = processor.processBlock(TestContent.oldVideoPressBlockWithAttrs, true) + Assertions.assertThat(processedBlock).isEqualTo(TestContent.newVideoPressBlockWithAttrs) + } + + @Test + fun `processBlock leaves Video block unchanged`() { + val nonMatchingId = "123" + val processor = VideoPressBlockProcessor(nonMatchingId, mediaFile) + val processedBlock = processor.processBlock(TestContent.oldVideoBlock) + Assertions.assertThat(processedBlock).isEqualTo(TestContent.oldVideoBlock) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/SiteCreationFixtures.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/SiteCreationFixtures.kt new file mode 100644 index 000000000000..f8efb2efdccd --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/SiteCreationFixtures.kt @@ -0,0 +1,55 @@ +package org.wordpress.android.ui.sitecreation + +import org.mockito.kotlin.mock +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.rest.wpcom.transactions.TransactionsRestClient.CreateShoppingCartResponse +import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged +import org.wordpress.android.fluxc.store.SiteStore.SiteError +import org.wordpress.android.fluxc.store.SiteStore.SiteErrorType.GENERIC_ERROR +import org.wordpress.android.fluxc.store.TransactionsStore.CreateShoppingCartError +import org.wordpress.android.fluxc.store.TransactionsStore.OnShoppingCartCreated +import org.wordpress.android.ui.domains.DomainRegistrationCheckoutWebViewActivity +import org.wordpress.android.ui.domains.DomainRegistrationCompletedEvent +import org.wordpress.android.ui.sitecreation.SiteCreationResult.Completed +import org.wordpress.android.ui.sitecreation.SiteCreationResult.Created +import org.wordpress.android.ui.sitecreation.SiteCreationResult.CreatedButNotFetched +import org.wordpress.android.ui.sitecreation.domains.DomainModel +import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState +import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.CREATE_SITE +import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.FAILURE +import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.SUCCESS +import org.wordpress.android.ui.sitecreation.theme.defaultTemplateSlug + +const val SUB_DOMAIN = "test" +const val URL = "$SUB_DOMAIN.wordpress.com" +const val URL_CUSTOM = "$SUB_DOMAIN.host.com" +const val SITE_SLUG = "${SUB_DOMAIN}host0.wordpress.com" +val FREE_DOMAIN = DomainModel(URL, true, "", 1, false) +val PAID_DOMAIN = DomainModel(URL_CUSTOM, false, "$1", 2, true) + +const val SITE_REMOTE_ID = 1L + +val SITE_CREATION_STATE = SiteCreationState( + segmentId = 1, + siteDesign = defaultTemplateSlug, + domain = FREE_DOMAIN, +) + +val SITE_MODEL = SiteModel().apply { siteId = SITE_REMOTE_ID; url = SITE_SLUG } + +val CHECKOUT_DETAILS = DomainRegistrationCheckoutWebViewActivity.OpenCheckout.CheckoutDetails(SITE_MODEL, SITE_SLUG) +val CHECKOUT_EVENT = DomainRegistrationCompletedEvent(URL_CUSTOM, "email@host.com") + +val FETCH_SUCCESS = OnSiteChanged(1) +val FETCH_ERROR = OnSiteChanged(0).apply { error = SiteError(GENERIC_ERROR) } + +val CART_SUCCESS = OnShoppingCartCreated(mock()) +val CART_ERROR = OnShoppingCartCreated(mock()) + +val RESULT_CREATED = mock() +val RESULT_NOT_IN_LOCAL_DB = CreatedButNotFetched.NotInLocalDb(SITE_MODEL) +val RESULT_IN_CART = CreatedButNotFetched.InCart(SITE_MODEL) +val RESULT_COMPLETED = Completed(SITE_MODEL) + +val SERVICE_SUCCESS = SiteCreationServiceState(SUCCESS, Pair(SITE_REMOTE_ID, SITE_SLUG)) +val SERVICE_ERROR = SiteCreationServiceState(FAILURE, SiteCreationServiceState(CREATE_SITE)) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVMTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVMTest.kt index 70bee937febc..2708dad18238 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVMTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/SiteCreationMainVMTest.kt @@ -7,14 +7,15 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat +import org.mockito.kotlin.argWhere import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq import org.mockito.kotlin.isA import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -27,27 +28,28 @@ import org.wordpress.android.R import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.model.experiments.Variation.Control import org.wordpress.android.fluxc.model.experiments.Variation.Treatment +import org.wordpress.android.ui.domains.DomainsRegistrationTracker import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleEmpty import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleGeneral import org.wordpress.android.ui.sitecreation.SiteCreationMainVM.SiteCreationScreenTitle.ScreenTitleStepCount +import org.wordpress.android.ui.sitecreation.SiteCreationResult.CreatedButNotFetched.DomainRegistrationPurchased import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState.SiteCreationCompleted import org.wordpress.android.ui.sitecreation.usecases.FetchHomePageLayoutsUseCase import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.config.SiteCreationDomainPurchasingFeatureConfig import org.wordpress.android.util.experiments.SiteCreationDomainPurchasingExperiment +import org.wordpress.android.util.extensions.getParcelableCompat import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.wizard.WizardManager import org.wordpress.android.viewmodel.SingleLiveEvent import org.wordpress.android.viewmodel.helpers.DialogHolder +import kotlin.test.assertEquals +import kotlin.test.assertIs -private const val LOCAL_SITE_ID = 1 private const val SEGMENT_ID = 1L private const val VERTICAL = "Test Vertical" -private const val DOMAIN = "test.domain.com" private const val STEP_COUNT = 20 private const val FIRST_STEP_INDEX = 1 private const val LAST_STEP_INDEX = STEP_COUNT @@ -62,7 +64,7 @@ class SiteCreationMainVMTest : BaseUnitTest() { lateinit var navigationTargetObserver: Observer @Mock - lateinit var wizardFinishedObserver: Observer + lateinit var onCompletedObserver: Observer @Mock lateinit var wizardExitedObserver: Observer @@ -103,6 +105,9 @@ class SiteCreationMainVMTest : BaseUnitTest() { @Mock lateinit var domainPurchasingFeatureConfig: SiteCreationDomainPurchasingFeatureConfig + @Mock + lateinit var domainsRegistrationTracker: DomainsRegistrationTracker + private val wizardManagerNavigatorLiveData = SingleLiveEvent() private lateinit var viewModel: SiteCreationMainVM @@ -111,13 +116,12 @@ class SiteCreationMainVMTest : BaseUnitTest() { fun setUp() { whenever(wizardManager.navigatorLiveData).thenReturn(wizardManagerNavigatorLiveData) whenever(wizardManager.showNextStep()).then { - wizardManagerNavigatorLiveData.value = siteCreationStep - Unit + run { wizardManagerNavigatorLiveData.value = siteCreationStep } } viewModel = getNewViewModel() viewModel.start(null, SiteCreationSource.UNSPECIFIED) viewModel.navigationTargetObservable.observeForever(navigationTargetObserver) - viewModel.wizardFinishedObservable.observeForever(wizardFinishedObserver) + viewModel.onCompleted.observeForever(onCompletedObserver) viewModel.dialogActionObservable.observeForever(dialogActionsObserver) viewModel.exitFlowObservable.observeForever(wizardExitedObserver) viewModel.onBackPressedObservable.observeForever(onBackPressedObserver) @@ -128,25 +132,86 @@ class SiteCreationMainVMTest : BaseUnitTest() { @Test fun domainSelectedResultsInNextStep() { - viewModel.onDomainsScreenFinished(DOMAIN) + viewModel.onDomainsScreenFinished(FREE_DOMAIN) verify(wizardManager).showNextStep() } @Test fun siteCreationStateUpdatedWithSelectedDomain() { - viewModel.onDomainsScreenFinished(DOMAIN) - assertThat(currentWizardState(viewModel).domain).isEqualTo(DOMAIN) + viewModel.onDomainsScreenFinished(FREE_DOMAIN) + assertThat(currentWizardState(viewModel).domain).isEqualTo(FREE_DOMAIN) } @Test - fun wizardFinishedInvokedOnSitePreviewCompleted() { - val state = SiteCreationCompleted(LOCAL_SITE_ID, false) - viewModel.onSitePreviewScreenFinished(state) + fun `on wizard finished is propagated`() { + viewModel.onWizardFinished(RESULT_COMPLETED) + verify(onCompletedObserver).onChanged(eq(RESULT_COMPLETED to false)) + } - val captor = ArgumentCaptor.forClass(CreateSiteState::class.java) - verify(wizardFinishedObserver).onChanged(captor.capture()) + @Test + fun `on cart created propagates details to show checkout`() { + viewModel.onCartCreated(CHECKOUT_DETAILS) + assertEquals(CHECKOUT_DETAILS, viewModel.showDomainCheckout.value) + } - assertThat(captor.value).isEqualTo(state) + @Test + fun `on cart created tracks checkout webview viewed`() { + viewModel.onCartCreated(CHECKOUT_DETAILS) + verify(domainsRegistrationTracker).trackDomainsPurchaseWebviewViewed(eq(CHECKOUT_DETAILS.site), eq(true)) + } + + @Test + fun `on cart created updates result`() { + viewModel.onCartCreated(CHECKOUT_DETAILS) + + // Assert on the private state via bundle + viewModel.writeToBundle(savedInstanceState) + verify(savedInstanceState).putParcelable( + eq(KEY_SITE_CREATION_STATE), + argWhere { it.result == RESULT_IN_CART } + ) + } + + @Test + fun `on checkout result when null shows previous step`() { + viewModel.onCheckoutResult(null) + + verify(wizardManager).onBackPressed() + verify(onBackPressedObserver).onChanged(anyOrNull()) + } + + @Test + fun `on checkout result when not null shows next step`() { + viewModel.onCartCreated(CHECKOUT_DETAILS) + + viewModel.onCheckoutResult(CHECKOUT_EVENT) + + verify(wizardManager).showNextStep() + } + + @Test + fun `on checkout result when not null updates result`() { + viewModel.onCartCreated(CHECKOUT_DETAILS) + + viewModel.onCheckoutResult(CHECKOUT_EVENT) + + assertIs(currentWizardState(viewModel).result).run { + assertEquals(CHECKOUT_DETAILS.site, site) + assertEquals(CHECKOUT_EVENT.domainName, domainName) + assertEquals(CHECKOUT_EVENT.email, email) + } + } + + @Test + fun `on progress screen finished updates result`() { + viewModel.onFreeSiteCreated(SITE_MODEL) + assertThat(currentWizardState(viewModel).result).isEqualTo(RESULT_NOT_IN_LOCAL_DB) + } + + @Test + fun `on progress screen finished shows next step`() { + viewModel.onFreeSiteCreated(SITE_MODEL) + verify(wizardManager).showNextStep() } @Test @@ -185,6 +250,7 @@ class SiteCreationMainVMTest : BaseUnitTest() { @Test fun dialogShownOnBackPressedWhenLastStepAndSiteCreationNotCompleted() { whenever(wizardManager.isLastStep()).thenReturn(true) + viewModel.onWizardCancelled() viewModel.onBackPressed() verify(dialogActionsObserver).onChanged(any()) } @@ -192,7 +258,7 @@ class SiteCreationMainVMTest : BaseUnitTest() { @Test fun flowExitedOnBackPressedWhenLastStepAndSiteCreationCompleted() { whenever(wizardManager.isLastStep()).thenReturn(true) - viewModel.onSiteCreationCompleted() + viewModel.onWizardFinished(RESULT_COMPLETED) viewModel.onBackPressed() verify(wizardExitedObserver).onChanged(anyOrNull()) } @@ -245,7 +311,7 @@ class SiteCreationMainVMTest : BaseUnitTest() { /* we need to model a real use case of data only existing for steps the user has visited (Segment only in this case). Otherwise, subsequent steps' state will be cleared and make the test fail. (issue #10189)*/ val expectedState = SiteCreationState(segmentId = SEGMENT_ID) - whenever(savedInstanceState.getParcelable(KEY_SITE_CREATION_STATE)) + whenever(savedInstanceState.getParcelableCompat(KEY_SITE_CREATION_STATE)) .thenReturn(expectedState) // we need to create a new instance of the VM as the `viewModel` has already been started in setUp() @@ -266,7 +332,7 @@ class SiteCreationMainVMTest : BaseUnitTest() { whenever(savedInstanceState.getInt(KEY_CURRENT_STEP)).thenReturn(index) // siteCreationState is not nullable - we need to set it - whenever(savedInstanceState.getParcelable(KEY_SITE_CREATION_STATE)) + whenever(savedInstanceState.getParcelableCompat(KEY_SITE_CREATION_STATE)) .thenReturn(SiteCreationState()) // we need to create a new instance of the VM as the `viewModel` has already been started in setUp() @@ -290,7 +356,7 @@ class SiteCreationMainVMTest : BaseUnitTest() { @Test fun `given instance state is not null, when start, then site creation accessed is not tracked`() { val expectedState = SiteCreationState(segmentId = SEGMENT_ID) - whenever(savedInstanceState.getParcelable(KEY_SITE_CREATION_STATE)) + whenever(savedInstanceState.getParcelableCompat(KEY_SITE_CREATION_STATE)) .thenReturn(expectedState) val newViewModel = getNewViewModel() @@ -309,6 +375,7 @@ class SiteCreationMainVMTest : BaseUnitTest() { verify(tracker, never()).trackSiteCreationDomainPurchasingExperimentVariation(any()) } + @Test fun `given domain purchasing experiment on, when start in control variation, then experiment is tracked`() { whenever(domainPurchasingFeatureConfig.isEnabledState()).thenReturn(true) @@ -329,8 +396,7 @@ class SiteCreationMainVMTest : BaseUnitTest() { verify(tracker).trackSiteCreationDomainPurchasingExperimentVariation(isA()) } - private fun currentWizardState(vm: SiteCreationMainVM) = - vm.navigationTargetObservable.lastEvent!!.wizardState + private fun currentWizardState(vm: SiteCreationMainVM) = vm.navigationTargetObservable.lastEvent!!.wizardState private fun getNewViewModel() = SiteCreationMainVM( tracker, @@ -342,5 +408,6 @@ class SiteCreationMainVMTest : BaseUnitTest() { jetpackFeatureRemovalOverlayUtil, domainPurchasingExperiment, domainPurchasingFeatureConfig, + domainsRegistrationTracker, ) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModelTest.kt index cb0d89d48b0b..2678a283b23c 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/domains/SiteCreationDomainsViewModelTest.kt @@ -11,9 +11,9 @@ import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.any +import org.mockito.kotlin.argWhere import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq -import org.mockito.kotlin.firstValue import org.mockito.kotlin.lastValue import org.mockito.kotlin.mock import org.mockito.kotlin.secondValue @@ -31,12 +31,12 @@ import org.wordpress.android.fluxc.store.ProductsStore import org.wordpress.android.fluxc.store.ProductsStore.OnProductsFetched import org.wordpress.android.fluxc.store.SiteStore.OnSuggestedDomains import org.wordpress.android.fluxc.store.SiteStore.SuggestDomainError -import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.DomainModel import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.DomainsUiState +import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.DomainsUiState.CreateSiteButtonState import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.DomainsUiState.DomainsUiContentState import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Cost -import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Variant +import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.New.DomainUiState.Tag import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.Old import org.wordpress.android.ui.sitecreation.domains.SiteCreationDomainsViewModel.ListItemUiState.Old.DomainUiState.UnavailableDomain import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker @@ -75,7 +75,7 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { private lateinit var uiStateObserver: Observer @Mock - private lateinit var createSiteBtnObserver: Observer + private lateinit var createSiteBtnObserver: Observer @Mock private lateinit var clearBtnObserver: Observer @@ -292,17 +292,14 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { verify(clearBtnObserver, times(1)).onChanged(captor.capture()) } - /** - * Verifies that create site button is properly propagated when a domain is selected. - */ @Test - fun verifyCreateSiteBtnClickedPropagated() = testWithSuccessResponse { - val domainName = "test.domain" - viewModel.onDomainSelected(mockDomain(domainName)) + fun `verify click on the create site button emits the selected domain`() = testWithSuccessResponse { + val selectedDomain = mockDomain("test.domain") + viewModel.onDomainSelected(selectedDomain) + viewModel.onCreateSiteBtnClicked() - val captor = ArgumentCaptor.forClass(String::class.java) - verify(createSiteBtnObserver, times(1)).onChanged(captor.capture()) - assertThat(captor.firstValue).isEqualTo(domainName) + + verify(createSiteBtnObserver).onChanged(argWhere { it == selectedDomain }) } @Test @@ -320,6 +317,15 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { ) } + @Test + fun `verify create site button text is not changed when purchasing feature is OFF`() = testWithSuccessResponse { + viewModel.start() + + viewModel.onDomainSelected(mock()) + + assertIs(viewModel.uiState.value?.createSiteButtonState) + } + // region New UI private fun testNewUi(block: suspend CoroutineScope.() -> Unit) = test { @@ -336,10 +342,11 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { val suggestions = List(size) { DomainSuggestionResponse().apply { - domain_name = "$query-$it.com" + domain_name = if (it == 0) query else "$query-$it.com" is_free = it % 2 == 0 cost = if (is_free) "Free" else "$$it.00" product_id = it + supports_privacy = !is_free } } @@ -374,6 +381,25 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { verify(productsStore).fetchProducts(eq(TYPE_DOMAINS_PRODUCT)) } + @Test + fun `verify create site button text changes when selecting a free domain`() = testNewUi { + viewModel.start() + + viewModel.onDomainSelected(mockDomain(free = true)) + + assertIs(viewModel.uiState.value?.createSiteButtonState) + } + + @Test + fun `verify create site button text changes when selecting a non-free domain`() = testNewUi { + viewModel.start() + + viewModel.onDomainSelected(mockDomain(free = false)) + + assertIs(viewModel.uiState.value?.createSiteButtonState) + } + + @Test fun `verify all domain results from api are visible`() = testWithSuccessResultNewUi { (query, results) -> viewModel.start() @@ -419,7 +445,7 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { } @Test - fun `verify sale domain results from api have variant 'Sale'`() = testWithSuccessResultNewUi { (query) -> + fun `verify sale domain results from api have tag 'Sale'`() = testWithSuccessResultNewUi { (query) -> whenever(productsStore.fetchProducts(any())).thenReturn(OnProductsFetched(SALE_PRODUCTS)) viewModel.start() @@ -427,7 +453,7 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { viewModel.onQueryChanged(query) advanceUntilIdle() - assertThat(uiDomains).filteredOn { it.variant is Variant.Sale }.hasSameSizeAs(SALE_PRODUCTS) + assertThat(uiDomains.flatMap { it.tags }).filteredOn { it is Tag.Sale }.hasSameSizeAs(SALE_PRODUCTS) } @Test @@ -437,7 +463,7 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { viewModel.onQueryChanged(query) advanceUntilIdle() - assertThat(uiDomains.map { it.variant }).containsOnlyOnce(Variant.Recommended) + assertThat(uiDomains.flatMap { it.tags }).filteredOn { it is Tag.Recommended }.singleElement() } @Test @@ -447,7 +473,18 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { viewModel.onQueryChanged(query) advanceUntilIdle() - assertThat(uiDomains.map { it.variant }).containsOnlyOnce(Variant.BestAlternative) + assertThat(uiDomains.flatMap { it.tags }).filteredOn { it is Tag.BestAlternative }.singleElement() + } + + @Test + fun `verify selected domain is propagated to UI on click`() = testWithSuccessResultNewUi { (query) -> + viewModel.start() + + viewModel.onQueryChanged(query) + advanceUntilIdle() + viewModel.onDomainSelected(mockDomain(query)) + + assertThat(uiDomains.map { it.isSelected }).containsOnlyOnce(true) } // endregion @@ -464,7 +501,7 @@ class SiteCreationDomainsViewModelTest : BaseUnitTest() { assertThat(uiState.searchInputUiState.showProgress).isEqualTo(showProgress) assertThat(uiState.searchInputUiState.showClearButton).isEqualTo(showClearButton) assertThat(uiState.contentState).isInstanceOf(DomainsUiContentState.Initial::class.java) - assertThat(uiState.createSiteButtonContainerVisibility).isEqualTo(false) + assertThat(uiState.createSiteButtonState).isNull() } /** diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/CreateSiteUseCaseTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/CreateSiteUseCaseTest.kt index 0cbf77042355..bbecfe3041ba 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/CreateSiteUseCaseTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/CreateSiteUseCaseTest.kt @@ -5,33 +5,46 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.any +import org.mockito.kotlin.argThat import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.Dispatcher -import org.wordpress.android.fluxc.action.SiteAction import org.wordpress.android.fluxc.annotations.action.Action import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.fluxc.store.SiteStore.NewSitePayload import org.wordpress.android.fluxc.store.SiteStore.OnNewSiteCreated import org.wordpress.android.fluxc.store.SiteStore.SiteVisibility +import org.wordpress.android.ui.sitecreation.FREE_DOMAIN +import org.wordpress.android.ui.sitecreation.PAID_DOMAIN +import org.wordpress.android.ui.sitecreation.SITE_REMOTE_ID import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceData import org.wordpress.android.ui.sitecreation.usecases.CreateSiteUseCase import org.wordpress.android.util.UrlUtilsWrapper +import kotlin.test.assertEquals +import kotlin.test.assertNotNull private const val SITE_TITLE = "site title" -private val DUMMY_SITE_DATA: SiteCreationServiceData = SiteCreationServiceData( +private val SITE_DATA_FREE = SiteCreationServiceData( 123, "slug", - "domain", - SITE_TITLE + FREE_DOMAIN.domainName, + SITE_TITLE, + FREE_DOMAIN.isFree, +) +private val SITE_DATA_PAID = SiteCreationServiceData( + 123, + "slug", + PAID_DOMAIN.domainName, + SITE_TITLE, + PAID_DOMAIN.isFree, ) private const val LANGUAGE_ID = "lang_id" private const val TIMEZONE_ID = "timezone_id" +private val EVENT = OnNewSiteCreated(newSiteRemoteId = SITE_REMOTE_ID) @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) @@ -45,83 +58,65 @@ class CreateSiteUseCaseTest : BaseUnitTest() { @Mock private lateinit var urlUtilsWrapper: UrlUtilsWrapper private lateinit var useCase: CreateSiteUseCase - private lateinit var event: OnNewSiteCreated @Before fun setUp() { useCase = CreateSiteUseCase(dispatcher, store, urlUtilsWrapper) - event = OnNewSiteCreated(newSiteRemoteId = 123) + whenever(dispatcher.dispatch(any())).then { useCase.onNewSiteCreated(EVENT) } } @Test fun coroutineResumedWhenResultEventDispatched() = test { - whenever(dispatcher.dispatch(any())).then { useCase.onNewSiteCreated(event) } - val resultEvent = useCase.createSite(DUMMY_SITE_DATA, LANGUAGE_ID, TIMEZONE_ID) - - assertThat(resultEvent).isEqualTo(event) + val resultEvent = useCase.createSite(SITE_DATA_FREE, LANGUAGE_ID, TIMEZONE_ID) + assertThat(resultEvent).isEqualTo(EVENT) } @Test fun verifySiteDataPropagated() = test { - whenever(dispatcher.dispatch(any())).then { useCase.onNewSiteCreated(event) } - useCase.createSite(DUMMY_SITE_DATA, LANGUAGE_ID, TIMEZONE_ID) - - val captor = ArgumentCaptor.forClass(Action::class.java) - verify(dispatcher).dispatch(captor.capture()) + useCase.createSite(SITE_DATA_PAID, LANGUAGE_ID, TIMEZONE_ID) + + verify(dispatcher).dispatch(argPayload { + assertEquals(SITE_DATA_PAID.domain, siteName) + assertEquals(SITE_DATA_PAID.segmentId, segmentId) + assertEquals(SITE_DATA_PAID.title, siteTitle) + val findAvailableUrl = assertNotNull(findAvailableUrl) + findAvailableUrl + }) + } - assertThat(captor.value.type).isEqualTo(SiteAction.CREATE_NEW_SITE) - assertThat(captor.value.payload).isInstanceOf(NewSitePayload::class.java) - val payload = captor.value.payload as NewSitePayload - assertThat(payload.siteName).isEqualTo(DUMMY_SITE_DATA.domain) - assertThat(payload.segmentId).isEqualTo(DUMMY_SITE_DATA.segmentId) - assertThat(payload.siteTitle).isEqualTo(SITE_TITLE) + @Test + fun verifySiteDataWhenFreePropagatesNoFindAvailableUrl() = test { + whenever(urlUtilsWrapper.extractSubDomain(any())).thenReturn(SITE_DATA_FREE.domain) + useCase.createSite(SITE_DATA_FREE, LANGUAGE_ID, TIMEZONE_ID) + verify(dispatcher).dispatch(argPayload { + assertEquals(SITE_DATA_FREE.domain, siteName) + findAvailableUrl == null + }) } @Test fun verifyDryRunIsFalse() = test { - whenever(dispatcher.dispatch(any())).then { useCase.onNewSiteCreated(event) } - useCase.createSite(DUMMY_SITE_DATA, LANGUAGE_ID, TIMEZONE_ID) - - val captor = ArgumentCaptor.forClass(Action::class.java) - verify(dispatcher).dispatch(captor.capture()) - - val payload = captor.value.payload as NewSitePayload - assertThat(payload.dryRun).isEqualTo(false) + useCase.createSite(SITE_DATA_FREE, LANGUAGE_ID, TIMEZONE_ID) + verify(dispatcher).dispatch(argPayload { !dryRun }) } @Test fun verifyCreatesPublicSite() = test { - whenever(dispatcher.dispatch(any())).then { useCase.onNewSiteCreated(event) } - useCase.createSite(DUMMY_SITE_DATA, LANGUAGE_ID, TIMEZONE_ID) - - val captor = ArgumentCaptor.forClass(Action::class.java) - verify(dispatcher).dispatch(captor.capture()) - - val payload = captor.value.payload as NewSitePayload - assertThat(payload.visibility).isEqualTo(SiteVisibility.PUBLIC) + useCase.createSite(SITE_DATA_FREE, LANGUAGE_ID, TIMEZONE_ID) + verify(dispatcher).dispatch(argPayload { visibility == SiteVisibility.PUBLIC }) } @Test fun verifyPropagatesLanguageId() = test { - whenever(dispatcher.dispatch(any())).then { useCase.onNewSiteCreated(event) } - useCase.createSite(DUMMY_SITE_DATA, LANGUAGE_ID, TIMEZONE_ID) - - val captor = ArgumentCaptor.forClass(Action::class.java) - verify(dispatcher).dispatch(captor.capture()) - - val payload = captor.value.payload as NewSitePayload - assertThat(payload.language).isEqualTo(LANGUAGE_ID) + useCase.createSite(SITE_DATA_FREE, LANGUAGE_ID, TIMEZONE_ID) + verify(dispatcher).dispatch(argPayload { language == LANGUAGE_ID }) } @Test fun verifyPropagatesTimeZoneId() = test { - whenever(dispatcher.dispatch(any())).then { useCase.onNewSiteCreated(event) } - useCase.createSite(DUMMY_SITE_DATA, LANGUAGE_ID, TIMEZONE_ID) - - val captor = ArgumentCaptor.forClass(Action::class.java) - verify(dispatcher).dispatch(captor.capture()) - - val payload = captor.value.payload as NewSitePayload - assertThat(payload.timeZoneId).isEqualTo(TIMEZONE_ID) + useCase.createSite(SITE_DATA_FREE, LANGUAGE_ID, TIMEZONE_ID) + verify(dispatcher).dispatch(argPayload { timeZoneId == TIMEZONE_ID }) } } + +fun argPayload(predicate: NewSitePayload.() -> Boolean) = argThat> { predicate(payload) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt index edbb991c60d2..8d1d17845f05 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/previews/SitePreviewViewModelTest.kt @@ -1,75 +1,53 @@ package org.wordpress.android.ui.sitecreation.previews -import android.os.Bundle import androidx.lifecycle.Observer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.advanceUntilIdle import org.assertj.core.api.Assertions.assertThat import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.eq -import org.mockito.kotlin.notNull +import org.mockito.kotlin.isA +import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.Dispatcher -import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.SiteStore import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged -import org.wordpress.android.fluxc.store.SiteStore.SiteError -import org.wordpress.android.fluxc.store.SiteStore.SiteErrorType.GENERIC_ERROR +import org.wordpress.android.ui.sitecreation.FETCH_ERROR +import org.wordpress.android.ui.sitecreation.FETCH_SUCCESS +import org.wordpress.android.ui.sitecreation.RESULT_COMPLETED +import org.wordpress.android.ui.sitecreation.RESULT_CREATED +import org.wordpress.android.ui.sitecreation.RESULT_NOT_IN_LOCAL_DB +import org.wordpress.android.ui.sitecreation.SITE_CREATION_STATE +import org.wordpress.android.ui.sitecreation.SITE_MODEL +import org.wordpress.android.ui.sitecreation.SITE_REMOTE_ID +import org.wordpress.android.ui.sitecreation.SUB_DOMAIN +import org.wordpress.android.ui.sitecreation.SiteCreationResult.Created import org.wordpress.android.ui.sitecreation.SiteCreationState +import org.wordpress.android.ui.sitecreation.URL import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState.SiteCreationCompleted -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState.SiteNotCreated -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.CreateSiteState.SiteNotInLocalDb -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewStartServiceData import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewContentUiState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewFullscreenErrorUiState.SitePreviewConnectionErrorUiState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewFullscreenErrorUiState.SitePreviewGenericErrorUiState -import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewFullscreenProgressUiState import org.wordpress.android.ui.sitecreation.previews.SitePreviewViewModel.SitePreviewUiState.SitePreviewWebErrorUiState +import org.wordpress.android.ui.sitecreation.progress.LOADING_STATE_TEXT_ANIMATION_DELAY import org.wordpress.android.ui.sitecreation.services.FetchWpComSiteUseCase -import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState -import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.CREATE_SITE -import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.FAILURE -import org.wordpress.android.ui.sitecreation.services.SiteCreationServiceState.SiteCreationStep.SUCCESS -import org.wordpress.android.ui.sitecreation.theme.defaultTemplateSlug -import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.util.UrlUtilsWrapper -private const val SUB_DOMAIN = "test" -private const val DOMAIN = ".wordpress.com" -private const val URL = "$SUB_DOMAIN$DOMAIN" -private const val REMOTE_SITE_ID = 1L -private const val LOCAL_SITE_ID = 2 -private val SITE_CREATION_STATE = SiteCreationState(segmentId = 1, siteDesign = defaultTemplateSlug, domain = URL) - @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) class SitePreviewViewModelTest : BaseUnitTest() { - @Mock - private lateinit var dispatcher: Dispatcher - - @Mock - private lateinit var siteStore: SiteStore - - @Mock - private lateinit var bundle: Bundle - - @Mock - private lateinit var fetchWpComUseCase: FetchWpComSiteUseCase - - @Mock - private lateinit var networkUtils: NetworkUtilsWrapper + private var dispatcher = mock() + private var siteStore = mock() + private var fetchWpComSiteUseCase = mock() @Mock private lateinit var urlUtils: UrlUtilsWrapper @@ -81,16 +59,7 @@ class SitePreviewViewModelTest : BaseUnitTest() { private lateinit var uiStateObserver: Observer @Mock - private lateinit var startServiceObserver: Observer - - @Mock - private lateinit var onHelpedClickedObserver: Observer - - @Mock - private lateinit var onCancelWizardClickedObserver: Observer - - @Mock - private lateinit var onOkClickedObserver: Observer + private lateinit var onOkClickedObserver: Observer @Mock private lateinit var preloadPreviewObserver: Observer @@ -102,149 +71,47 @@ class SitePreviewViewModelTest : BaseUnitTest() { viewModel = SitePreviewViewModel( dispatcher, siteStore, - fetchWpComUseCase, - networkUtils, + fetchWpComSiteUseCase, urlUtils, tracker, testDispatcher(), testDispatcher() ) viewModel.uiState.observeForever(uiStateObserver) - viewModel.startCreateSiteService.observeForever(startServiceObserver) - viewModel.onHelpClicked.observeForever(onHelpedClickedObserver) - viewModel.onCancelWizardClicked.observeForever(onCancelWizardClickedObserver) viewModel.onOkButtonClicked.observeForever(onOkClickedObserver) viewModel.preloadPreview.observeForever(preloadPreviewObserver) - whenever(networkUtils.isNetworkAvailable()).thenReturn(true) - whenever(urlUtils.extractSubDomain(URL)).thenReturn(SUB_DOMAIN) - whenever(urlUtils.addUrlSchemeIfNeeded(URL, true)).thenReturn(URL) - whenever(urlUtils.removeScheme(URL)).thenReturn(URL) - whenever(siteStore.getSiteBySiteId(REMOTE_SITE_ID)).thenReturn(createLocalDbSiteModelId()) - } - - private fun testWithSuccessResponse(block: suspend CoroutineScope.() -> T) { - test { - whenever(fetchWpComUseCase.fetchSiteWithRetry(REMOTE_SITE_ID)) - .thenReturn(OnSiteChanged(1)) - block() - } - } - - private fun testWithErrorResponse(block: suspend CoroutineScope.() -> T) { - test { - val onSiteChanged = OnSiteChanged(0) - onSiteChanged.error = SiteError(GENERIC_ERROR) - whenever(fetchWpComUseCase.fetchSiteWithRetry(REMOTE_SITE_ID)) - .thenReturn(onSiteChanged) - block() - } - } - - @Test - fun `progress shown on start`() = test { - initViewModel() - - assertThat(viewModel.uiState.value).isInstanceOf(SitePreviewFullscreenProgressUiState::class.java) - viewModel.onSiteCreationServiceStateUpdated(createServiceSuccessState()) // complete flow - } - - @Test - @Ignore("This test runs indefinitely without completing due to the way it is structured to test the animate field.") - fun `ProgressUiState's animate field is false only for first emitted event`() = test { - initViewModel() - assertThat((viewModel.uiState.value as SitePreviewFullscreenProgressUiState).animate).isFalse() - - (1..100).forEach { - advanceTimeBy(LOADING_STATE_TEXT_ANIMATION_DELAY) - assertThat((viewModel.uiState.value as SitePreviewFullscreenProgressUiState).animate).isTrue() - } - } - - @Test - @Ignore("This test runs indefinitely without completing due to the way it is structured to test the loading state.") - fun `ProgressUiState's text changes every LOADING_STATE_TEXT_ANIMATION_DELAY seconds`() = test { - initViewModel() - viewModel.onSiteCreationServiceStateUpdated(createServiceSuccessState()) // complete flow - - (1..100).forEach { - val lastTextId = (viewModel.uiState.value as SitePreviewFullscreenProgressUiState).loadingTextResId - advanceTimeBy(LOADING_STATE_TEXT_ANIMATION_DELAY) - assertThat((viewModel.uiState.value as SitePreviewFullscreenProgressUiState).loadingTextResId) - .isNotEqualTo(lastTextId) - } - } - @Test - fun `service started on start`() = test { - initViewModel() - - assertThat(viewModel.startCreateSiteService.value).isNotNull - viewModel.onSiteCreationServiceStateUpdated(createServiceSuccessState()) // complete flow - } - - @Test - fun `error shown on start when internet access not available`() = test { - whenever(networkUtils.isNetworkAvailable()).thenReturn(false) - initViewModel() - advanceUntilIdle() // skip delays - assertThat(viewModel.uiState.value).isInstanceOf(SitePreviewConnectionErrorUiState::class.java) - } - - @Test - fun `error shown on service failure`() { - initViewModel() - viewModel.onSiteCreationServiceStateUpdated(createServiceFailureState()) - assertThat(viewModel.uiState.value).isInstanceOf(SitePreviewGenericErrorUiState::class.java) - } - - @Test - fun `displaying error screen cancels the progress animation job`() = test { - initViewModel() - viewModel.onSiteCreationServiceStateUpdated(createServiceFailureState()) - (1..100).forEach { - advanceTimeBy(LOADING_STATE_TEXT_ANIMATION_DELAY) - assertThat(viewModel.uiState.value).isInstanceOf(SitePreviewGenericErrorUiState::class.java) - } + whenever(urlUtils.extractSubDomain(any())).thenReturn(SUB_DOMAIN) + whenever(urlUtils.addUrlSchemeIfNeeded(any(), eq(true))).thenReturn(URL) + whenever(siteStore.getSiteBySiteId(SITE_REMOTE_ID)).thenReturn(SITE_MODEL) } @Test - fun `service started on retry`() { - initViewModel() - viewModel.onSiteCreationServiceStateUpdated(createServiceFailureState()) - viewModel.retry() - assertThat(viewModel.startCreateSiteService.value).isNotNull + fun `on start fetches site by remote id when result is created`() = testWith(FETCH_SUCCESS) { + startViewModel(SITE_CREATION_STATE.copy(result = RESULT_NOT_IN_LOCAL_DB)) + verify(fetchWpComSiteUseCase).fetchSiteWithRetry(SITE_REMOTE_ID) } @Test - fun `on Help click is propagated`() { - viewModel.onHelpClicked() - verify(onHelpedClickedObserver).onChanged(null) - } - - @Test - fun `on WizardCanceled click is propagated`() { - viewModel.onCancelWizardClicked() - assertThat(viewModel.onCancelWizardClicked.value).isEqualTo(SiteNotCreated) - } - - @Test - fun `on OK click is propagated`() { + fun `on start does not show preview when fetching fails`() = testWith(FETCH_ERROR) { + startViewModel() + verify(siteStore, never()).getSiteBySiteId(SITE_REMOTE_ID) viewModel.onOkButtonClicked() - assertThat(viewModel.onOkButtonClicked.value).isEqualTo(SiteNotCreated) + verify(uiStateObserver, never()).onChanged(isA()) } @Test fun `show content on UrlLoaded`() { - initViewModel() + startViewModel() viewModel.onUrlLoaded() assertThat(viewModel.uiState.value).isInstanceOf(SitePreviewContentUiState::class.java) } @Test fun `displaying content cancels the progress animation job`() = test { - initViewModel() + startViewModel() viewModel.onUrlLoaded() - (1..100).forEach { + repeat(100) { advanceTimeBy(LOADING_STATE_TEXT_ANIMATION_DELAY) assertThat(viewModel.uiState.value).isInstanceOf(SitePreviewContentUiState::class.java) } @@ -252,131 +119,35 @@ class SitePreviewViewModelTest : BaseUnitTest() { @Test fun `show webview empty screen on WebViewError`() { - initViewModel() + startViewModel() viewModel.onWebViewError() assertThat(viewModel.uiState.value).isInstanceOf(SitePreviewWebErrorUiState::class.java) } @Test - fun `start pre-loading WebView on service success`() = testWithSuccessResponse { - initViewModel() - viewModel.onSiteCreationServiceStateUpdated(createServiceSuccessState()) + fun `on start preloads the preview when result is Completed`() { + startViewModel(SITE_CREATION_STATE.copy(result = RESULT_COMPLETED)) assertThat(viewModel.preloadPreview.value).isEqualTo(URL) } @Test - fun `fetch newly created SiteModel on service success`() = testWithSuccessResponse { - initViewModel() - viewModel.onSiteCreationServiceStateUpdated(createServiceSuccessState()) - verify(fetchWpComUseCase).fetchSiteWithRetry(REMOTE_SITE_ID) - } - - @Test - fun `CreateSiteState is SiteNotCreated on init`() { - initViewModel() - assertThat(getCreateSiteState()).isEqualTo(SiteNotCreated) - } - - @Test - fun `CreateSiteState is SiteCreationCompleted on fetchFromRemote success`() = testWithSuccessResponse { - initViewModel() - viewModel.onSiteCreationServiceStateUpdated(createServiceSuccessState()) - assertThat(getCreateSiteState()).isEqualTo(SiteCreationCompleted(LOCAL_SITE_ID, false)) - } - - @Test - fun `CreateSiteState is NotInLocalDb on fetchFromRemote failure`() = testWithErrorResponse { - initViewModel() - viewModel.onSiteCreationServiceStateUpdated(createServiceSuccessState()) - assertThat(getCreateSiteState()).isEqualTo(SiteNotInLocalDb(REMOTE_SITE_ID, false)) - } - - @Test - fun `CreateSiteState is saved into bundle`() { - initViewModel(null) - - viewModel.writeToBundle(bundle) - - verify(bundle).putParcelable(eq(KEY_CREATE_SITE_STATE), notNull()) - } - - @Test - fun `show fullscreen progress when restoring from SiteNotCreated state`() = testWithSuccessResponse { - whenever(bundle.getParcelable(KEY_CREATE_SITE_STATE)).thenReturn(SiteNotCreated) - initViewModel(bundle) - - assertThat(viewModel.uiState.value).isInstanceOf(SitePreviewFullscreenProgressUiState::class.java) - viewModel.onSiteCreationServiceStateUpdated(createServiceSuccessState()) // complete flow - } - - @Test - fun `service started when restoring from SiteNotCreated state`() { - whenever(bundle.getParcelable(KEY_CREATE_SITE_STATE)).thenReturn(SiteNotCreated) - initViewModel(bundle) - - assertThat(viewModel.startCreateSiteService.value).isNotNull - } - - @Test - fun `show fullscreen progress when restoring from SiteNotInLocalDb state`() = testWithSuccessResponse { - whenever(bundle.getParcelable(KEY_CREATE_SITE_STATE)).thenReturn(SiteNotCreated) - initViewModel(bundle) - - assertThat(viewModel.uiState.value).isInstanceOf(SitePreviewFullscreenProgressUiState::class.java) - viewModel.onSiteCreationServiceStateUpdated(createServiceSuccessState()) // complete flow - } - - @Test - fun `start pre-loading WebView when restoring from SiteNotInLocalDb state`() = testWithSuccessResponse { - whenever(bundle.getParcelable(KEY_CREATE_SITE_STATE)) - .thenReturn(SiteNotInLocalDb(REMOTE_SITE_ID, false)) - initViewModel(bundle) - - assertThat(viewModel.uiState.value).isInstanceOf(SitePreviewFullscreenProgressUiState::class.java) - } - - @Test - fun `fetch newly created SiteModel when restoring from SiteNotInLocalDb state`() = testWithSuccessResponse { - whenever(bundle.getParcelable(KEY_CREATE_SITE_STATE)) - .thenReturn(SiteNotInLocalDb(REMOTE_SITE_ID, false)) - initViewModel(bundle) - - verify(fetchWpComUseCase).fetchSiteWithRetry(REMOTE_SITE_ID) - } - - @Test - fun `start pre-loading WebView when restoring from SiteCreationCompleted state`() { - whenever(bundle.getParcelable(KEY_CREATE_SITE_STATE)) - .thenReturn(SiteCreationCompleted(LOCAL_SITE_ID, false)) - initViewModel(bundle) - - assertThat(viewModel.preloadPreview.value).isEqualTo(URL) - } - - private fun initViewModel(savedState: Bundle? = null) { - viewModel.start(SITE_CREATION_STATE, savedState) + fun `on ok button click is propagated`() = testWith(FETCH_SUCCESS) { + startViewModel() + viewModel.onOkButtonClicked() + verify(onOkClickedObserver).onChanged(anyOrNull()) } - private fun createServiceFailureState(): SiteCreationServiceState { - val stateBeforeFailure = SiteCreationServiceState(CREATE_SITE) - return SiteCreationServiceState(FAILURE, stateBeforeFailure) - } + // region Helpers - private fun createServiceSuccessState(): SiteCreationServiceState { - return SiteCreationServiceState(SUCCESS, Pair(REMOTE_SITE_ID, URL)) + private fun testWith(response: OnSiteChanged, block: suspend CoroutineScope.() -> Unit) = test { + whenever(fetchWpComSiteUseCase.fetchSiteWithRetry(SITE_REMOTE_ID)).thenReturn(response) + block() } - private fun createLocalDbSiteModelId(): SiteModel { - val localDbSiteModel = SiteModel() - localDbSiteModel.id = LOCAL_SITE_ID - return localDbSiteModel + private fun startViewModel(state: SiteCreationState = SITE_CREATION_STATE.copy(result = RESULT_CREATED)) { + whenever(RESULT_CREATED.site).thenReturn(SITE_MODEL) + viewModel.start(state) } - /** - * `createSiteState` is a private property -> get its value using `onOkButtonClicked` - */ - private fun getCreateSiteState(): CreateSiteState? { - viewModel.onOkButtonClicked() - return viewModel.onOkButtonClicked.value - } + // endregion } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/progress/SiteCreationProgressViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/progress/SiteCreationProgressViewModelTest.kt new file mode 100644 index 000000000000..7b178d89f60b --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/progress/SiteCreationProgressViewModelTest.kt @@ -0,0 +1,269 @@ +package org.wordpress.android.ui.sitecreation.progress + +import androidx.lifecycle.Observer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.atMost +import org.mockito.kotlin.check +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.refEq +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.TransactionsStore.OnShoppingCartCreated +import org.wordpress.android.ui.domains.DomainRegistrationCheckoutWebViewActivity.OpenCheckout.CheckoutDetails +import org.wordpress.android.ui.domains.usecases.CreateCartUseCase +import org.wordpress.android.ui.sitecreation.CART_ERROR +import org.wordpress.android.ui.sitecreation.CART_SUCCESS +import org.wordpress.android.ui.sitecreation.PAID_DOMAIN +import org.wordpress.android.ui.sitecreation.RESULT_IN_CART +import org.wordpress.android.ui.sitecreation.SERVICE_ERROR +import org.wordpress.android.ui.sitecreation.SERVICE_SUCCESS +import org.wordpress.android.ui.sitecreation.SITE_CREATION_STATE +import org.wordpress.android.ui.sitecreation.SITE_REMOTE_ID +import org.wordpress.android.ui.sitecreation.SITE_SLUG +import org.wordpress.android.ui.sitecreation.SiteCreationState +import org.wordpress.android.ui.sitecreation.misc.SiteCreationTracker +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressViewModel.SiteProgressUiState +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressViewModel.SiteProgressUiState.Error.ConnectionError +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressViewModel.SiteProgressUiState.Error.GenericError +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressViewModel.SiteProgressUiState.Loading +import org.wordpress.android.ui.sitecreation.progress.SiteCreationProgressViewModel.StartServiceData +import org.wordpress.android.util.NetworkUtilsWrapper +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@ExperimentalCoroutinesApi +@RunWith(MockitoJUnitRunner::class) +class SiteCreationProgressViewModelTest : BaseUnitTest() { + private var networkUtils = mock() + private var tracker = mock() + private val createCartUseCase = mock() + + private val uiStateObserver = mock>() + private val startServiceObserver = mock>() + private val onHelpClickedObserver = mock>() + private val onCancelWizardClickedObserver = mock>() + private val onRemoteSiteCreatedObserver = mock>() + private val onCartCreatedObserver = mock>() + + private lateinit var viewModel: SiteCreationProgressViewModel + + @Before + fun setUp() { + viewModel = SiteCreationProgressViewModel( + networkUtils, + tracker, + createCartUseCase, + testDispatcher(), + ) + viewModel.uiState.observeForever(uiStateObserver) + viewModel.startCreateSiteService.observeForever(startServiceObserver) + viewModel.onHelpClicked.observeForever(onHelpClickedObserver) + viewModel.onCancelWizardClicked.observeForever(onCancelWizardClickedObserver) + viewModel.onFreeSiteCreated.observeForever(onRemoteSiteCreatedObserver) + viewModel.onCartCreated.observeForever(onCartCreatedObserver) + + whenever(networkUtils.isNetworkAvailable()).thenReturn(true) + } + + @Test + fun `on start shows progress`() = test { + startViewModel() + assertIs(viewModel.uiState.value) + } + + @Test + fun `on start emits service event`() = test { + startViewModel() + assertNotNull(viewModel.startCreateSiteService.value) + } + + @Test + fun `on start emits service event for free domains with isFree true`() = test { + startViewModel(SITE_CREATION_STATE) + val request = assertNotNull(viewModel.startCreateSiteService.value).serviceData + assertTrue(request.isFree) + } + + @Test + fun `on start emits service event for paid domains with isFree false`() = test { + startViewModel(SITE_CREATION_STATE.copy(domain = PAID_DOMAIN)) + val request = assertNotNull(viewModel.startCreateSiteService.value).serviceData + assertFalse(request.isFree) + } + + @Test + fun `on start shows error when network is not available`() = test { + whenever(networkUtils.isNetworkAvailable()).thenReturn(false) + startViewModel() + advanceUntilIdle() + assertIs(viewModel.uiState.value) + } + + @Test + fun `on start shows first loading text without animation`() = test { + startViewModel() + verify(uiStateObserver).onChanged(check { !it.animate }) + } + + @Test + fun `on start changes the loading text with animation after delay`() = test { + startViewModel() + advanceTimeBy(LOADING_STATE_TEXT_ANIMATION_DELAY) + verify(uiStateObserver).onChanged(check { it.animate }) + } + + @Test + fun `on start changes the loading text with animation after delay 4 times`() = test { + startViewModel() + val captor = argumentCaptor() + (1..9).forEach { + verify(uiStateObserver, atMost(it)).onChanged(captor.capture()) + advanceTimeBy(LOADING_STATE_TEXT_ANIMATION_DELAY) + } + assertThat(captor.allValues.distinctBy { it.loadingTextResId }).hasSize(4) + } + + @Test + fun `on retry click emits service event with the previous result`() { + startViewModel() + viewModel.onSiteCreationServiceStateUpdated(SERVICE_ERROR) + viewModel.retry() + assertEquals(viewModel.startCreateSiteService.value?.previousState, SERVICE_ERROR.payload) + } + + @Test + fun `on help click is propagated`() { + startViewModel() + viewModel.onHelpClicked() + verify(onHelpClickedObserver).onChanged(isNull()) + } + + @Test + fun `on cancel wizard click is propagated`() { + startViewModel() + viewModel.onCancelWizardClicked() + verify(onCancelWizardClickedObserver).onChanged(isNull()) + } + + @Test + fun `on service success propagates site`() { + startViewModel() + viewModel.onSiteCreationServiceStateUpdated(SERVICE_SUCCESS) + verify(onRemoteSiteCreatedObserver).onChanged(argThat { + assertEquals(SITE_REMOTE_ID, siteId) + url == SITE_SLUG + }) + } + + @Test + fun `on service success for paid domain creates cart`() = test { + startViewModel(SITE_CREATION_STATE.copy(domain = PAID_DOMAIN)) + viewModel.onSiteCreationServiceStateUpdated(SERVICE_SUCCESS) + verify(createCartUseCase).execute( + any(), + eq(PAID_DOMAIN.productId), + eq(PAID_DOMAIN.domainName), + eq(PAID_DOMAIN.supportsPrivacy), + any(), + ) + } + + @Test + fun `on cart success propagates checkout details`() = testWith(CART_SUCCESS) { + startViewModel(SITE_CREATION_STATE.copy(domain = PAID_DOMAIN)) + viewModel.onSiteCreationServiceStateUpdated(SERVICE_SUCCESS) + verify(onCartCreatedObserver).onChanged(argThat { + assertEquals(SITE_REMOTE_ID, site.siteId) + assertEquals(SITE_SLUG, site.url) + domainName == PAID_DOMAIN.domainName + }) + } + + @Test + fun `on cart failure shows generic error`() = testWith(CART_ERROR) { + startViewModel(SITE_CREATION_STATE.copy(domain = PAID_DOMAIN)) + viewModel.onSiteCreationServiceStateUpdated(SERVICE_SUCCESS) + assertIs(viewModel.uiState.value) + } + + @Test + fun `on service failure shows generic error`() { + startViewModel() + viewModel.onSiteCreationServiceStateUpdated(SERVICE_ERROR) + assertIs(viewModel.uiState.value) + } + + @Test + fun `on restart with same paid domain reuses previous blog to create new cart`() = testWith(CART_SUCCESS) { + val state = SITE_CREATION_STATE.copy(domain = PAID_DOMAIN) + startViewModel(state) + val previous = SiteModel().apply { siteId = 9L;url = "blog.wordpress.com" } + viewModel.onSiteCreationServiceStateUpdated(SERVICE_SUCCESS.copy(payload = previous.siteId to previous.url)) + + startViewModel(state.copy(result = RESULT_IN_CART)) + + verify(startServiceObserver, atMost(1)).onChanged(any()) + verify(createCartUseCase).execute( + refEq(previous), + eq(PAID_DOMAIN.productId), + eq(PAID_DOMAIN.domainName), + eq(PAID_DOMAIN.supportsPrivacy), + any() + ) + } + + @Test + fun `on restart with different paid domain emits service event`() = testWith(CART_SUCCESS) { + startViewModel(SITE_CREATION_STATE.copy(domain = PAID_DOMAIN)) + viewModel.onSiteCreationServiceStateUpdated(SERVICE_SUCCESS) + + startViewModel(SITE_CREATION_STATE.copy(domain = PAID_DOMAIN.copy(domainName = "different"))) + + verify(startServiceObserver, times(2)).onChanged(any()) + } + + @Test + fun `on restart with free domain emits service event`() = testWith(CART_SUCCESS) { + startViewModel(SITE_CREATION_STATE.copy(domain = PAID_DOMAIN)) + viewModel.onSiteCreationServiceStateUpdated(SERVICE_SUCCESS) + clearInvocations(createCartUseCase) + + startViewModel(SITE_CREATION_STATE) + + verify(startServiceObserver, times(2)).onChanged(any()) + verifyNoMoreInteractions(createCartUseCase) + } + + // region Helpers + private fun testWith(response: OnShoppingCartCreated, block: suspend CoroutineScope.() -> Unit) = test { + whenever(createCartUseCase.execute(any(), any(), any(), any(), any())).thenReturn(response) + block() + } + + private fun startViewModel(state: SiteCreationState = SITE_CREATION_STATE) { + viewModel.start(state) + } + + // endregion +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceManagerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceManagerTest.kt index 3b76e62a929a..a70333b10838 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceManagerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/sitecreation/services/SiteCreationServiceManagerTest.kt @@ -34,7 +34,8 @@ private val DUMMY_SITE_DATA: SiteCreationServiceData = SiteCreationServiceData( 123, "slug", "domain", - null + null, + true, ) private val IDLE_STATE = SiteCreationServiceState(IDLE) diff --git a/WordPress/src/test/java/org/wordpress/android/util/SiteUtilsTest.kt b/WordPress/src/test/java/org/wordpress/android/util/SiteUtilsTest.kt index a0ca0e80176f..477f03537081 100644 --- a/WordPress/src/test/java/org/wordpress/android/util/SiteUtilsTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/util/SiteUtilsTest.kt @@ -180,6 +180,7 @@ class SiteUtilsTest { @Test fun `supportsStoriesFeature returns true when origin is wpcom rest`() { + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowStoryPost()).thenReturn(true) val site = SiteModel().apply { origin = SiteModel.ORIGIN_WPCOM_REST setIsWPCom(true) @@ -192,6 +193,7 @@ class SiteUtilsTest { @Test fun `supportsStoriesFeature returns true when Jetpack site meets requirement`() { + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowStoryPost()).thenReturn(true) val site = initJetpackSite().apply { jetpackVersion = SiteUtils.WP_STORIES_JETPACK_VERSION } @@ -218,7 +220,7 @@ class SiteUtilsTest { origin = SiteModel.ORIGIN_WPCOM_REST setIsWPCom(true) } - whenever(jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()).thenReturn(true) + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowStoryPost()).thenReturn(false) val supportsStoriesFeature = SiteUtils.supportsStoriesFeature(site, jetpackFeatureRemovalPhaseHelper) diff --git a/WordPress/src/test/java/org/wordpress/android/util/extensions/IndividualJetpackPluginExtensionsKtTest.kt b/WordPress/src/test/java/org/wordpress/android/util/extensions/IndividualJetpackPluginExtensionsKtTest.kt new file mode 100644 index 000000000000..bd86a449cf47 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/util/extensions/IndividualJetpackPluginExtensionsKtTest.kt @@ -0,0 +1,171 @@ +package org.wordpress.android.util.extensions + +import org.junit.Test +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.persistence.JetpackCPConnectedSiteModel +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@Suppress("MaxLineLength") +class IndividualJetpackPluginExtensionsKtTest { + // region SiteModel extensions + @Test + fun `SiteModel - Should return FALSE if plugins string is null when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse(siteModel(null).isJetpackIndividualPluginConnectedWithoutFullPlugin()) + } + + @Test + fun `SiteModel - Should return FALSE if plugins string is empty when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse(siteModel("").isJetpackIndividualPluginConnectedWithoutFullPlugin()) + } + + @Test + fun `SiteModel - Should return FALSE if plugins string is not valid when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse(siteModel("something").isJetpackIndividualPluginConnectedWithoutFullPlugin()) + } + + @Test + fun `SiteModel - Should return FALSE if plugins list is empty when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse(siteModel("").isJetpackIndividualPluginConnectedWithoutFullPlugin()) + } + + @Test + fun `SiteModel - Should return FALSE if plugins list contains jetpack when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse(siteModel("jetpack-1,jetpack").isJetpackIndividualPluginConnectedWithoutFullPlugin()) + } + + @Test + fun `SiteModel - Should return FALSE if plugins list does not contain at least one element that starts with jetpack- when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse(siteModel("plugin1,plugin2").isJetpackIndividualPluginConnectedWithoutFullPlugin()) + } + + @Test + fun `SiteModel - Should return TRUE if plugins list is not empty, does not contain jetpack and contains at least one element that starts with jetpack- when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertTrue(siteModel("jetpack-1").isJetpackIndividualPluginConnectedWithoutFullPlugin()) + assertTrue(siteModel("jetpack-1,something").isJetpackIndividualPluginConnectedWithoutFullPlugin()) + assertTrue(siteModel("something,jetpack-1").isJetpackIndividualPluginConnectedWithoutFullPlugin()) + } + + @Test + fun `SiteModel - Should return list of plugin names if list contains known individual jetpack plugins, filtering out non-jetpack plugins and unknown jetpack plugins`() { + val model = siteModel( + "jetpack-search," + + "jetpack-backup," + + "jetpack-protect," + + "jetpack-videopress," + + "jetpack-social," + + "jetpack-boost," + + "jetpack-unknown," + + "not-jetpack-plugin" + ) + val expected = listOf( + "Jetpack Search", + "Jetpack VaultPress Backup", + "Jetpack Protect", + "Jetpack VideoPress", + "Jetpack Social", + "Jetpack Boost", + ) + assertEquals(expected, model.activeIndividualJetpackPluginNames()) + } + // endregion + + // region JetpackCPConnectedSiteModel extensions + @Test + fun `JetpackCPConnectedSiteModel - Should return FALSE if plugins string is null when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse( + jetpackCPConnectedSiteModel(null) + .isJetpackIndividualPluginConnectedWithoutFullPlugin() + ) + } + + @Test + fun `JetpackCPConnectedSiteModel - Should return FALSE if plugins string is empty when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse(jetpackCPConnectedSiteModel("").isJetpackIndividualPluginConnectedWithoutFullPlugin()) + } + + @Test + fun `JetpackCPConnectedSiteModel - Should return FALSE if plugins string is not valid when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse( + jetpackCPConnectedSiteModel("something") + .isJetpackIndividualPluginConnectedWithoutFullPlugin() + ) + } + + @Test + fun `JetpackCPConnectedSiteModel - Should return FALSE if plugins list is empty when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse(jetpackCPConnectedSiteModel("").isJetpackIndividualPluginConnectedWithoutFullPlugin()) + } + + @Test + fun `JetpackCPConnectedSiteModel - Should return FALSE if plugins list contains jetpack when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse( + jetpackCPConnectedSiteModel("jetpack-1,jetpack") + .isJetpackIndividualPluginConnectedWithoutFullPlugin() + ) + } + + @Test + fun `JetpackCPConnectedSiteModel - Should return FALSE if plugins list does not contain at least one element that starts with jetpack- when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertFalse( + jetpackCPConnectedSiteModel("plugin1,plugin2") + .isJetpackIndividualPluginConnectedWithoutFullPlugin() + ) + } + + @Test + fun `JetpackCPConnectedSiteModel - Should return TRUE if plugins list is not empty, does not contain jetpack and contains at least one element that starts with jetpack- when isJetpackIndividualPluginConnectedWithoutFullPlugin is called`() { + assertTrue( + jetpackCPConnectedSiteModel("jetpack-1") + .isJetpackIndividualPluginConnectedWithoutFullPlugin() + ) + assertTrue( + jetpackCPConnectedSiteModel("jetpack-1,something") + .isJetpackIndividualPluginConnectedWithoutFullPlugin() + ) + assertTrue( + jetpackCPConnectedSiteModel("something,jetpack-1") + .isJetpackIndividualPluginConnectedWithoutFullPlugin() + ) + } + + @Test + fun `JetpackCPConnectedSiteModel - Should return list of plugin names if list contains known individual jetpack plugins, filtering out non-jetpack plugins and unknown jetpack plugins`() { + val model = jetpackCPConnectedSiteModel( + "jetpack-search," + + "jetpack-backup," + + "jetpack-protect," + + "jetpack-videopress," + + "jetpack-social," + + "jetpack-boost," + + "jetpack-unknown," + + "not-jetpack-plugin" + ) + val expected = listOf( + "Jetpack Search", + "Jetpack VaultPress Backup", + "Jetpack Protect", + "Jetpack VideoPress", + "Jetpack Social", + "Jetpack Boost", + ) + assertEquals(expected, model.activeIndividualJetpackPluginNames()) + } + // endregion + + private fun siteModel(activeJpPlugins: String?) = + SiteModel().apply { + activeJetpackConnectionPlugins = activeJpPlugins + } + + private fun jetpackCPConnectedSiteModel(activeJpPlugins: String?) = + JetpackCPConnectedSiteModel( + remoteSiteId = null, + localSiteId = 0, + url = "url", + name = "name", + description = "description", + activeJetpackConnectionPlugins = activeJpPlugins?.split(",")?.toList() ?: listOf() + ) +} diff --git a/WordPress/src/test/java/org/wordpress/android/util/extensions/SiteModelExtensionsKtTest.kt b/WordPress/src/test/java/org/wordpress/android/util/extensions/SiteModelExtensionsKtTest.kt deleted file mode 100644 index 7e47a5a72635..000000000000 --- a/WordPress/src/test/java/org/wordpress/android/util/extensions/SiteModelExtensionsKtTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.wordpress.android.util.extensions - -import org.junit.Test -import org.wordpress.android.fluxc.model.SiteModel -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class SiteModelExtensionsKtTest { - @Test - fun `Should return FALSE if plugins string is null when isJetpackConnectedWithoutFullPlugin is called`() { - assertFalse(siteModel(null).isJetpackConnectedWithoutFullPlugin()) - } - - @Test - fun `Should return FALSE if plugins string is empty when isJetpackConnectedWithoutFullPlugin is called`() { - assertFalse(siteModel("").isJetpackConnectedWithoutFullPlugin()) - } - - @Test - fun `Should return FALSE if plugins string is not valid when isJetpackConnectedWithoutFullPlugin is called`() { - assertFalse(siteModel("something").isJetpackConnectedWithoutFullPlugin()) - } - - @Test - fun `Should return FALSE if plugins list is empty when isJetpackConnectedWithoutFullPlugin is called`() { - assertFalse(siteModel("").isJetpackConnectedWithoutFullPlugin()) - } - - @Test - fun `Should return FALSE if plugins list contains jetpack when isJetpackConnectedWithoutFullPlugin is called`() { - assertFalse(siteModel("jetpack-1,jetpack").isJetpackConnectedWithoutFullPlugin()) - } - - @Test - @Suppress("MaxLineLength") - fun `Should return FALSE if plugins list does not contain at least one element that starts with jetpack- when isJetpackConnectedWithoutFullPlugin is called`() { - assertFalse(siteModel("plugin1,plugin2").isJetpackConnectedWithoutFullPlugin()) - } - - @Test - @Suppress("MaxLineLength") - fun `Should return TRUE if plugins list is not empty, does not contain jetpack and contains at least one element that starts with jetpack- when isJetpackConnectedWithoutFullPlugin is called`() { - assertTrue(siteModel("jetpack-1").isJetpackConnectedWithoutFullPlugin()) - assertTrue(siteModel("jetpack-1,something").isJetpackConnectedWithoutFullPlugin()) - } - - private fun siteModel(activeJpPlugins: String?) = - SiteModel().apply { - activeJetpackConnectionPlugins = activeJpPlugins - } -} diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/accounts/PostSignupInterstitialViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/accounts/PostSignupInterstitialViewModelTest.kt index f027a3af04b2..09cc72d8ceb2 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/accounts/PostSignupInterstitialViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/accounts/PostSignupInterstitialViewModelTest.kt @@ -2,18 +2,21 @@ package org.wordpress.android.viewmodel.accounts import androidx.lifecycle.Observer import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest import org.wordpress.android.analytics.AnalyticsTracker.Stat.WELCOME_NO_SITES_INTERSTITIAL_ADD_SELF_HOSTED_SITE_TAPPED import org.wordpress.android.analytics.AnalyticsTracker.Stat.WELCOME_NO_SITES_INTERSTITIAL_CREATE_NEW_SITE_TAPPED import org.wordpress.android.analytics.AnalyticsTracker.Stat.WELCOME_NO_SITES_INTERSTITIAL_DISMISSED import org.wordpress.android.analytics.AnalyticsTracker.Stat.WELCOME_NO_SITES_INTERSTITIAL_SHOWN import org.wordpress.android.ui.accounts.UnifiedLoginTracker +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginHelper import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.viewmodel.accounts.PostSignupInterstitialViewModel.NavigationAction @@ -27,13 +30,19 @@ class PostSignupInterstitialViewModelTest : BaseUnitTest() { private val appPrefs: AppPrefsWrapper = mock() private val unifiedLoginTracker: UnifiedLoginTracker = mock() private val analyticsTracker: AnalyticsTrackerWrapper = mock() + private val wpJetpackIndividualPluginHelper: WPJetpackIndividualPluginHelper = mock() private val observer: Observer = mock() private lateinit var viewModel: PostSignupInterstitialViewModel @Before fun setUp() { - viewModel = PostSignupInterstitialViewModel(appPrefs, unifiedLoginTracker, analyticsTracker) + viewModel = PostSignupInterstitialViewModel( + appPrefs, + unifiedLoginTracker, + analyticsTracker, + wpJetpackIndividualPluginHelper + ) viewModel.navigationAction.observeForever(observer) } @@ -45,6 +54,27 @@ class PostSignupInterstitialViewModelTest : BaseUnitTest() { verify(appPrefs).shouldShowPostSignupInterstitial = false } + @Test + fun `given overlay should show when interstitial is shown then show jetpack individual plugin overlay`() = test { + whenever(wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay()).thenReturn(true) + + viewModel.onInterstitialShown() + advanceUntilIdle() + + assertThat(viewModel.navigationAction.value).isEqualTo(NavigationAction.SHOW_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY) + } + + @Test + fun `given overlay should not show when interstitial is shown then don't show jetpack individual plugin overlay`() = + test { + whenever(wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay()).thenReturn(false) + + viewModel.onInterstitialShown() + advanceUntilIdle() + + assertThat(viewModel.navigationAction.value).isNull() + } + @Test fun `when create new site button is pressed should start site creation flow`() { viewModel.onCreateNewSiteButtonPressed() diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModelTest.kt index caaa6135a49e..c412e26ecddb 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModelTest.kt @@ -2,6 +2,7 @@ package org.wordpress.android.viewmodel.activitylog import android.text.SpannableString import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -21,7 +22,10 @@ import org.wordpress.android.BaseUnitTest import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.activity.ActivityLogModel +import org.wordpress.android.fluxc.model.dashboard.CardModel +import org.wordpress.android.fluxc.network.rest.wpcom.dashboard.CardsUtils import org.wordpress.android.fluxc.store.ActivityLogStore +import org.wordpress.android.fluxc.store.dashboard.CardsStore import org.wordpress.android.fluxc.tools.FormattableContent import org.wordpress.android.fluxc.tools.FormattableRange import org.wordpress.android.ui.activitylog.detail.ActivityLogDetailModel @@ -31,6 +35,59 @@ import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.ResourceProvider import java.util.Date +/* ACTIVITY */ +const val ACTIVITY_ID = "activity123" +const val ACTIVITY_SUMMARY = "activity" +const val ACTIVITY_NAME = "name" +const val ACTIVITY_TYPE = "create a blog" +const val ACTIVITY_IS_REWINDABLE = false +const val ACTIVITY_REWIND_ID = "10.0" +const val ACTIVITY_GRID_ICON = "gridicon.jpg" +const val ACTIVITY_STATUS = "OK" +const val ACTIVITY_ACTOR_TYPE = "author" +const val ACTIVITY_ACTOR_NAME = "John Smith" +const val ACTIVITY_ACTOR_WPCOM_USER_ID = 15L +const val ACTIVITY_ACTOR_ROLE = "admin" +const val ACTIVITY_ACTOR_ICON_URL = "dog.jpg" +const val ACTIVITY_PUBLISHED_DATE = "2021-12-27 11:33:55" +const val ACTIVITY_CONTENT = "content" + +private val ACTIVITY_LOG_MODEL = ActivityLogModel( + summary = ACTIVITY_SUMMARY, + content = FormattableContent(text = ACTIVITY_CONTENT), + name = ACTIVITY_NAME, + actor = ActivityLogModel.ActivityActor( + displayName = ACTIVITY_ACTOR_NAME, + type = ACTIVITY_ACTOR_TYPE, + wpcomUserID = ACTIVITY_ACTOR_WPCOM_USER_ID, + avatarURL = ACTIVITY_ACTOR_ICON_URL, + role = ACTIVITY_ACTOR_ROLE, + ), + type = ACTIVITY_TYPE, + published = CardsUtils.fromDate(ACTIVITY_PUBLISHED_DATE), + rewindable = ACTIVITY_IS_REWINDABLE, + rewindID = ACTIVITY_REWIND_ID, + gridicon = ACTIVITY_GRID_ICON, + status = ACTIVITY_STATUS, + activityID = ACTIVITY_ID +) + +private val ACTIVITY_CARD_MODEL = CardModel.ActivityCardModel( + activities = listOf(ACTIVITY_LOG_MODEL) +) + +private val CARDS_MODEL: List = listOf( + ACTIVITY_CARD_MODEL +) + + +private val data = CardsStore.CardsResult( + model = CARDS_MODEL +) + +private val emptyData = CardsStore.CardsResult( + model = emptyList() +) @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) class ActivityLogDetailViewModelTest : BaseUnitTest() { @@ -48,6 +105,10 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { @Mock private lateinit var site: SiteModel + + @Mock + private lateinit var cardsStore: CardsStore + private lateinit var viewModel: ActivityLogDetailViewModel private val areButtonsVisible = true @@ -92,7 +153,10 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { dispatcher, activityLogStore, resourceProvider, - htmlMessageUtils + htmlMessageUtils, + cardsStore, + testDispatcher(), + testDispatcher() ) viewModel.activityLogItem.observeForever { lastEmittedItem = it } viewModel.restoreVisible.observeForever { restoreVisible = it } @@ -117,7 +181,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { val areButtonsVisible = false val isRestoreHidden = false - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel(areButtonsVisible = areButtonsVisible, isRestoreHidden = isRestoreHidden) assertEquals(false, restoreVisible) } @@ -127,7 +191,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { val areButtonsVisible = false val isRestoreHidden = true - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel(areButtonsVisible = areButtonsVisible, isRestoreHidden = isRestoreHidden) assertEquals(false, restoreVisible) } @@ -137,7 +201,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { val areButtonsVisible = true val isRestoreHidden = false - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel(areButtonsVisible = areButtonsVisible, isRestoreHidden = isRestoreHidden) assertEquals(true, restoreVisible) } @@ -147,7 +211,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { val areButtonsVisible = true val isRestoreHidden = true - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel(areButtonsVisible = areButtonsVisible, isRestoreHidden = isRestoreHidden) assertEquals(false, restoreVisible) } @@ -157,7 +221,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { val areButtonsVisible = false val isRestoreHidden = false - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel(areButtonsVisible = areButtonsVisible, isRestoreHidden = isRestoreHidden) assertEquals(false, downloadBackupVisible) } @@ -167,7 +231,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { val areButtonsVisible = true val isRestoreHidden = false - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel(areButtonsVisible = areButtonsVisible, isRestoreHidden = isRestoreHidden) assertEquals(true, downloadBackupVisible) } @@ -177,7 +241,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { val areButtonsVisible = true val isRestoreHidden = false - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel(areButtonsVisible = areButtonsVisible, isRestoreHidden = isRestoreHidden) assertFalse(multisiteVisible.first) assertNull(multisiteVisible.second) @@ -188,7 +252,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { val areButtonsVisible = true val isRestoreHidden = true - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel(areButtonsVisible = areButtonsVisible, isRestoreHidden = isRestoreHidden) assertTrue(multisiteVisible.first) assertNotNull(multisiteVisible.second) @@ -198,7 +262,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { fun emitsUIModelOnStart() { whenever(activityLogStore.getActivityLogForSite(site)).thenReturn(listOf(activityLogModel)) - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel() assertNotNull(lastEmittedItem) lastEmittedItem?.let { @@ -217,7 +281,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { ) whenever(activityLogStore.getActivityLogForSite(site)).thenReturn(listOf(updatedActivity)) - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel() assertNotNull(lastEmittedItem) lastEmittedItem?.let { @@ -237,7 +301,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { ) whenever(activityLogStore.getActivityLogForSite(site)).thenReturn(listOf(updatedActivity)) - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel() assertNotNull(lastEmittedItem) lastEmittedItem?.let { @@ -250,11 +314,11 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { fun doesNotReemitUIModelOnStartWithTheSameActivityID() { whenever(activityLogStore.getActivityLogForSite(site)).thenReturn(listOf(activityLogModel)) - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel() lastEmittedItem = null - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel() assertNull(lastEmittedItem) } @@ -267,11 +331,11 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { val secondActivity = activityLogModel.copy(activityID = activityID2, content = updatedContent) whenever(activityLogStore.getActivityLogForSite(site)).thenReturn(listOf(activityLogModel, secondActivity)) - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel() lastEmittedItem = null - viewModel.start(site, activityID2, areButtonsVisible, isRestoreHidden) + startViewModel(activityID = activityID2) assertNotNull(lastEmittedItem) lastEmittedItem?.let { @@ -286,7 +350,7 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { lastEmittedItem = mock() - viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden) + startViewModel() assertNull(lastEmittedItem) } @@ -343,4 +407,67 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { assertEquals(model, (it as ActivityLogDetailNavigationEvents.ShowBackupDownload).model) } } + + @Test + fun `given card entry, when no results exist, then emits null`() { + whenever(activityLogStore.getActivityLogForSite(site)).thenReturn(listOf()) + whenever(cardsStore.getCards(any(), any())).thenReturn(flowOf(emptyData)) + + lastEmittedItem = mock() + + startViewModel(isDashboardCardEntry = true) + + assertNull(lastEmittedItem) + } + + @Test + fun `given card entry, when activityLog is empty, then emits value from dashboard table`() { + whenever(activityLogStore.getActivityLogForSite(site)).thenReturn(listOf()) + whenever(cardsStore.getCards(any(), any())).thenReturn(flowOf(data)) + + lastEmittedItem = null + + startViewModel(activityID = ACTIVITY_ID, isDashboardCardEntry = true) + + assertNotNull(lastEmittedItem) + } + + @Test + fun `given card entry, when activityId is not found in activity log, then emits value from dashboard table`() { + whenever(cardsStore.getCards(any(), any())).thenReturn(flowOf(data)) + + lastEmittedItem = null + + startViewModel(activityID = ACTIVITY_ID, isDashboardCardEntry = true) + + assertNotNull(lastEmittedItem) + } + + @Test + fun `given card entry, when activityId is found in activity log, then emits value from activity log`() { + val changedText = "new text" + val updatedContent = FormattableContent(text = changedText) + val secondActivity = activityLogModel.copy(activityID = ACTIVITY_ID, content = updatedContent) + whenever(activityLogStore.getActivityLogForSite(site)).thenReturn(listOf(activityLogModel, secondActivity)) + + lastEmittedItem = null + + startViewModel(activityID = ACTIVITY_ID, isDashboardCardEntry = true) + + assertNotNull(lastEmittedItem) + lastEmittedItem?.let { + assertEquals(it.activityID, ACTIVITY_ID) + assertEquals(it.content, updatedContent) + } + } + + private fun startViewModel( + site: SiteModel = this.site, + activityID: String = this.activityID, + areButtonsVisible: Boolean = this.areButtonsVisible, + isRestoreHidden: Boolean = this.isRestoreHidden, + isDashboardCardEntry: Boolean = false + ) { + viewModel.start(site, activityID, areButtonsVisible, isRestoreHidden, isDashboardCardEntry) + } } diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/SitePickerViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/SitePickerViewModelTest.kt index 61c0b1f809ef..b3d3088f2e6a 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/SitePickerViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/SitePickerViewModelTest.kt @@ -7,13 +7,16 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest +import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginHelper import org.wordpress.android.ui.main.SitePickerAdapter.SiteRecord import org.wordpress.android.viewmodel.Event import org.wordpress.android.viewmodel.main.SitePickerViewModel.Action import org.wordpress.android.viewmodel.main.SitePickerViewModel.Action.AskForSiteSelection import org.wordpress.android.viewmodel.main.SitePickerViewModel.Action.ContinueReblogTo import org.wordpress.android.viewmodel.main.SitePickerViewModel.Action.NavigateToState +import org.wordpress.android.viewmodel.main.SitePickerViewModel.Action.ShowJetpackIndividualPluginOverlay import org.wordpress.android.viewmodel.main.SitePickerViewModel.NavigateState.TO_NO_SITE_SELECTED import org.wordpress.android.viewmodel.main.SitePickerViewModel.NavigateState.TO_SITE_SELECTED @@ -25,9 +28,12 @@ class SitePickerViewModelTest : BaseUnitTest() { @Mock private lateinit var siteRecord: SiteRecord + @Mock + private lateinit var wpJetpackIndividualPluginHelper: WPJetpackIndividualPluginHelper + @Before fun setUp() { - viewModel = SitePickerViewModel() + viewModel = SitePickerViewModel(wpJetpackIndividualPluginHelper) } @Test @@ -87,4 +93,26 @@ class SitePickerViewModelTest : BaseUnitTest() { viewModel.onRefreshReblogActionMode() assertThat(result!!.peekContent()).isEqualTo(NavigateToState(TO_SITE_SELECTED, siteRecord)) } + + @Test + fun `when onSiteListLoaded is invoked then show jetpack individual plugin overlay`() = + test { + whenever(wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay()).thenReturn(true) + + viewModel.onSiteListLoaded() + advanceUntilIdle() + + assertThat(viewModel.onActionTriggered.value?.peekContent()).isEqualTo(ShowJetpackIndividualPluginOverlay) + } + + @Test + fun `when onSiteListLoaded is invoked then don't show jetpack individual plugin overlay`() = + test { + whenever(wpJetpackIndividualPluginHelper.shouldShowJetpackIndividualPluginOverlay()).thenReturn(false) + + viewModel.onSiteListLoaded() + advanceUntilIdle() + + assertThat(viewModel.onActionTriggered.value?.peekContent()).isNull() + } } diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt index 044f44396544..e5968b3e901b 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/main/WPMainActivityViewModelTest.kt @@ -59,6 +59,7 @@ import org.wordpress.android.ui.whatsnew.FeatureAnnouncementItem import org.wordpress.android.ui.whatsnew.FeatureAnnouncementProvider import org.wordpress.android.util.BuildConfigWrapper import org.wordpress.android.util.NoDelayCoroutineDispatcher +import org.wordpress.android.util.SiteUtilsWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.viewmodel.main.WPMainActivityViewModel.FocusPointInfo import java.util.Date @@ -121,6 +122,9 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Mock private lateinit var blazeStore: BlazeStore + @Mock + private lateinit var siteUtilsWrapper: SiteUtilsWrapper + private val featureAnnouncement = FeatureAnnouncement( "14.7", 2, @@ -169,7 +173,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { whenever(quickStartRepository.activeTask).thenReturn(activeTask) whenever(bloggingPromptsSettingsHelper.shouldShowPromptsFeature()).thenReturn(false) whenever(bloggingPromptsStore.getPromptForDate(any(), any())).thenReturn(flowOf(bloggingPrompt)) - whenever(jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()).thenReturn(false) + whenever(siteUtilsWrapper.supportsStoriesFeature(any(), any())).thenReturn(true) viewModel = WPMainActivityViewModel( featureAnnouncementProvider, buildConfigWrapper, @@ -184,7 +188,8 @@ class WPMainActivityViewModelTest : BaseUnitTest() { NoDelayCoroutineDispatcher(), jetpackFeatureRemovalPhaseHelper, blazeFeatureUtils, - blazeStore + blazeStore, + siteUtilsWrapper ) viewModel.onFeatureAnnouncementRequested.observeForever( onFeatureAnnouncementRequestedObserver @@ -537,6 +542,7 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Test fun `new post action is triggered from FAB when no full access to content if stories unavailable`() { startViewModelWithDefaultParameters() + whenever(siteUtilsWrapper.supportsStoriesFeature(any(), any())).thenReturn(false) viewModel.onFabClicked(site = initSite(hasFullAccessToContent = false, isWpcomOrJpSite = false)) assertThat(viewModel.isBottomSheetShowing.value).isNull() assertThat(viewModel.createAction.value).isEqualTo(CREATE_NEW_POST) @@ -604,7 +610,9 @@ class WPMainActivityViewModelTest : BaseUnitTest() { @Test fun `onResume set expected content message when user has not full access to content`() { startViewModelWithDefaultParameters() + whenever(siteUtilsWrapper.supportsStoriesFeature(any(), any())).thenReturn(true) viewModel.onResume(site = initSite(hasFullAccessToContent = false), isOnMySitePageWithValidSite = true) + assertThat(fabUiState!!.CreateContentMessageId) .isEqualTo(R.string.create_post_page_fab_tooltip_contributors_stories_enabled) } diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListCreateMenuViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListCreateMenuViewModelTest.kt index cd765375826c..e53509395f32 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListCreateMenuViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListCreateMenuViewModelTest.kt @@ -5,6 +5,7 @@ import org.assertj.core.api.Assertions import org.junit.Before import org.junit.Test import org.mockito.Mock +import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -18,6 +19,7 @@ import org.wordpress.android.ui.main.MainActionListItem.ActionType.CREATE_NEW_ST import org.wordpress.android.ui.main.MainActionListItem.ActionType.NO_ACTION import org.wordpress.android.ui.main.MainActionListItem.CreateAction import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.util.SiteUtilsWrapper import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper @ExperimentalCoroutinesApi @@ -36,13 +38,16 @@ class PostListCreateMenuViewModelTest : BaseUnitTest() { @Mock private lateinit var jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper + @Mock lateinit var siteUtilsWrapper: SiteUtilsWrapper + @Before fun setUp() { - whenever(jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()).thenReturn(false) + whenever(siteUtilsWrapper.supportsStoriesFeature(any(), any())).thenReturn(true) viewModel = PostListCreateMenuViewModel( appPrefsWrapper, analyticsTrackerWrapper, - jetpackFeatureRemovalPhaseHelper + jetpackFeatureRemovalPhaseHelper, + siteUtilsWrapper ) } @@ -132,8 +137,6 @@ class PostListCreateMenuViewModelTest : BaseUnitTest() { @Test fun `start set expected content message`() { - whenever(site.isWPCom).thenReturn(true) - viewModel.start(site, false) Assertions.assertThat(viewModel.fabUiState.value!!.CreateContentMessageId) .isEqualTo(R.string.create_post_story_fab_tooltip) diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelperTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelperTest.kt index 25c82ad40260..d45db6f59cab 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelperTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelperTest.kt @@ -243,6 +243,7 @@ class PostListItemUiStateHelperTest { @Test fun `verify published post actions`() { + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowPublishedPostStatsButton()).thenReturn(true) val state = createPostListItemUiState( post = createPostModel(status = POST_STATE_PUBLISH) ) @@ -282,8 +283,8 @@ class PostListItemUiStateHelperTest { } @Test - fun `given published post with stats access when jetpack removal phase then stats is not in menu`() { - whenever(jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()).thenReturn(true) + fun `given published post with stats access when jetpack removal phase then stats is in menu`() { + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowPublishedPostStatsButton()).thenReturn(true) val state = createPostListItemUiState( post = createPostModel(status = POST_STATE_PUBLISH) ) @@ -294,16 +295,17 @@ class PostListItemUiStateHelperTest { assertThat(state.actions).hasSize(3) val moreActions = (state.actions[2] as MoreItem).actions - assertThat(moreActions[0].buttonType).isEqualTo(PostListButtonType.BUTTON_COPY) - assertThat(moreActions[1].buttonType).isEqualTo(PostListButtonType.BUTTON_MOVE_TO_DRAFT) - assertThat(moreActions[2].buttonType).isEqualTo(PostListButtonType.BUTTON_COPY_URL) - assertThat(moreActions[3].buttonType).isEqualTo(PostListButtonType.BUTTON_TRASH) - assertThat(moreActions).hasSize(4) + assertThat(moreActions[0].buttonType).isEqualTo(PostListButtonType.BUTTON_STATS) + assertThat(moreActions[1].buttonType).isEqualTo(PostListButtonType.BUTTON_COPY) + assertThat(moreActions[2].buttonType).isEqualTo(PostListButtonType.BUTTON_MOVE_TO_DRAFT) + assertThat(moreActions[3].buttonType).isEqualTo(PostListButtonType.BUTTON_COPY_URL) + assertThat(moreActions[4].buttonType).isEqualTo(PostListButtonType.BUTTON_TRASH) + assertThat(moreActions).hasSize(5) } @Test - fun `given published post with stats access when not jetpack removal phase then stats is in menu`() { - whenever(jetpackFeatureRemovalPhaseHelper.shouldRemoveJetpackFeatures()).thenReturn(true) + fun `given published post with stats access when not jetpack removal phase then stats is not in menu`() { + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowPublishedPostStatsButton()).thenReturn(false) val state = createPostListItemUiState( post = createPostModel(status = POST_STATE_PUBLISH) ) diff --git a/WordPress/src/wordpress/AndroidManifest.xml b/WordPress/src/wordpress/AndroidManifest.xml index 431ff13a90d1..8239f1bf75fa 100644 --- a/WordPress/src/wordpress/AndroidManifest.xml +++ b/WordPress/src/wordpress/AndroidManifest.xml @@ -1,5 +1,6 @@ - + + android:exported="true" + tools:ignore="ExportedContentProvider" /> Build.VERSION_CODES.N) { + get() = when (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { true -> FontFamily(Font(font.eb_garamond)) false -> Serif } diff --git a/WordPress/src/wordpress/res/drawable/img_quick_start_tour_illustration.xml b/WordPress/src/wordpress/res/drawable/img_quick_start_tour_illustration.xml index 5dc717cd1ea5..eeb8f6f0005a 100644 --- a/WordPress/src/wordpress/res/drawable/img_quick_start_tour_illustration.xml +++ b/WordPress/src/wordpress/res/drawable/img_quick_start_tour_illustration.xml @@ -1,5 +1,6 @@ - + android:strokeColor="#00000000" + tools:ignore="VectorPath"/> - + android:strokeColor="#00000000" + tools:ignore="VectorPath"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/config/lint/baseline.xml b/config/lint/baseline.xml new file mode 100644 index 000000000000..cc23272312a3 --- /dev/null +++ b/config/lint/baseline.xml @@ -0,0 +1,4591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/lint/lint.xml b/config/lint/lint.xml new file mode 100644 index 000000000000..edf90c499e35 --- /dev/null +++ b/config/lint/lint.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fastlane/jetpack_metadata/android/ar/changelogs/1333.txt b/fastlane/jetpack_metadata/android/ar/changelogs/1333.txt new file mode 100644 index 000000000000..e136650adbf3 --- /dev/null +++ b/fastlane/jetpack_metadata/android/ar/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +لا توجد تحديثات في هذا الأسبوع. نكون بصدد الهدوء في الوقت الحالي. diff --git a/fastlane/jetpack_metadata/android/de-DE/changelogs/1333.txt b/fastlane/jetpack_metadata/android/de-DE/changelogs/1333.txt new file mode 100644 index 000000000000..82ff83f42381 --- /dev/null +++ b/fastlane/jetpack_metadata/android/de-DE/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Diese Woche gibt es keine Updates. In der Zwischenzeit kühlen wir einfach unsere Jets weiter ab. diff --git a/fastlane/jetpack_metadata/android/en-US/changelogs/1317.txt b/fastlane/jetpack_metadata/android/en-US/changelogs/1317.txt deleted file mode 100644 index 8d0c8ea27e2e..000000000000 --- a/fastlane/jetpack_metadata/android/en-US/changelogs/1317.txt +++ /dev/null @@ -1 +0,0 @@ -We’ve added a little extra support for sites with individual plugins. These plugins don’t support all app features yet, but you can always install the full Jetpack plugin instead. Ready to launch? diff --git a/fastlane/jetpack_metadata/android/en-US/changelogs/1333.txt b/fastlane/jetpack_metadata/android/en-US/changelogs/1333.txt new file mode 100644 index 000000000000..e0ec2ba9142a --- /dev/null +++ b/fastlane/jetpack_metadata/android/en-US/changelogs/1333.txt @@ -0,0 +1 @@ +No updates this week. In the meantime, we're just over here cooling our jets. diff --git a/fastlane/jetpack_metadata/android/es-ES/changelogs/1333.txt b/fastlane/jetpack_metadata/android/es-ES/changelogs/1333.txt new file mode 100644 index 000000000000..6531495253c4 --- /dev/null +++ b/fastlane/jetpack_metadata/android/es-ES/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +No hay actualizaciones esta semana. Mientras tanto, aquí estamos, relajándonos. diff --git a/fastlane/jetpack_metadata/android/fr-FR/changelogs/1333.txt b/fastlane/jetpack_metadata/android/fr-FR/changelogs/1333.txt new file mode 100644 index 000000000000..0345a3a75b20 --- /dev/null +++ b/fastlane/jetpack_metadata/android/fr-FR/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1 : +Pas de mise à jour cette semaine. Nous en profitons pour recharger nos batteries. diff --git a/fastlane/jetpack_metadata/android/id/changelogs/1333.txt b/fastlane/jetpack_metadata/android/id/changelogs/1333.txt new file mode 100644 index 000000000000..10d440c9deb5 --- /dev/null +++ b/fastlane/jetpack_metadata/android/id/changelogs/1333.txt @@ -0,0 +1,2 @@ +22,1: +Tidak ada pembaruan minggu ini. Sementara itu, kami sedang berusaha mendinginkan jet kami. diff --git a/fastlane/jetpack_metadata/android/it-IT/changelogs/1333.txt b/fastlane/jetpack_metadata/android/it-IT/changelogs/1333.txt new file mode 100644 index 000000000000..820d0dace366 --- /dev/null +++ b/fastlane/jetpack_metadata/android/it-IT/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Nessun aggiornamento questa settimana. Nel frattempo, siamo qui a raffreddare i nostri jet. diff --git a/fastlane/jetpack_metadata/android/iw-IL/changelogs/1333.txt b/fastlane/jetpack_metadata/android/iw-IL/changelogs/1333.txt new file mode 100644 index 000000000000..fd506bbbec2d --- /dev/null +++ b/fastlane/jetpack_metadata/android/iw-IL/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +אין עדכונים השבוע. בינתיים, אנחנו נותנים למנוע להתקרר קצת. diff --git a/fastlane/jetpack_metadata/android/ja-JP/changelogs/1333.txt b/fastlane/jetpack_metadata/android/ja-JP/changelogs/1333.txt new file mode 100644 index 000000000000..168848791061 --- /dev/null +++ b/fastlane/jetpack_metadata/android/ja-JP/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +今週は更新がありません。 動きがあり次第お知らせします。 diff --git a/fastlane/jetpack_metadata/android/ko-KR/changelogs/1333.txt b/fastlane/jetpack_metadata/android/ko-KR/changelogs/1333.txt new file mode 100644 index 000000000000..2bc76a2ae550 --- /dev/null +++ b/fastlane/jetpack_metadata/android/ko-KR/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +이번 주에는 업데이트가 없습니다. 잠시 쉬었다 가겠습니다. diff --git a/fastlane/jetpack_metadata/android/nl-NL/changelogs/1333.txt b/fastlane/jetpack_metadata/android/nl-NL/changelogs/1333.txt new file mode 100644 index 000000000000..8f2f6d7d289e --- /dev/null +++ b/fastlane/jetpack_metadata/android/nl-NL/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Geen update voor deze week. In de tussentijd laten wij even onze jets afkoelen. diff --git a/fastlane/jetpack_metadata/android/pt-BR/changelogs/1333.txt b/fastlane/jetpack_metadata/android/pt-BR/changelogs/1333.txt new file mode 100644 index 000000000000..928e6fdd6e89 --- /dev/null +++ b/fastlane/jetpack_metadata/android/pt-BR/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Nenhuma atualização essa semana. Enquanto isso, estamos aproveitando para relaxar e se despreocupar. diff --git a/fastlane/jetpack_metadata/android/ru-RU/changelogs/1333.txt b/fastlane/jetpack_metadata/android/ru-RU/changelogs/1333.txt new file mode 100644 index 000000000000..ec7768da62d0 --- /dev/null +++ b/fastlane/jetpack_metadata/android/ru-RU/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +На этой неделе нет обновлений. Готовим мотор к разбегу. diff --git a/fastlane/jetpack_metadata/android/sv-SE/changelogs/1333.txt b/fastlane/jetpack_metadata/android/sv-SE/changelogs/1333.txt new file mode 100644 index 000000000000..36c0c989bb10 --- /dev/null +++ b/fastlane/jetpack_metadata/android/sv-SE/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Inga uppdateringar den här veckan. Vi tar tillfället i akt att ta det lugnt. diff --git a/fastlane/jetpack_metadata/android/tr-TR/changelogs/1333.txt b/fastlane/jetpack_metadata/android/tr-TR/changelogs/1333.txt new file mode 100644 index 000000000000..ea850dfc04b4 --- /dev/null +++ b/fastlane/jetpack_metadata/android/tr-TR/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Bu hafta bir güncelleme bulunmuyor. Bu arada biz de burada dinleniyoruz. diff --git a/fastlane/jetpack_metadata/android/zh-CN/changelogs/1333.txt b/fastlane/jetpack_metadata/android/zh-CN/changelogs/1333.txt new file mode 100644 index 000000000000..26a1c69f81d1 --- /dev/null +++ b/fastlane/jetpack_metadata/android/zh-CN/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +本周无更新。 在此期间,我们只是保持冷静和放松。 diff --git a/fastlane/jetpack_metadata/android/zh-TW/changelogs/1333.txt b/fastlane/jetpack_metadata/android/zh-TW/changelogs/1333.txt new file mode 100644 index 000000000000..15ce43567587 --- /dev/null +++ b/fastlane/jetpack_metadata/android/zh-TW/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +本週沒有更新。 我們正在為下次突破做好準備。 diff --git a/fastlane/metadata/android/ar/changelogs/1333.txt b/fastlane/metadata/android/ar/changelogs/1333.txt new file mode 100644 index 000000000000..293d47a64d52 --- /dev/null +++ b/fastlane/metadata/android/ar/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +عند تسجيل الدخول إلى موقع مستضاف ذاتيًا يتصل بـ Jetpack من خلال الإضافات الفردية، سترى نافذة منبثقة تفيد بأن هذا النوع من الاتصال لا يدعم الميزات الأساسية في التطبيق حتى الآن. يمكنك حل تلك المشكلة عن طريق التبديل إلى تطبيق Jetpack. أعلى وأعلى وبعيد. diff --git a/fastlane/metadata/android/de-DE/changelogs/1333.txt b/fastlane/metadata/android/de-DE/changelogs/1333.txt new file mode 100644 index 000000000000..5a52d557a298 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Wenn du dich bei einer selbst gehosteten Website anmeldest, die eine Verbindung mit Jetpack über einzelne Plugins herstellt, wird dir ein Pop-up-Fenster mit einem Hinweis angezeigt, dass diese Verbindungsart die Kernfunktionen der App noch nicht unterstützt. Du kannst dieses Problem umgehen, indem du zur Jetpack-App wechselst. Auf, auf und davon. diff --git a/fastlane/metadata/android/en-US/changelogs/1317.txt b/fastlane/metadata/android/en-US/changelogs/1317.txt deleted file mode 100644 index cc844c82bf38..000000000000 --- a/fastlane/metadata/android/en-US/changelogs/1317.txt +++ /dev/null @@ -1 +0,0 @@ -Say hello to our shiny new in-app landing screen! It almost makes you want to look at it all day and not publish anything new. Almost. diff --git a/fastlane/metadata/android/en-US/changelogs/1333.txt b/fastlane/metadata/android/en-US/changelogs/1333.txt new file mode 100644 index 000000000000..3dcbd049e215 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/1333.txt @@ -0,0 +1 @@ +When you log in to a self-hosted site that connects to Jetpack through individual plugins, you’ll see a pop-up stating that this type of connection doesn’t support the app’s core features yet. You can get around that problem by switching over to the Jetpack app. Up, up, and away. diff --git a/fastlane/metadata/android/es-ES/changelogs/1333.txt b/fastlane/metadata/android/es-ES/changelogs/1333.txt new file mode 100644 index 000000000000..4e6b5ef9e7b2 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Cuando accedes a un sitio autoalojado que se conecta a Jetpackmediante plugins individuales, verás una ventana emergente que te avisa de que este tipo de conexión aun no es compatible con las funcionalidades base de la aplicación. Puedes evitar ese problema pasándote a la aplicación de Jetpack. Hasta el infinito y más allá. diff --git a/fastlane/metadata/android/fr-CA/changelogs/1333.txt b/fastlane/metadata/android/fr-CA/changelogs/1333.txt new file mode 100644 index 000000000000..3184726b8f52 --- /dev/null +++ b/fastlane/metadata/android/fr-CA/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1 : +Si vous vous connectez à un site auto-hébergé qui se connecte à Jetpack via des extensions individuelles, une fenêtre contextuelle s’affiche pour vous indiquer que ce type de connexion ne prend pas encore en charge les fonctionnalités principales de l’application. Vous pouvez contourner ce problème en basculant vers l’application Jetpack. Toujours plus haut, toujours plus loin. diff --git a/fastlane/metadata/android/fr-FR/changelogs/1333.txt b/fastlane/metadata/android/fr-FR/changelogs/1333.txt new file mode 100644 index 000000000000..3184726b8f52 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1 : +Si vous vous connectez à un site auto-hébergé qui se connecte à Jetpack via des extensions individuelles, une fenêtre contextuelle s’affiche pour vous indiquer que ce type de connexion ne prend pas encore en charge les fonctionnalités principales de l’application. Vous pouvez contourner ce problème en basculant vers l’application Jetpack. Toujours plus haut, toujours plus loin. diff --git a/fastlane/metadata/android/id/changelogs/1317.txt b/fastlane/metadata/android/id/changelogs/1317.txt deleted file mode 100644 index 211cc7163852..000000000000 --- a/fastlane/metadata/android/id/changelogs/1317.txt +++ /dev/null @@ -1,2 +0,0 @@ -21.8: -Sambut in-app landing screen kami yang baru nan menawan! Anda akan takjub melihatnya seharian dan tidak ingin memublikasikan apa pun. diff --git a/fastlane/metadata/android/id/changelogs/1333.txt b/fastlane/metadata/android/id/changelogs/1333.txt new file mode 100644 index 000000000000..901a02ab16c4 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Saat Anda login ke situs yang dihosting sendiri yang terhubung ke Jetpack dari plugin terpisah, Anda akan melihat pop-up yang menginformasikan bahwa koneksi jenis ini belum mendukung fitur inti aplikasi. Solusinya, Anda dapat beralih ke aplikasi Jetpack. Semudah itu. diff --git a/fastlane/metadata/android/it-IT/changelogs/1333.txt b/fastlane/metadata/android/it-IT/changelogs/1333.txt new file mode 100644 index 000000000000..5c296cd6f446 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Quando accedi a un sito ospitato personalmente che si connette a Jetpack tramite singoli plugin, vedrai un pop-up che indica che questo tipo di connessione non supporta ancora le funzionalità principali dell'app. Puoi aggirare il problema passando all'app Jetpack. Un lampo e via. diff --git a/fastlane/metadata/android/iw-IL/changelogs/1333.txt b/fastlane/metadata/android/iw-IL/changelogs/1333.txt new file mode 100644 index 000000000000..58355877758c --- /dev/null +++ b/fastlane/metadata/android/iw-IL/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +כאשר מתחברים לאתר באחסון עצמי שמתחבר אל Jetpack דרך תוספים יחידים, תופיע הודעה קופצת כדי לידע אותך שסוג החיבור הזה עדיין לא תומך באפשרויות הליבה של האפליקציה. אפשר לעקוף את הבעיה על ידי מעבר לאפליקציה של Jetpack. זה הזמן להמריא. diff --git a/fastlane/metadata/android/ja-JP/changelogs/1333.txt b/fastlane/metadata/android/ja-JP/changelogs/1333.txt new file mode 100644 index 000000000000..ec5cb3fdd2fa --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +個別のプラグインで Jetpack に接続するインストール型サイトにログインすると、このタイプの接続がアプリのコア機能に未対応であることを示すポップアップが表示されます。 Jetpack アプリに切り替えると、そのような問題もなくずっと快適です。 diff --git a/fastlane/metadata/android/ko-KR/changelogs/1333.txt b/fastlane/metadata/android/ko-KR/changelogs/1333.txt new file mode 100644 index 000000000000..96cfaf445856 --- /dev/null +++ b/fastlane/metadata/android/ko-KR/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +개별 플러그인을 통해 젯팩에 연결되는 독립 호스트 사이트에 로그인하면 이 연결 유형에서는 아직 앱의 핵심 기능이 지원되지 않는다는 팝업 메시지가 표시됩니다. 젯팩 앱으로 전환하여 해당 문제를 해결할 수 있습니다. 문제가 멀리 사라집니다. diff --git a/fastlane/metadata/android/nl-NL/changelogs/1333.txt b/fastlane/metadata/android/nl-NL/changelogs/1333.txt new file mode 100644 index 000000000000..fdda4de3ad32 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Wanneer je inlogt op een zelfstandig gehoste website die individuele plugins gebruikt om verbinding te maken met Jetpack, zul je een pop-up scherm zien waarop staat dat deze verbindingssoort nog niet de kernfuncties van de app ondersteunt. Je kunt dit oplossen door over te schakelen naar de Jetpack-app. Daar gaan we. diff --git a/fastlane/metadata/android/pl-PL/changelogs/1317.txt b/fastlane/metadata/android/pl-PL/changelogs/1317.txt deleted file mode 100644 index c8ac6fc878b8..000000000000 --- a/fastlane/metadata/android/pl-PL/changelogs/1317.txt +++ /dev/null @@ -1,2 +0,0 @@ -21.8: -Przywitaj się z naszym nowym, bajeranckim ekranem docelowym w aplikacji! Być może będziesz chcieć patrzeć na niego przez cały dzień i nie publikować niczego nowego. Być może. diff --git a/fastlane/metadata/android/ru-RU/changelogs/1333.txt b/fastlane/metadata/android/ru-RU/changelogs/1333.txt new file mode 100644 index 000000000000..471258e07177 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Входя на автономный сайт с подключением к Jetpack через отдельные плагины, вы увидите всплывающее окно с уведомлением о том, что этот тип подключения пока не поддерживает основные функции приложения. Этой проблемы можно избежать, если переключиться на приложение Jetpack. Хоп-хоп, и выбрались. diff --git a/fastlane/metadata/android/sv-SE/changelogs/1333.txt b/fastlane/metadata/android/sv-SE/changelogs/1333.txt new file mode 100644 index 000000000000..e63076a55654 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +När du loggar in på en webbplats som drivs på egen server och ansluter till Jetpack via enskilda tillägg visas en popup som meddelar att denna typ av anslutning inte stöder appens kärnfunktioner ännu. Du kan kringgå det här problemet genom att växla över till Jetpack-appen. Upp, upp och iväg. diff --git a/fastlane/metadata/android/tr-TR/changelogs/1333.txt b/fastlane/metadata/android/tr-TR/changelogs/1333.txt new file mode 100644 index 000000000000..b2c3712f2c71 --- /dev/null +++ b/fastlane/metadata/android/tr-TR/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +Bireysel eklentiler aracılığıyla Jetpack'e bağlanan, kendi kendine barındırılan bir sitede oturum açtığınızda, bu tür bir bağlantının uygulamanın temel özelliklerini henüz desteklemediğini belirten bir açılır pencere göreceksiniz. Jetpack uygulamasına geçerek bu sorunu çözebilirsiniz. Yukarı, yukarı ve uzağa. diff --git a/fastlane/metadata/android/zh-CN/changelogs/1333.txt b/fastlane/metadata/android/zh-CN/changelogs/1333.txt new file mode 100644 index 000000000000..38f60bac7913 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +当您登录到通过各个插件连接到 Jetpack 的自托管站点时,您可能会看到一个弹出窗口,指出该应用程序的核心功能不支持这种类型的连接。 您可以切换到 Jetpack 应用程序来解决这个问题,从而快速恢复正常。 diff --git a/fastlane/metadata/android/zh-TW/changelogs/1333.txt b/fastlane/metadata/android/zh-TW/changelogs/1333.txt new file mode 100644 index 000000000000..3fc1d7f19b36 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/1333.txt @@ -0,0 +1,2 @@ +22.1: +登入透過個別外掛程式連結至 Jetpack 的自助託管網站時,你會看到彈出視窗說明這類連結尚未支援應用程式的核心功能。 切換到 Jetpack 應用程式就能解決問題。海闊天空。 diff --git a/fastlane/resources/values/strings.xml b/fastlane/resources/values/strings.xml index 147ee3cca6ef..9f7df1ab3409 100644 --- a/fastlane/resources/values/strings.xml +++ b/fastlane/resources/values/strings.xml @@ -1072,35 +1072,25 @@ No network available To view your stats, log in to the WordPress.com account. - + Jetpack - Install Jetpack - Your website credentials will not be stored and are used only for the purpose of installing Jetpack. - Installing Jetpack - Installing Jetpack on your site. This can take up to a few minutes to complete. - Jetpack installed - Now that Jetpack is installed, we just need to get you set up. This will only take a minute. - There was a problem - Jetpack could not be installed at this time. - Set up - Retry Log in to the WordPress.com account you used to connect Jetpack. - - - Jetpack icon - Install Jetpack - Your website credentials will not be stored and are used only for the purpose of installing Jetpack. - Continue - Installing Jetpack - Installing Jetpack on your site. This can take up to a few minutes to complete. - Jetpack installed - Ready to use this site with the app. - Done - Error icon - There was a problem - Jetpack could not be installed at this time. - Retry - Contact Support + Jetpack icon + Install Jetpack + Your website credentials will not be stored and are used only for the purpose of installing Jetpack. + Continue + Installing Jetpack + Installing Jetpack on your site. This can take up to a few minutes to complete. + Jetpack installed + Ready to use this site with the app. + Now that Jetpack is installed, we just need to get you set up. This will only take a minute. + Done + Set up + Error icon + There was a problem + Jetpack could not be installed at this time. + Retry + Contact Support Activity Log @@ -1507,6 +1497,15 @@ Follows Likes + + Fix + Dismiss notification permission warning. + Push notifications are turned off. + Push notifications are turned off + You\'ll need to open the app to see notifications. + Go to Settings → Notifications → App Settings, and turn %1$s on to be notified immediately. + Turn on notifications + Notification Settings On @@ -2261,6 +2260,11 @@ WordPress for Android Support + Content + Traffic + Manage + WP Admin + External Configuration Look and Feel @@ -2356,10 +2360,8 @@ %d answers View all responses - This site is using an individual plugin, which doesn\'t support all features of the app yet. Please install the full Jetpack plugin. - - Please delete the WordPress app + Welcome to the Jetpack app. You can uninstall the WordPress app. Choose site @@ -2811,8 +2813,12 @@ Choose from WordPress Media Library Choose from Tenor %s needs permissions to access your photos + %s needs permission to access your music, audio, photos and videos + %s needs permission to access your photos and videos + %s needs permission to access your photos + %s needs permission to access your videos + %s needs permission to access your audios Allow - %1$s was denied access to your photos. To fix this, edit your permissions and turn on %2$s. Use this photo Use this video Use this audio @@ -2832,7 +2838,7 @@ Media insert failed: %s Media insert failed. Media loading failed - %1$s was denied access to your photos. To fix this, edit your permissions and turn on %2$s and %3$s. + %1$s was denied access to your media files. To fix this, edit your permissions and turn on %2$s. Powered by Tenor @@ -2857,6 +2863,10 @@ Permissions It looks like you turned off permissions required for this feature.<br/><br/>To change this, edit your permissions and make sure <strong>%s</strong> is enabled. Storage + Photos and videos + @string/permission_images + Music and audio + Photos and videos & Music and audio Camera Microphone @@ -3339,6 +3349,8 @@ Recommended Sale This domain is already registered + Purchase domain + Continue with subdomain No available addresses matching your search Your search includes characters not supported in WordPress.com domains. The following characters are allowed: A–Z, a–z, 0–9. This domain is unavailable @@ -4065,6 +4077,10 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Notify me Update Done + Turn on push notifications + To use blogging reminders, you\'ll need to turn on push notifications. + @string/notifications_permission_bottom_sheet_description_2 + @string/notifications_permission_bottom_sheet_button All set! Reminders removed! You\'ll get reminders to blog %1$s a week on %2$s at %3$s. @@ -4304,6 +4320,12 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Check your email on this device! We just sent a magic link to Link Rel + %s has moved to the Jetpack app. + %s have moved to the Jetpack app. + Switch to the Jetpack app + Learn more at Jetpack.com + Stats, Reader, Notifications and other Jetpack powered features have been removed from the WordPress app, and can now only be found in the Jetpack app. + Switching is free and only takes a minute. Welcome to Jetpack! @@ -4318,7 +4340,7 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> We\'ll turn off notifications from the WordPress app. Thanks for switching to Jetpack! We’ve transferred all your data and settings. Everything is right where you left it. - Please <b>delete the WordPress app</b> to avoid data conflicts. + We recommend <b>uninstalling the WordPress app</b> on your device to avoid data conflicts. Remove WordPress App icon Finish Try again @@ -4328,9 +4350,9 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Unable to connect to the internet. Please check to make sure your network connection is working and try again. We are unable to transfer your data and settings without a network connection. - You no longer need the WordPress app - It looks like you still have the WordPress app installed. We recommend you delete the WordPress app to avoid data conflicts. - Please <b>delete the WordPress app</b> to avoid data conflicts. + You no longer need the WordPress app on your device + It looks like you still have the WordPress app installed. + We recommend <b>uninstalling the WordPress app</b> on your device to avoid data conflicts. Got it Need help? @@ -4372,6 +4394,8 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Get notifications for new comments, likes, views, and more. The Jetpack mobile app is designed to work in companion with the Jetpack plugin. Switch now to get access to stats, notifications, reader, and more. Your site has the Jetpack plugin + Moving to the Jetpack app in a few days. + urilinks @@ -4392,7 +4416,7 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Stats, Reader, Notifications and other features will soon move to the Jetpack mobile app. - Unlock your site’s full potential. Get stats, notications and more with Jetpack. + Unlock your site’s full potential. Get stats, notifications and more with Jetpack. @string/learn_more Remind me later Featured @@ -4403,19 +4427,40 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> Please install the full Jetpack plugin + %1$s is using %2$s, which doesn’t support all features of the app yet.\n\nPlease install the %3$s to use the app with this site. + + %1$s is using %2$s, which don’t support all features of the app yet.\n\nPlease install the %3$s to use the app with this site. + - %1$s is using %2$s, which don’t support all features of the app yet.\n\nPlease install the %3$s to use the app with this site. + %1$s is using %2$s, which doesn’t support all features of the app yet. Please install the %3$s. + + %1$s is using %2$s, which don’t support all features of the app yet. Please install the %3$s. the %1$s plugin individual Jetpack plugins full Jetpack plugin + This site By setting up Jetpack you agree to our Terms and conditions Install the full plugin Contact support Close + + Own your online identity with a custom domain + Stake your claim on your corner of the web with a site address that\'s easy to find, share, and follow. + Hide this + Promote your content with Blaze Display your work across millions of sites. @@ -4436,4 +4481,44 @@ translators: %s: Select control option value e.g: "Auto, 25%". --> @string/label_done_button @string/blaze_activity_title + + + Switch to the Jetpack app + Please switch to the Jetpack app where we’ll guide you through connecting the full Jetpack plugin to use this site with the app. + + Unable to access one of your sites + Unable to access some of your sites + + + <b>%1$s</b> is using the <b>%2$s</b> plugin, which isn’t supported by the WordPress app. + + <b>%1$s</b> is using individual Jetpack plugins, which aren’t supported by the WordPress app. + Sites with individual Jetpack plugins aren’t supported by the WordPress app. + + <b>%1$s</b> is using the <b>%2$s</b> plugin + + <b>%1$s</b> is using %2$s individual Jetpack plugins + + + @string/pages + Add Pages to your site + Create Another Page + Start with bespoke, mobile friendly layouts + Add description + Add title + Describe the purpose of the image. Leave empty if decorative. + Dynamic + Manual + More + Playback Bar Color + Playback Settings + Privacy and Rating + Remove blocks
diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java index 755a3797b0e6..62b39abc9ebf 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTracker.java @@ -956,6 +956,9 @@ public enum Stat { QRLOGIN_VERIFY_SCAN_AGAIN, JETPACK_POWERED_BANNER_TAPPED, JETPACK_POWERED_BADGE_TAPPED, + REMOVE_STATIC_POSTER_DISPLAYED, + REMOVE_STATIC_POSTER_GET_JETPACK_TAPPED, + REMOVE_STATIC_POSTER_LINK_TAPPED, JETPACK_POWERED_BOTTOM_SHEET_GET_JETPACK_APP_TAPPED, JETPACK_POWERED_BOTTOM_SHEET_CONTINUE_TAPPED, SHARED_LOGIN_START, @@ -1031,7 +1034,14 @@ public enum Stat { BLAZE_FLOW_STARTED, BLAZE_FLOW_COMPLETED, BLAZE_FLOW_CANCELED, - BLAZE_FLOW_ERROR + BLAZE_FLOW_ERROR, + WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_SHOWN, + WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_DISMISSED, + WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_PRIMARY_TAPPED, + DASHBOARD_CARD_DOMAIN_SHOWN, + DASHBOARD_CARD_DOMAIN_TAPPED, + DASHBOARD_CARD_DOMAIN_MORE_MENU_TAPPED, + DASHBOARD_CARD_DOMAIN_HIDDEN } private static final List TRACKERS = new ArrayList<>(); diff --git a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java index e1ac7a6a15b3..2badec984577 100644 --- a/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java +++ b/libs/analytics/src/main/java/org/wordpress/android/analytics/AnalyticsTrackerNosara.java @@ -2405,6 +2405,12 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) { return "jetpack_powered_banner_tapped"; case JETPACK_POWERED_BADGE_TAPPED: return "jetpack_powered_badge_tapped"; + case REMOVE_STATIC_POSTER_DISPLAYED: + return "remove_static_poster_displayed"; + case REMOVE_STATIC_POSTER_GET_JETPACK_TAPPED: + return "remove_static_poster_get_jetpack_tapped"; + case REMOVE_STATIC_POSTER_LINK_TAPPED: + return "remove_static_poster_link_tapped"; case JETPACK_POWERED_BOTTOM_SHEET_GET_JETPACK_APP_TAPPED: return "jetpack_powered_bottom_sheet_get_jetpack_app_tapped"; case JETPACK_POWERED_BOTTOM_SHEET_CONTINUE_TAPPED: @@ -2557,6 +2563,20 @@ public static String getEventNameForStat(AnalyticsTracker.Stat stat) { return "blaze_flow_canceled"; case BLAZE_FLOW_ERROR: return "blaze_flow_error"; + case WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_SHOWN: + return "wp_individual_site_overlay_viewed"; + case WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_DISMISSED: + return "wp_individual_site_overlay_dismissed"; + case WP_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY_PRIMARY_TAPPED: + return "wp_individual_site_overlay_primary_tapped"; + case DASHBOARD_CARD_DOMAIN_SHOWN: + return "direct_domains_purchase_dashboard_card_shown"; + case DASHBOARD_CARD_DOMAIN_TAPPED: + return "direct_domains_purchase_dashboard_card_tapped"; + case DASHBOARD_CARD_DOMAIN_MORE_MENU_TAPPED: + return "direct_domains_purchase_dashboard_card_menu_tapped"; + case DASHBOARD_CARD_DOMAIN_HIDDEN: + return "direct_domains_purchase_dashboard_card_hidden"; } return null; } diff --git a/libs/editor/build.gradle b/libs/editor/build.gradle index dc1130cc7be3..c182ec25a51f 100644 --- a/libs/editor/build.gradle +++ b/libs/editor/build.gradle @@ -89,9 +89,6 @@ dependencies { exclude module: 'react-native' } - // Required Aztec dependencies (they should be included but Jitpack seems to be stripping these out) - implementation "org.jsoup:jsoup:$jsoupVersion" - implementation "org.wordpress:utils:$wordPressUtilsVersion" implementation "androidx.lifecycle:lifecycle-common:$androidxLifecycleVersion" diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java b/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java index 09dedc9b30fb..6ccb1aaf0218 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/EditorFragmentAbstract.java @@ -208,6 +208,7 @@ public interface EditorFragmentListener extends DialogVisibilityProvider { void onAddFileClicked(boolean allowMultipleSelection); void onAddAudioFileClicked(boolean allowMultipleSelection); void onPerformFetch(String path, boolean enableCaching, Consumer onResult, Consumer onError); + void onPerformPost(String path, Map body, Consumer onResult, Consumer onError); void showUserSuggestions(Consumer onResult); void showXpostSuggestions(Consumer onResult); void onGutenbergEditorSetFocalPointPickerTooltipShown(boolean tooltipShown); diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java index 8a239417db0e..96b137e84117 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergContainerFragment.java @@ -182,6 +182,10 @@ public void toggleHtmlMode() { mWPAndroidGlueCode.toggleEditorMode(mHtmlModeEnabled); } + public void sendToJSPostSaveEvent() { + mWPAndroidGlueCode.sendToJSPostSaveEvent(); + } + /** * Returns the contents of the content field from the JavaScript editor. Should be called from a background thread * where possible. diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java index d03a1beab7a9..bb62eecb8ac2 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergEditorFragment.java @@ -32,6 +32,7 @@ import androidx.lifecycle.LiveData; import com.android.volley.toolbox.ImageLoader; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableNativeMap; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.gson.Gson; @@ -61,6 +62,7 @@ import org.wordpress.aztec.IHistoryListener; import org.wordpress.mobile.WPAndroidGlue.Media; import org.wordpress.mobile.WPAndroidGlue.MediaOption; +import org.wordpress.mobile.WPAndroidGlue.RequestExecutor; import org.wordpress.mobile.WPAndroidGlue.ShowSuggestionsUtil; import org.wordpress.mobile.WPAndroidGlue.UnsupportedBlock; import org.wordpress.mobile.WPAndroidGlue.WPAndroidGlueCode.OnBlockTypeImpressionsEventListener; @@ -397,7 +399,21 @@ public void run() { }, mTextWatcher::postTextChanged, mEditorFragmentListener::onAuthHeaderRequested, - mEditorFragmentListener::onPerformFetch, + new RequestExecutor() { + @Override + public void performGetRequest(String path, boolean enableCaching, Consumer onSuccess, + Consumer onError) { + mEditorFragmentListener.onPerformFetch(path, enableCaching, onSuccess, onError); + } + @Override + public void performPostRequest( + String path, + ReadableMap data, + Consumer onSuccess, + Consumer onError) { + mEditorFragmentListener.onPerformPost(path, data.toHashMap(), onSuccess, onError); + } + }, mEditorImagePreviewListener::onImagePreviewRequested, mEditorEditMediaListener::onMediaEditorRequested, new OnGutenbergDidRequestUnsupportedBlockFallbackListener() { @@ -1114,6 +1130,10 @@ private void toggleHtmlMode() { getGutenbergContainerFragment().toggleHtmlMode(); } + public void sendToJSPostSaveEvent() { + getGutenbergContainerFragment().sendToJSPostSaveEvent(); + } + /* * TODO: REMOVE THIS ONCE AZTEC COMPLETELY REPLACES THE VISUAL EDITOR IN WPANDROID APP */ @@ -1258,12 +1278,19 @@ public void appendMediaFiles(Map mediaList) { int mediaId = isNetworkUrl ? Integer.valueOf(mediaEntry.getValue().getMediaId()) : mediaEntry.getValue().getId(); String url = isNetworkUrl ? mediaEntry.getKey() : "file://" + mediaEntry.getKey(); + MediaFile mediaFile = mediaEntry.getValue(); + WritableNativeMap metadata = new WritableNativeMap(); + String videoPressGuid = mediaFile.getVideoPressGuid(); + if (videoPressGuid != null) { + metadata.putString("videopressGUID", videoPressGuid); + } rnMediaList.add(createRNMediaUsingMimeType(mediaId, url, - mediaEntry.getValue().getMimeType(), - mediaEntry.getValue().getCaption(), - mediaEntry.getValue().getTitle(), - mediaEntry.getValue().getAlt())); + mediaFile.getMimeType(), + mediaFile.getCaption(), + mediaFile.getTitle(), + mediaFile.getAlt(), + metadata)); } getGutenbergContainerFragment().appendMediaFiles(rnMediaList); diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergUtils.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergUtils.java index f0a8bdee3ff4..6d95f7da360f 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergUtils.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergUtils.java @@ -1,5 +1,6 @@ package org.wordpress.android.editor.gutenberg; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.res.Configuration; @@ -26,6 +27,7 @@ public static Boolean isDarkMode(Context context) { * * @return Bundle a map of "english string" => [ "current locale string" ] */ + @SuppressLint("AppBundleLocaleChanges") public static Bundle getTranslations(Activity activity) { Bundle translations = new Bundle(); Locale defaultLocale = new Locale("en"); diff --git a/libs/editor/src/main/res/layout/alert_create_link.xml b/libs/editor/src/main/res/layout/alert_create_link.xml index 77d25c8fa948..56aca3b8b722 100644 --- a/libs/editor/src/main/res/layout/alert_create_link.xml +++ b/libs/editor/src/main/res/layout/alert_create_link.xml @@ -1,10 +1,11 @@ - + - + android:layout_height="match_parent" + tools:ignore="UnusedResources"> - + #f9fbfc @color/wp_grey_lighten_10 - @color/wp_grey_lighten_10 + @color/wp_grey_lighten_10 @color/wp_grey - @color/wp_grey + @color/wp_grey @color/wp_grey_lighten_30 - #ff006b98 + #ff006b98 @color/wp_grey_darken_30 @color/text @color/wp_grey_light @color/wp_grey_darken_10 - @color/wp_blue_medium + @color/wp_blue_medium @color/wp_grey @color/wp_grey_lighten_30 @color/wp_grey_darken_30 diff --git a/libs/editor/src/main/res/values/dimens.xml b/libs/editor/src/main/res/values/dimens.xml index 869fbfab2ecb..75e91040cdec 100644 --- a/libs/editor/src/main/res/values/dimens.xml +++ b/libs/editor/src/main/res/values/dimens.xml @@ -1,6 +1,6 @@ - + 44dp 49dp diff --git a/libs/editor/src/main/res/values/styles.xml b/libs/editor/src/main/res/values/styles.xml index e580de9560c0..170c82247bc5 100755 --- a/libs/editor/src/main/res/values/styles.xml +++ b/libs/editor/src/main/res/values/styles.xml @@ -1,8 +1,6 @@ - + - - - @@ -35,7 +33,7 @@ @color/sourceview_separator -